@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.
package/dist/axi.css ADDED
@@ -0,0 +1,1152 @@
1
+ /* axi design language - generated by scripts/build.mjs.
2
+ Edit the files in src/ and run `npm run build`; do not edit this file. */
3
+
4
+ /* --- tokens.css --- */
5
+ /* axi design language - tokens.
6
+ The only file in this system permitted to contain a colour literal. Every
7
+ component resolves its colours through the names below, which is what keeps
8
+ a future light theme a palette swap instead of a rewrite.
9
+
10
+ Three layers:
11
+ - surface & text: the neutral ramp the language is drawn on
12
+ - accent & status: the per-app override surface. An app that sets
13
+ --axi-accent and nothing else is correctly themed.
14
+ - form: geometry and type. Shared by every property; overriding these
15
+ means leaving the design language, not theming it. */
16
+ :root {
17
+ /* --- surface & text --- */
18
+ --axi-ground: #15181d; /* the page itself */
19
+ --axi-surface: #222731; /* a panel raised off the page */
20
+ --axi-surface-raised: #2b313d; /* a chip or popover raised off a panel */
21
+ --axi-ink-line: #0c0e12; /* the outline every raised element is drawn with */
22
+ --axi-rule: #3a4250; /* internal rules, inside an outlined panel */
23
+ --axi-text: #f4f6f9;
24
+ --axi-text-dim: #a7b0be;
25
+ --axi-text-faint: #7c8695;
26
+ --axi-scrim: rgba(6, 7, 9, .72); /* behind a drawer */
27
+
28
+ /* --- accent & status --- */
29
+ --axi-accent: #ffc53d;
30
+ /* Text drawn on top of an accent fill. Near-black suits every accent bright
31
+ enough to pass contrast on this ground; an app choosing a dark accent
32
+ overrides this to --axi-text rather than editing components. */
33
+ --axi-accent-ink: var(--axi-ink-line);
34
+ --axi-meta: #4ec3ff; /* the one cool ink: meta only, never a status */
35
+ --axi-ok: #2fd38a;
36
+ --axi-warn: #ff7a2f;
37
+ --axi-danger: #ff5252;
38
+
39
+ /* --- form: outline and offset --- */
40
+ /* Two steps, and only two. A panel is drawn heavier than a control so a
41
+ toolbar full of controls still reads as sitting inside its panel. */
42
+ --axi-border-panel: 4px;
43
+ --axi-offset-panel: 6px;
44
+ --axi-border-control: 3px;
45
+ --axi-offset-control: 3px;
46
+ /* The deepened blocks a hover lifts into. Not a third and fourth step: a
47
+ step is a border+offset pair a thing rests at, and nothing rests at
48
+ these. They are tokenised so the offset check can enumerate every legal
49
+ block instead of admitting bare literals. */
50
+ --axi-offset-panel-hover: 10px;
51
+ --axi-offset-control-hover: 6px;
52
+ --axi-radius: 10px;
53
+ --axi-radius-sm: 6px;
54
+ /* Not a form step: a hairline used inside prose (inline code, table rules,
55
+ the list bullet) where the two form steps above would read as too heavy
56
+ for a line of running text. */
57
+ --axi-border-hairline: 2px;
58
+
59
+ /* --- form: measure --- */
60
+ --axi-page: 1280px; /* the default reading//browsing width */
61
+ --axi-page-narrow: 46rem; /* prose */
62
+ --axi-page-wide: 1660px; /* dense card grids */
63
+ --axi-gutter: 18px;
64
+
65
+ /* --- form: type --- */
66
+ --axi-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI Variable", "Segoe UI", Roboto, sans-serif;
67
+ --axi-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
68
+ /* Font shorthands, so a component sets `font: var(--axi-t-h2)` in one
69
+ declaration. Tracking cannot ride in the shorthand, so the headings that
70
+ need it carry a paired --axi-ls-* token. */
71
+ --axi-t-display: 900 54px/1.02 var(--axi-sans);
72
+ --axi-ls-display: -.045em;
73
+ --axi-t-h1: 900 30px/1.1 var(--axi-sans);
74
+ --axi-ls-h1: -.035em;
75
+ --axi-t-h2: 900 24px/1.15 var(--axi-sans);
76
+ --axi-ls-h2: -.035em;
77
+ --axi-t-h3: 900 19px/1.2 var(--axi-sans);
78
+ --axi-ls-h3: -.025em;
79
+ --axi-t-body: 400 15px/1.55 var(--axi-sans);
80
+ --axi-t-small: 400 13.5px/1.5 var(--axi-sans);
81
+ --axi-t-label: 800 13px/1 var(--axi-sans);
82
+ --axi-ls-label: .02em;
83
+ --axi-t-micro: 800 11px/1 var(--axi-sans);
84
+ --axi-ls-micro: .04em;
85
+ --axi-t-eyebrow: 900 11.5px/1 var(--axi-sans);
86
+ --axi-ls-eyebrow: .1em;
87
+ }
88
+
89
+ /* --- base.css --- */
90
+ /* axi design language - base.
91
+ Element-level defaults every axi property inherits. Nothing here is a
92
+ component; if it needs a class, it belongs in a later layer. */
93
+
94
+ *, *::before, *::after { box-sizing: border-box; }
95
+
96
+ /* Dark is the only theme shipped today. Declaring the scheme means form
97
+ controls, scrollbars and the like come up dark from the first paint rather
98
+ than flashing light, and it is the one line a light theme will flip. */
99
+ html { color-scheme: dark; }
100
+
101
+ body {
102
+ margin: 0;
103
+ background: var(--axi-ground);
104
+ color: var(--axi-text);
105
+ font: var(--axi-t-body);
106
+ -webkit-font-smoothing: antialiased;
107
+ }
108
+
109
+ /* Links inherit their colour by default: in this language a link is usually
110
+ inside something that has already chosen an ink, and a globally accented
111
+ link would fight every card title and nav item. Components opt into the
112
+ accent where a link should read as one. */
113
+ a { color: inherit; }
114
+
115
+ /* The focus ring is accent-coloured and thick enough to read against a
116
+ near-black outline, which a 1px ring does not. It is drawn entirely with
117
+ `outline`, which follows whatever radius the element already has: a
118
+ `border-radius` here would reshape the focused element itself, which was
119
+ harmless for our components only because each one's own radius happens to
120
+ land later in the build ORDER, and visibly wrong for an unclassed
121
+ checkbox or link. */
122
+ :focus-visible {
123
+ outline: var(--axi-border-control) solid var(--axi-accent);
124
+ outline-offset: 2px;
125
+ }
126
+
127
+ .axi-sr-only {
128
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
129
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
130
+ }
131
+
132
+ /* Everything this language animates is the hover lift, and the lift is the
133
+ one thing a reader who has asked for less motion is asking not to see.
134
+ Neutralise the movement, not the appearance: the resting transforms - the
135
+ diamond's rotation, the sigil, the search glyph's centring - are geometry
136
+ rather than motion and are deliberately untouched, so nothing looks
137
+ different until you point at it. The !important is what lets this sit in
138
+ the base layer and still outrank component hover rules that come later in
139
+ the build ORDER at equal specificity. */
140
+ @media (prefers-reduced-motion: reduce) {
141
+ *, *::before, *::after {
142
+ transition-duration: .01ms !important;
143
+ animation-duration: .01ms !important;
144
+ animation-iteration-count: 1 !important;
145
+ scroll-behavior: auto !important;
146
+ }
147
+ .axi-btn:hover,
148
+ .axi-pill:hover,
149
+ .axi-pill[aria-pressed="true"]:hover,
150
+ .axi-select:hover,
151
+ .axi-card:hover,
152
+ .axi-drawer__close:hover {
153
+ transform: none !important;
154
+ }
155
+ }
156
+
157
+ /* --- primitives.css --- */
158
+ /* axi design language - primitives.
159
+ The pieces that recur in every property. Two rules govern the whole file:
160
+ a raised thing is outlined in --axi-ink-line and carries a hard offset
161
+ block, never a blur; and a filled chip asserts a value while an outlined
162
+ one annotates. */
163
+
164
+ /* ---------- panel ---------- */
165
+ /* The surface every larger component is built on. Kept as its own class so a
166
+ consumer can raise an arbitrary block into the language without waiting for
167
+ us to ship a component for it. */
168
+ .axi-panel {
169
+ background: var(--axi-surface);
170
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
171
+ border-radius: var(--axi-radius);
172
+ box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
173
+ /* A panel owns its padding so every consumer doesn't have to invent one;
174
+ override per instance with --axi-panel-pad where a tighter or looser
175
+ fit is genuinely called for. */
176
+ padding: var(--axi-panel-pad, 26px);
177
+ }
178
+
179
+ /* ---------- button ---------- */
180
+ .axi-btn {
181
+ display: inline-flex; align-items: center; gap: 8px;
182
+ padding: 12px 20px;
183
+ border: var(--axi-border-control) solid var(--axi-ink-line);
184
+ border-radius: 8px;
185
+ background: var(--axi-ground);
186
+ color: var(--axi-text-dim);
187
+ font: var(--axi-t-label);
188
+ letter-spacing: var(--axi-ls-label);
189
+ text-transform: uppercase;
190
+ text-decoration: none;
191
+ cursor: pointer;
192
+ transition: transform .1s, box-shadow .1s;
193
+ }
194
+ /* Hover lifts. Nothing in this language fades or glows: the block deepens and
195
+ the element moves against it, which reads instantly even in peripheral
196
+ vision and costs no colour. */
197
+ .axi-btn:hover {
198
+ color: var(--axi-text);
199
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
200
+ transform: translate(-2px, -2px);
201
+ }
202
+ .axi-btn--primary {
203
+ background: var(--axi-accent);
204
+ color: var(--axi-accent-ink);
205
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
206
+ }
207
+ /* The one button that rests with a block already under it, so the generic
208
+ hover's translate alone would move element and block together and leave the
209
+ lower-right edge where it started - the "grows rather than lifts" failure
210
+ rule 4 warns about. It lifts the way a panel does: the block deepens too. */
211
+ .axi-btn--primary:hover {
212
+ color: var(--axi-accent-ink);
213
+ box-shadow: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
214
+ }
215
+ .axi-btn--ghost { background: transparent; }
216
+ .axi-btn--dashed { background: transparent; border-style: dashed; }
217
+
218
+ /* ---------- pill ---------- */
219
+ /* A filter toggle. Its pressed state fills with whatever colour the consumer
220
+ has put on it, so the control reads as the thing it filters to rather than
221
+ as a generic "selected". Consumers set that colour with --axi-pill-fill,
222
+ on the pill or on any ancestor - it is read through a fallback and never
223
+ declared on the component, which is what keeps both placements working. */
224
+ .axi-pill {
225
+ display: inline-flex; align-items: center; gap: 6px;
226
+ padding: 10px 9px;
227
+ border: var(--axi-border-control) solid var(--axi-ink-line);
228
+ border-radius: 8px;
229
+ background: var(--axi-ground);
230
+ color: var(--axi-text-dim);
231
+ font: var(--axi-t-label);
232
+ letter-spacing: var(--axi-ls-label);
233
+ text-transform: uppercase;
234
+ cursor: pointer;
235
+ transition: transform .1s, box-shadow .1s;
236
+ }
237
+ .axi-pill:hover {
238
+ color: var(--axi-text);
239
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
240
+ transform: translate(-2px, -2px);
241
+ }
242
+ .axi-pill[aria-pressed="true"] {
243
+ background: var(--axi-pill-fill, var(--axi-accent));
244
+ color: var(--axi-accent-ink);
245
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
246
+ }
247
+ /* Spelled out rather than left to source order: `.axi-pill:hover` and
248
+ `.axi-pill[aria-pressed="true"]` have identical specificity, so without this
249
+ rule a pressed pill under the cursor kept its pressed block while still
250
+ taking the hover's translate - a slide, not a lift, decided by which rule
251
+ happened to come last in the file. A pressed pill keeps its fill and lifts. */
252
+ .axi-pill[aria-pressed="true"]:hover {
253
+ background: var(--axi-pill-fill, var(--axi-accent));
254
+ color: var(--axi-accent-ink);
255
+ box-shadow: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
256
+ transform: translate(-2px, -2px);
257
+ }
258
+
259
+ /* ---------- switch ---------- */
260
+ /* On/off for a setting, where a pill's "pressed" is not enough: a pill says
261
+ which of several filters you chose, a switch says what state one thing is
262
+ in when nothing else is on screen to compare it against. Rule 5 is what
263
+ makes it legible - the filled track asserts the setting's status, and the
264
+ status is the only thing that changes between the two states.
265
+
266
+ It is a slot with something sitting in it, so it takes no block: the block
267
+ belongs to things you press. The track carries a full ink edge at the
268
+ control weight and the raised surface, because an off switch inside a panel
269
+ is a surface on a surface - without the edge the track vanishes and all you
270
+ see is a slug floating in the card.
271
+
272
+ The slug is ink in both states. That is deliberate: on and off differ in
273
+ what colour is in the slot, never in how bright the part that moves is. */
274
+ .axi-switch {
275
+ position: relative;
276
+ flex: none;
277
+ display: inline-block;
278
+ width: var(--axi-switch-w, 46px);
279
+ height: var(--axi-switch-h, 26px);
280
+ padding: 0;
281
+ border: var(--axi-border-control) solid var(--axi-ink-line);
282
+ border-radius: var(--axi-radius-sm);
283
+ background: var(--axi-surface-raised);
284
+ cursor: pointer;
285
+ }
286
+ /* Read through a fallback and never declared on the component, so a consumer
287
+ can set the on-colour on the switch or on any ancestor - the same contract
288
+ as --axi-pill-fill. A switch that turns something dangerous on can fill in
289
+ --axi-danger and say so. */
290
+ .axi-switch[aria-checked="true"] { background: var(--axi-switch-fill, var(--axi-accent)); }
291
+ /* Pinned to the track's insets rather than given a height and centred, so the
292
+ slug stays correct if the track is resized. Only its travel is a transform,
293
+ which is the one thing rule 11 lets move. */
294
+ .axi-switch__knob {
295
+ position: absolute;
296
+ top: 2px; bottom: 2px; left: 2px;
297
+ width: var(--axi-switch-knob, 16px);
298
+ border-radius: 2px;
299
+ background: var(--axi-ink-line);
300
+ transition: transform .1s;
301
+ }
302
+ /* The travel is the track's inner width less the slug and its two insets.
303
+ Spelled as a calc over the same tokens the track is built from so it stays
304
+ right at any size, and - the part that matters - at any border weight: a
305
+ hard-coded travel measured against one weight puts the slug through the
306
+ right-hand edge the moment rule 3's numbers change. */
307
+ .axi-switch[aria-checked="true"] .axi-switch__knob {
308
+ transform: translateX(calc(
309
+ var(--axi-switch-w, 46px) - 2 * var(--axi-border-control)
310
+ - var(--axi-switch-knob, 16px) - 4px));
311
+ }
312
+
313
+ /* ---------- chip ---------- */
314
+ /* Filled asserts, outlined annotates. The unmodified chip is a neutral fact;
315
+ the status modifiers assert a value; --meta is the only outlined variant and
316
+ is drawn in the reserved cool ink, so a reader can tell commentary from
317
+ data without reading either. */
318
+ .axi-chip {
319
+ display: inline-flex; align-items: center; gap: 6px;
320
+ padding: 5px 9px;
321
+ border: var(--axi-border-control) solid var(--axi-ink-line);
322
+ border-radius: var(--axi-radius-sm);
323
+ background: var(--axi-surface-raised);
324
+ color: var(--axi-text-dim);
325
+ font: var(--axi-t-micro);
326
+ letter-spacing: var(--axi-ls-micro);
327
+ text-transform: uppercase;
328
+ }
329
+ .axi-chip--ok { background: var(--axi-ok); color: var(--axi-ink-line); }
330
+ .axi-chip--warn { background: var(--axi-warn); color: var(--axi-ink-line); }
331
+ .axi-chip--danger { background: var(--axi-danger); color: var(--axi-ink-line); }
332
+ .axi-chip--accent { background: var(--axi-accent); color: var(--axi-accent-ink); }
333
+ .axi-chip--meta {
334
+ background: transparent;
335
+ border-color: var(--axi-meta);
336
+ color: var(--axi-meta);
337
+ }
338
+
339
+ /* ---------- count badge ---------- */
340
+ .axi-badge-count {
341
+ display: inline-block;
342
+ padding: 1px 7px;
343
+ border-radius: 99px;
344
+ background: var(--axi-accent);
345
+ color: var(--axi-accent-ink);
346
+ font: var(--axi-t-micro);
347
+ }
348
+
349
+ /* ---------- diamond ---------- */
350
+ /* The family motif: a rotated outlined square. Used as a bullet, a status dot
351
+ and, scaled up behind a glyph, the brand sigil. */
352
+ .axi-diamond {
353
+ display: inline-block;
354
+ width: 9px; height: 9px; flex: none;
355
+ transform: rotate(45deg);
356
+ background: var(--axi-meta);
357
+ border: var(--axi-border-control) solid var(--axi-ink-line);
358
+ }
359
+ .axi-diamond--accent { background: var(--axi-accent); }
360
+ .axi-diamond--ok { background: var(--axi-ok); }
361
+ .axi-diamond--warn { background: var(--axi-warn); }
362
+ .axi-diamond--danger { background: var(--axi-danger); }
363
+ /* A legend key, where the diamond stands for a series rather than a state.
364
+ Opt-in rather than a default on .axi-diamond: --axi-series is read from
365
+ whatever ancestor sets it, and a chart panel that sets one would otherwise
366
+ silently recolour every bullet inside it. */
367
+ .axi-diamond--series { background: var(--axi-series, var(--axi-accent)); }
368
+
369
+ /* ---------- input ---------- */
370
+ .axi-input {
371
+ width: 100%;
372
+ padding: 11px 12px;
373
+ background: var(--axi-ground);
374
+ border: var(--axi-border-control) solid var(--axi-ink-line);
375
+ border-radius: 8px;
376
+ color: var(--axi-text);
377
+ font: var(--axi-t-label);
378
+ font-size: 14px;
379
+ }
380
+ .axi-input::placeholder { color: var(--axi-text-faint); font-weight: 500; }
381
+
382
+ /* A search input with the magnifier drawn inside it. The glyph is the
383
+ consumer's; this only reserves the room and positions it. */
384
+ .axi-search { position: relative; }
385
+ .axi-search .axi-input { padding-left: 38px; }
386
+ .axi-search__icon {
387
+ position: absolute; left: 13px; top: 50%; transform: translateY(-50%);
388
+ color: var(--axi-accent); font-weight: 900; pointer-events: none;
389
+ }
390
+
391
+ /* ---------- select ---------- */
392
+ /* The closed box is ours everywhere: strip the native control and draw the
393
+ caret, so a select sits alongside the other controls as just another
394
+ outlined chip instead of announcing the OS. */
395
+ .axi-select {
396
+ appearance: none;
397
+ padding: 10px 30px 10px 9px;
398
+ border: var(--axi-border-control) solid var(--axi-ink-line);
399
+ border-radius: 8px;
400
+ background-color: var(--axi-ground);
401
+ color: var(--axi-text-dim);
402
+ font: var(--axi-t-label);
403
+ font-size: 12.5px;
404
+ letter-spacing: var(--axi-ls-label);
405
+ text-transform: uppercase;
406
+ cursor: pointer;
407
+ /* Two triangles meeting: a caret with no image and no icon font. */
408
+ background-image:
409
+ linear-gradient(45deg, transparent 50%, var(--axi-accent) 50%),
410
+ linear-gradient(135deg, var(--axi-accent) 50%, transparent 50%);
411
+ background-position: calc(100% - 16px) calc(50% + 2px), calc(100% - 11px) calc(50% + 2px);
412
+ background-size: 5px 5px, 5px 5px;
413
+ background-repeat: no-repeat;
414
+ transition: transform .1s, box-shadow .1s;
415
+ }
416
+ .axi-select:hover {
417
+ color: var(--axi-text);
418
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
419
+ transform: translate(-2px, -2px);
420
+ }
421
+
422
+ /* The popup stays OS chrome until a browser lets us style it. Where one does
423
+ (Chromium's base-select), the list is drawn with the same outline and offset
424
+ block as our own popovers, so both dropdown kinds read as one family; where
425
+ it does not, the closed box above is still ours and the list is native. */
426
+ @supports (appearance: base-select) {
427
+ /* base-select draws its own ::picker-icon, so the hand-drawn caret above
428
+ would be a second arrow. */
429
+ .axi-select, .axi-select::picker(select) { appearance: base-select; }
430
+ .axi-select { background-image: none; padding-right: 9px; }
431
+ .axi-select::picker-icon { color: var(--axi-accent); transition: none; }
432
+ .axi-select::picker(select) {
433
+ margin-top: 9px; padding: 6px;
434
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
435
+ border-radius: 9px;
436
+ background: var(--axi-surface-raised);
437
+ box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
438
+ }
439
+ .axi-select option {
440
+ display: flex; align-items: center; gap: 9px;
441
+ padding: 8px 9px;
442
+ border-radius: var(--axi-radius-sm);
443
+ background: transparent;
444
+ color: var(--axi-text-dim);
445
+ font: var(--axi-t-label);
446
+ font-size: 12.5px;
447
+ letter-spacing: var(--axi-ls-label);
448
+ text-transform: uppercase;
449
+ }
450
+ .axi-select option:hover, .axi-select option:focus {
451
+ background: var(--axi-ground); color: var(--axi-text);
452
+ }
453
+ /* The page-wide focus ring sits 2px outside its element; inside a picker
454
+ that is 2px into the neighbouring row, so pull it back in. */
455
+ .axi-select option:focus-visible { outline-offset: -3px; }
456
+ .axi-select option:checked { color: var(--axi-text); }
457
+ .axi-select option::checkmark { content: "\2713"; color: var(--axi-accent); font-weight: 900; }
458
+ }
459
+
460
+ /* --- layout.css --- */
461
+ /* axi design language - layout.
462
+ Three measures, one grid, two spacing helpers. Deliberately small: a
463
+ layout system large enough to express any page is a framework, and every
464
+ property here can reach for plain CSS grid the moment it needs something
465
+ these do not cover. */
466
+
467
+ /* The page wrapper. --axi-page is the default because most axi surfaces are
468
+ browsing views; the two modifiers exist because prose and dense catalogs
469
+ genuinely disagree about measure, and hard-coding either default made one
470
+ of them wrong. */
471
+ .axi-page {
472
+ max-width: var(--axi-page);
473
+ margin-inline: auto;
474
+ /* The gutter is a knob because measures nest: a --narrow prose column or a
475
+ --wide grid placed inside a page that has already paid the gutter would
476
+ otherwise pay it twice, with no way to say so but an inline
477
+ `padding-inline: 0`. Set --axi-page-pad: 0 on the inner one. */
478
+ padding-inline: var(--axi-page-pad, var(--axi-gutter));
479
+ }
480
+ /* Prose. Beyond this measure a line of body text gets hard to track back to
481
+ the start of the next one. */
482
+ .axi-page--narrow { max-width: var(--axi-page-narrow); }
483
+ /* Dense card grids, where width spent on margins is a column not shown. */
484
+ .axi-page--wide { max-width: var(--axi-page-wide); }
485
+
486
+ /* Auto-fill card grid. The minimum column width is per-instance rather than
487
+ global: a grid of ten app cards and a grid of sixty catalog entries want
488
+ genuinely different minimums, and both are this same component. */
489
+ .axi-grid {
490
+ display: grid;
491
+ grid-template-columns: repeat(auto-fill, minmax(var(--axi-grid-min, 300px), 1fr));
492
+ gap: var(--axi-gutter);
493
+ }
494
+
495
+ /* Vertical rhythm between siblings, set per-instance. */
496
+ .axi-stack { display: flex; flex-direction: column; gap: var(--axi-stack-gap, 12px); }
497
+ /* A horizontal run that wraps rather than overflowing. */
498
+ .axi-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--axi-row-gap, 10px); }
499
+
500
+ @media (max-width: 640px) {
501
+ /* The fallback must match the resting rule's (var(--axi-gutter), 18px) -
502
+ README.md documents --axi-page-pad's fallback as --axi-gutter, and a
503
+ literal here silently ignores a consumer's --axi-gutter override on
504
+ mobile, which is the one viewport where the gutter matters most. */
505
+ .axi-page { padding-inline: var(--axi-page-pad, var(--axi-gutter)); }
506
+ .axi-grid { grid-template-columns: 1fr; }
507
+ }
508
+
509
+ /* --- shells.css --- */
510
+ /* axi design language - shells.
511
+ Assembled components. Each is built from the primitives rather than
512
+ redefining them, so a change to .axi-panel moves the notice, the toolbar,
513
+ the popover and the card together. */
514
+
515
+ /* The small uppercase heading that labels a panel's contents. */
516
+ .axi-eyebrow {
517
+ margin: 0 0 14px;
518
+ font: var(--axi-t-eyebrow);
519
+ letter-spacing: var(--axi-ls-eyebrow);
520
+ text-transform: uppercase;
521
+ color: var(--axi-text-faint);
522
+ }
523
+
524
+ /* ---------- masthead ---------- */
525
+ .axi-mast {
526
+ position: sticky; top: 0; z-index: 40;
527
+ background: var(--axi-ground);
528
+ border-bottom: var(--axi-border-panel) solid var(--axi-ink-line);
529
+ }
530
+ .axi-mast__in {
531
+ max-width: var(--axi-page); margin-inline: auto;
532
+ padding-inline: var(--axi-gutter);
533
+ display: flex; align-items: center; gap: 22px; min-height: 66px; flex-wrap: wrap;
534
+ }
535
+ .axi-brand { display: flex; align-items: center; gap: 12px; text-decoration: none; }
536
+ /* The sigil is the diamond motif scaled up with a glyph on top. The diamond is
537
+ a rotated pseudo-element behind the text rather than a rotated box, so the
538
+ glyph stays upright. isolation keeps the negative z-index inside the sigil
539
+ instead of dropping it behind the masthead. */
540
+ .axi-sigil {
541
+ position: relative; isolation: isolate;
542
+ width: 34px; height: 34px; flex: none;
543
+ display: grid; place-items: center;
544
+ color: var(--axi-accent-ink); font-weight: 900; font-size: 13px;
545
+ }
546
+ .axi-sigil::before {
547
+ content: ""; position: absolute; inset: 0; z-index: -1;
548
+ transform: rotate(45deg);
549
+ border-radius: 5px;
550
+ background: var(--axi-accent);
551
+ border: var(--axi-border-control) solid var(--axi-ink-line);
552
+ }
553
+ .axi-brand__name { font: 800 17px/1.15 var(--axi-sans); letter-spacing: -.02em; }
554
+ .axi-brand__name small {
555
+ display: block;
556
+ font: 700 10.5px/1.4 var(--axi-sans);
557
+ color: var(--axi-text-faint);
558
+ letter-spacing: .09em; text-transform: uppercase;
559
+ }
560
+ .axi-tabs { display: flex; gap: 8px; margin-left: auto; }
561
+ .axi-tabs a {
562
+ padding: 8px 16px;
563
+ font: 800 13.5px/1 var(--axi-sans);
564
+ color: var(--axi-text-dim); text-decoration: none;
565
+ border: var(--axi-border-control) solid transparent;
566
+ border-radius: 8px;
567
+ }
568
+ .axi-tabs a:hover { color: var(--axi-text); border-color: var(--axi-rule); }
569
+ /* The current tab is filled and blocked - the same treatment a pressed pill
570
+ gets, because it is the same idea: this one is on. */
571
+ .axi-tabs a[aria-current="page"] {
572
+ background: var(--axi-accent);
573
+ color: var(--axi-accent-ink);
574
+ border-color: var(--axi-ink-line);
575
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
576
+ }
577
+
578
+ /* ---------- notice ---------- */
579
+ .axi-notice {
580
+ display: flex; gap: 14px; padding: 15px 18px;
581
+ background: var(--axi-surface);
582
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
583
+ border-radius: var(--axi-radius);
584
+ box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
585
+ }
586
+ .axi-notice__icon {
587
+ flex: none; width: 26px; height: 26px;
588
+ display: grid; place-items: center;
589
+ border-radius: var(--axi-radius-sm);
590
+ background: var(--axi-accent); color: var(--axi-accent-ink);
591
+ font-size: 14px; font-weight: 900;
592
+ border: var(--axi-border-control) solid var(--axi-ink-line);
593
+ }
594
+ .axi-notice p { margin: 0; font: var(--axi-t-small); color: var(--axi-text-dim); }
595
+ .axi-notice b { color: var(--axi-accent); font-weight: 800; }
596
+
597
+ /* ---------- toolbar ---------- */
598
+ .axi-toolbar {
599
+ display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
600
+ padding: 14px;
601
+ background: var(--axi-surface);
602
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
603
+ border-radius: var(--axi-radius);
604
+ box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
605
+ }
606
+
607
+ /* ---------- menu popover ---------- */
608
+ /* A disclosure, not a permanent row of controls. The trigger is an .axi-btn
609
+ with aria-expanded; this styles the panel it opens. */
610
+ .axi-menu { position: relative; }
611
+ .axi-menu__pop {
612
+ /* Above .axi-mast's z-index: 40. Both sit in the root stacking context, so
613
+ a toolbar scrolled under the sticky masthead would otherwise open its
614
+ menu behind it. */
615
+ position: absolute; top: calc(100% + 9px); left: 0; z-index: 41;
616
+ width: var(--axi-menu-width, 310px);
617
+ max-height: 340px; overflow-y: auto; padding: 8px;
618
+ background: var(--axi-surface-raised);
619
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
620
+ border-radius: 9px;
621
+ box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
622
+ }
623
+ .axi-menu__pop[hidden] { display: none; }
624
+ .axi-menu__pop label {
625
+ display: flex; gap: 9px; align-items: flex-start;
626
+ padding: 7px 8px; border-radius: var(--axi-radius-sm);
627
+ font-size: 13px; font-weight: 600; color: var(--axi-text-dim); cursor: pointer;
628
+ }
629
+ .axi-menu__pop label:hover { background: var(--axi-ground); color: var(--axi-text); }
630
+ .axi-menu__pop input { margin: 3px 0 0; accent-color: var(--axi-accent); }
631
+
632
+ /* ---------- card ---------- */
633
+ .axi-card {
634
+ position: relative; overflow: hidden;
635
+ width: 100%; text-align: left;
636
+ display: flex; flex-direction: column; gap: 11px;
637
+ padding: 22px 16px 14px;
638
+ background: var(--axi-surface);
639
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
640
+ border-radius: var(--axi-radius);
641
+ box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
642
+ font: inherit; color: inherit; text-decoration: none; cursor: pointer;
643
+ transition: transform .1s, box-shadow .1s;
644
+ }
645
+ .axi-card:hover {
646
+ transform: translate(-3px, -3px);
647
+ box-shadow: var(--axi-offset-panel-hover) var(--axi-offset-panel-hover) 0 var(--axi-ink-line);
648
+ }
649
+ /* The top strip is drawn from the card itself so a renderer does not have to
650
+ emit an extra element for it. It is opt-in: a card only gets a strip when
651
+ the consumer has a real value to encode in it, because a coloured strip that
652
+ means nothing is decoration impersonating data. */
653
+ .axi-card--strip::before {
654
+ content: ""; position: absolute; top: 0; left: 0; right: 0; height: 9px;
655
+ background: var(--axi-card-strip, var(--axi-accent));
656
+ border-bottom: var(--axi-border-panel) solid var(--axi-ink-line);
657
+ }
658
+ .axi-card__head { display: flex; gap: 12px; align-items: flex-start; }
659
+ .axi-card__glyph {
660
+ flex: none; width: 40px; height: 40px;
661
+ display: grid; place-items: center;
662
+ border-radius: 8px;
663
+ border: var(--axi-border-control) solid var(--axi-ink-line);
664
+ background: var(--axi-surface-raised);
665
+ color: var(--axi-text);
666
+ font: 900 15px/1 var(--axi-sans);
667
+ }
668
+ .axi-card__title { flex: 1; min-width: 0; }
669
+ .axi-card__title .axi-card__name {
670
+ display: block;
671
+ font: var(--axi-t-h3); letter-spacing: var(--axi-ls-h3);
672
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
673
+ }
674
+ .axi-card__title .axi-card__kind {
675
+ display: block; margin-top: 3px;
676
+ font: 700 11px/1.3 var(--axi-sans);
677
+ color: var(--axi-text-faint);
678
+ letter-spacing: .08em; text-transform: uppercase;
679
+ }
680
+ .axi-card p { margin: 0; font: var(--axi-t-small); color: var(--axi-text-dim); }
681
+ /* Pushed to the bottom so that in an equalised grid row every card's meta
682
+ strip lands on the same line and the rules read as continuous across the
683
+ grid. The cost is empty space on cards with less content. */
684
+ .axi-card__meta {
685
+ display: flex; gap: 14px; flex-wrap: wrap; align-items: center;
686
+ margin-top: auto; padding-top: 12px;
687
+ border-top: var(--axi-border-control) solid var(--axi-rule);
688
+ font: 700 12px/1 var(--axi-sans);
689
+ color: var(--axi-text-faint);
690
+ }
691
+ .axi-card__go { margin-left: auto; color: var(--axi-accent); font-weight: 900; text-transform: uppercase; letter-spacing: .05em; }
692
+ .axi-card:hover .axi-card__go { color: var(--axi-text); }
693
+
694
+ /* ---------- drawer ---------- */
695
+ .axi-scrim { position: fixed; inset: 0; z-index: 50; background: var(--axi-scrim); border: 0; }
696
+ .axi-drawer {
697
+ position: fixed; top: 0; right: 0; bottom: 0; z-index: 51;
698
+ width: min(var(--axi-drawer-width, 560px), 100vw);
699
+ display: flex; flex-direction: column;
700
+ background: var(--axi-surface);
701
+ border-left: var(--axi-border-panel) solid var(--axi-ink-line);
702
+ }
703
+ .axi-drawer[hidden], .axi-scrim[hidden] { display: none; }
704
+ .axi-drawer__head {
705
+ position: relative;
706
+ padding: 20px 24px 18px;
707
+ border-bottom: var(--axi-border-panel) solid var(--axi-ink-line);
708
+ }
709
+ .axi-drawer__head h2 {
710
+ margin: 2px 0 0; padding-right: 44px;
711
+ font: var(--axi-t-h2); letter-spacing: var(--axi-ls-h2);
712
+ }
713
+ .axi-drawer__close {
714
+ position: absolute; top: 16px; right: 18px;
715
+ width: 32px; height: 32px;
716
+ display: grid; place-items: center;
717
+ border-radius: 8px;
718
+ background: var(--axi-ground);
719
+ border: var(--axi-border-control) solid var(--axi-ink-line);
720
+ color: var(--axi-text-dim);
721
+ font-size: 15px; font-weight: 900; cursor: pointer;
722
+ transition: transform .1s, box-shadow .1s;
723
+ }
724
+ .axi-drawer__close:hover {
725
+ background: var(--axi-accent); color: var(--axi-accent-ink);
726
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
727
+ transform: translate(-2px, -2px);
728
+ }
729
+ .axi-drawer__body { overflow-y: auto; padding: 18px 24px 30px; }
730
+
731
+ /* ---------- pull-quote ---------- */
732
+ /* A cited quotation. The accent rule down the left is the only place in the
733
+ language where colour marks a block rather than filling a chip - a quote is
734
+ someone else's words and wants to look borrowed. */
735
+ .axi-quote {
736
+ margin: 0;
737
+ padding: 11px 13px;
738
+ border-left: var(--axi-border-panel) solid var(--axi-accent);
739
+ background: var(--axi-ground);
740
+ border-radius: 0 var(--axi-radius-sm) var(--axi-radius-sm) 0;
741
+ }
742
+ .axi-quote p { margin: 0; font-size: 13px; font-style: italic; color: var(--axi-text-dim); line-height: 1.5; }
743
+ .axi-quote cite {
744
+ display: block; margin-top: 7px;
745
+ font-style: normal; font-size: 11.5px; font-weight: 700;
746
+ color: var(--axi-accent);
747
+ }
748
+
749
+ /* ---------- window ---------- */
750
+ /* Chrome for a frameless desktop app, where the app is not a page inside a
751
+ browser but the outermost thing on the screen. Rule 3 says a raised
752
+ element is outlined; a window is the most raised element there is, so it
753
+ takes the panel outline. It carries no block, because a block is an
754
+ element's shadow on the surface behind it and there is nothing behind a
755
+ window that this language is entitled to draw on.
756
+ The titlebar is filled with the ink line rather than a surface: it is the
757
+ outline widened into a strip, which is why the app's content reads as
758
+ sitting inside the outline instead of under a second toolbar. */
759
+ .axi-window {
760
+ height: 100vh;
761
+ display: flex; flex-direction: column; overflow: hidden;
762
+ background: var(--axi-ground);
763
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
764
+ border-radius: var(--axi-radius);
765
+ }
766
+ .axi-titlebar {
767
+ flex: none;
768
+ display: flex; align-items: center; gap: 12px;
769
+ height: 38px; padding-left: 14px;
770
+ background: var(--axi-ink-line);
771
+ border-bottom: var(--axi-border-control) solid var(--axi-ink-line);
772
+ font: var(--axi-t-micro);
773
+ letter-spacing: var(--axi-ls-micro);
774
+ text-transform: uppercase;
775
+ color: var(--axi-text-faint);
776
+ /* The strip is the drag handle; the buttons opt back out in Electron. */
777
+ -webkit-app-region: drag;
778
+ }
779
+ .axi-titlebar__btns { margin-left: auto; display: flex; height: 100%; -webkit-app-region: no-drag; }
780
+ .axi-titlebar__btns button {
781
+ width: 44px;
782
+ border: 0;
783
+ background: transparent;
784
+ color: var(--axi-text-faint);
785
+ font: var(--axi-t-label);
786
+ cursor: pointer;
787
+ }
788
+ /* No lift here. A window button is chrome, not content: it has nothing to
789
+ lift off, and 44px of movement in the corner of every screen is exactly
790
+ the scattered motion rule 4 rations. It answers on the neutral ramp. */
791
+ .axi-titlebar__btns button:hover { background: var(--axi-surface); color: var(--axi-text); }
792
+ /* Close is the one destructive control in the chrome, and the one place a
793
+ status ink belongs in a titlebar. */
794
+ .axi-titlebar__btns button:last-child:hover { background: var(--axi-danger); color: var(--axi-ink-line); }
795
+ /* The scrolling region under the chrome. */
796
+ .axi-window__body { flex: 1; overflow-y: auto; }
797
+
798
+ @media (max-width: 640px) {
799
+ .axi-mast__in { min-height: 0; padding: 10px 16px; gap: 12px; }
800
+ .axi-tabs { margin-left: 0; width: 100%; overflow-x: auto; }
801
+ /* The offset blocks eat horizontal room a phone does not have, so the panel
802
+ step drops to the control step here rather than to an invented number.
803
+ The card's hover has to come down with it - left alone it kept deepening
804
+ to the panel hover block, so a card on a hover-emulating touch device
805
+ jumped further than it does on the desktop it was tuned for. */
806
+ .axi-notice, .axi-toolbar, .axi-card {
807
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
808
+ }
809
+ .axi-card:hover {
810
+ box-shadow: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
811
+ }
812
+ }
813
+
814
+ /* --- data.css --- */
815
+ /* axi design language - data.
816
+ Numbers, and the shapes numbers are drawn as. Everything here is panel
817
+ INTERIOR: a table, a meter and a plot all live inside a .axi-panel, and
818
+ none of them is a raised thing in its own right, so none of them carries a
819
+ block. They are drawn in rules (--axi-rule) and in the ink line, which is
820
+ what keeps a screen of forty metrics from reading as forty floating cards.
821
+
822
+ Three rules govern the file - docs/RULES.md 8, 9 and 10:
823
+ a table is the panel's interior; a quantity is drawn as length, never as
824
+ intensity; and a chart's ink is the accent, with the neutral ramp for
825
+ comparison and a domain palette only where the data owns its own colours. */
826
+
827
+ /* ---------- stat tile ---------- */
828
+ /* One number and its name. Flat on the ground inside its panel, so it takes
829
+ an outline and no block - it is content, not something raised off the
830
+ surface it sits on. The number stays in --axi-text unless it has a real
831
+ state: rule 5 applies to a figure exactly as it applies to a chip, and a
832
+ tile coloured for emphasis is decoration impersonating status. */
833
+ .axi-stat {
834
+ padding: 12px 14px;
835
+ background: var(--axi-ground);
836
+ border: var(--axi-border-control) solid var(--axi-ink-line);
837
+ border-radius: 8px;
838
+ }
839
+ .axi-stat__n {
840
+ display: block;
841
+ font: var(--axi-t-h1);
842
+ letter-spacing: var(--axi-ls-h1);
843
+ color: var(--axi-text);
844
+ font-variant-numeric: tabular-nums;
845
+ }
846
+ .axi-stat__k {
847
+ display: block;
848
+ margin-top: 5px;
849
+ font: var(--axi-t-micro);
850
+ letter-spacing: var(--axi-ls-micro);
851
+ text-transform: uppercase;
852
+ color: var(--axi-text-faint);
853
+ }
854
+ .axi-stat--accent .axi-stat__n { color: var(--axi-accent); }
855
+ .axi-stat--ok .axi-stat__n { color: var(--axi-ok); }
856
+ .axi-stat--warn .axi-stat__n { color: var(--axi-warn); }
857
+ .axi-stat--danger .axi-stat__n { color: var(--axi-danger); }
858
+ /* The one tile that annotates instead of asserting: a count of something the
859
+ app knows *about* the data rather than a measurement of it. */
860
+ .axi-stat--meta .axi-stat__n { color: var(--axi-meta); }
861
+
862
+ /* ---------- table ---------- */
863
+ /* Rule 8. Rows are separated by rules, never outlined and never blocked: the
864
+ panel is the raised thing and the table is what is inside it. The header
865
+ rule is the control weight so the head reads as a lid on the column; the
866
+ row rules are the hairline, which is exactly the case --axi-border-hairline
867
+ exists for - a line inside running content, where either form step would
868
+ turn a list of numbers into a grid of boxes. */
869
+ .axi-table {
870
+ width: 100%;
871
+ border-collapse: collapse;
872
+ font-variant-numeric: tabular-nums;
873
+ }
874
+ /* Numbers right, names left. Set on the element rather than asked of every
875
+ consumer, because a numeric column aligned left is unreadable and it is
876
+ the mistake every hand-built table makes. */
877
+ .axi-table th,
878
+ .axi-table td { text-align: right; white-space: nowrap; }
879
+ .axi-table th:first-child,
880
+ .axi-table td:first-child { text-align: left; }
881
+ .axi-table th {
882
+ padding: 0 10px 10px;
883
+ font: var(--axi-t-micro);
884
+ letter-spacing: var(--axi-ls-micro);
885
+ text-transform: uppercase;
886
+ color: var(--axi-text-faint);
887
+ border-bottom: var(--axi-border-control) solid var(--axi-rule);
888
+ }
889
+ .axi-table td {
890
+ padding: 9px 10px;
891
+ font: var(--axi-t-small);
892
+ font-weight: 700;
893
+ color: var(--axi-text-dim);
894
+ border-bottom: var(--axi-border-hairline) solid var(--axi-rule);
895
+ }
896
+ /* The row under the cursor comes forward on the neutral ramp. Not an ink:
897
+ hovering a row is not a status, and forty rows that each flash a colour on
898
+ the way past the one you want is the tinted-everything failure rule 2 is
899
+ about. */
900
+ .axi-table tbody tr:hover td { background: var(--axi-surface-raised); color: var(--axi-text); }
901
+ /* The measured value in a row, as opposed to its supporting numbers. */
902
+ .axi-table__num { color: var(--axi-text); }
903
+ /* A name cell: an icon, a diamond or a rank beside the label. */
904
+ .axi-table__who { display: flex; align-items: center; gap: 9px; }
905
+ /* Rank is the only fill a table gets, and only where the position is real -
906
+ a podium, not a row number. An outlined rank is the ordinary case. */
907
+ .axi-table__rank {
908
+ width: 22px; height: 22px; flex: none;
909
+ display: grid; place-items: center;
910
+ border: var(--axi-border-control) solid var(--axi-ink-line);
911
+ border-radius: var(--axi-radius-sm);
912
+ background: var(--axi-ground);
913
+ font: var(--axi-t-micro);
914
+ color: var(--axi-text-faint);
915
+ }
916
+ .axi-table__rank--top { background: var(--axi-accent); color: var(--axi-accent-ink); }
917
+
918
+ /* ---------- meter ---------- */
919
+ /* Rule 9: a proportion is a length. The track is the ground, the fill is the
920
+ value, and the fill is one ink at full strength - a tinted or faded bar is
921
+ the same lie as a tinted surface, and it is unreadable at the small sizes a
922
+ table of them is actually used at.
923
+ A meter holds one fill (a value) or several (a composition). There is no
924
+ divider between adjacent fills: two saturated inks already separate
925
+ themselves, and a line between them would be a third form step. */
926
+ .axi-meter {
927
+ display: flex;
928
+ height: var(--axi-meter-h, 12px);
929
+ background: var(--axi-ground);
930
+ border: var(--axi-border-control) solid var(--axi-ink-line);
931
+ border-radius: var(--axi-radius-sm);
932
+ overflow: hidden;
933
+ }
934
+ .axi-meter__fill {
935
+ flex: none;
936
+ height: 100%;
937
+ width: var(--axi-meter-v, 0%);
938
+ background: var(--axi-series, var(--axi-accent));
939
+ }
940
+
941
+ /* A labelled run of meters: the shape almost every "who did most of X" view
942
+ in an axi property turns out to be. Three columns, so the names, the bars
943
+ and the values each line up down the list; both outer columns are knobs
944
+ because a list of account names and a list of boon names disagree about
945
+ how much room a label needs. */
946
+ .axi-meter-list {
947
+ display: grid;
948
+ grid-template-columns: var(--axi-meter-label, 132px) 1fr var(--axi-meter-value, 62px);
949
+ align-items: center;
950
+ gap: 9px 12px;
951
+ }
952
+ .axi-meter-list__name {
953
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
954
+ font: var(--axi-t-small);
955
+ font-weight: 700;
956
+ color: var(--axi-text-dim);
957
+ }
958
+ .axi-meter-list__value {
959
+ text-align: right;
960
+ font: var(--axi-t-micro);
961
+ letter-spacing: var(--axi-ls-micro);
962
+ color: var(--axi-text);
963
+ font-variant-numeric: tabular-nums;
964
+ }
965
+
966
+ /* ---------- bars ---------- */
967
+ /* The same rule stood on end. The baseline is drawn at the control weight
968
+ because it is an axis - the one line in a chart that is structure rather
969
+ than data - and every column is outlined in the ink line so a short column
970
+ is still a shape and not a smear. Columns share their baseline with it, so
971
+ they drop their own bottom border rather than doubling it. */
972
+ .axi-bars {
973
+ display: flex;
974
+ align-items: flex-end;
975
+ gap: var(--axi-bars-gap, 6px);
976
+ height: var(--axi-plot-h, 180px);
977
+ border-bottom: var(--axi-border-control) solid var(--axi-ink-line);
978
+ }
979
+ .axi-bars__col {
980
+ flex: 1 1 0;
981
+ min-width: 4px;
982
+ height: var(--axi-bar-v, 0%);
983
+ display: flex;
984
+ flex-direction: column-reverse;
985
+ overflow: hidden;
986
+ background: var(--axi-series, var(--axi-accent));
987
+ border: var(--axi-border-control) solid var(--axi-ink-line);
988
+ border-bottom: 0;
989
+ border-radius: var(--axi-radius-sm) var(--axi-radius-sm) 0 0;
990
+ }
991
+ /* A stacked column. The parts are laid out from the baseline up, in source
992
+ order, so the markup reads bottom-to-top the way the chart does. */
993
+ .axi-bars__part {
994
+ flex: none;
995
+ width: 100%;
996
+ height: var(--axi-bar-part, 0%);
997
+ background: var(--axi-series, var(--axi-accent));
998
+ }
999
+
1000
+ /* ---------- plot ---------- */
1001
+ /* A frame for a line or an area, with its horizontal rules drawn in. The
1002
+ rules are hard stops in a repeating gradient, which is the select caret's
1003
+ exception to rule 1 - a gradient used to draw a shape, with no soft
1004
+ transition anywhere in it - and the only way to get N evenly spaced rules
1005
+ without asking every consumer to emit N empty divs. */
1006
+ .axi-plot {
1007
+ position: relative;
1008
+ height: var(--axi-plot-h, 180px);
1009
+ background-color: var(--axi-ground);
1010
+ background-image: repeating-linear-gradient(
1011
+ to top,
1012
+ var(--axi-rule) 0 var(--axi-border-hairline),
1013
+ transparent var(--axi-border-hairline) calc(100% / var(--axi-plot-rows, 4))
1014
+ );
1015
+ border: var(--axi-border-control) solid var(--axi-ink-line);
1016
+ border-radius: var(--axi-radius-sm);
1017
+ overflow: hidden;
1018
+ }
1019
+ /* The geometry itself is the consumer's - this language does not compute a
1020
+ path - but its ink and weight are ours, so a line in an axi property is
1021
+ the same line everywhere. */
1022
+ .axi-plot__svg { position: absolute; inset: 0; width: 100%; height: 100%; }
1023
+ .axi-plot__line {
1024
+ fill: none;
1025
+ stroke: var(--axi-series, var(--axi-accent));
1026
+ stroke-width: var(--axi-border-control);
1027
+ stroke-linejoin: round;
1028
+ stroke-linecap: round;
1029
+ vector-effect: non-scaling-stroke;
1030
+ }
1031
+ /* An area is the region under a line, filled at full strength like every
1032
+ other fill in this language. It is opaque, so two overlapping areas are
1033
+ not a chart this system draws - that is what the second line, or a second
1034
+ plot, is for. */
1035
+ .axi-plot__area { fill: var(--axi-series, var(--axi-accent)); stroke: none; }
1036
+
1037
+ /* ---------- axis and legend ---------- */
1038
+ /* The x labels under a plot. There is no y-axis component: the scale of a
1039
+ chart belongs in the label above it, in words, where it is readable at a
1040
+ glance and survives being screenshotted into Discord. */
1041
+ .axi-axis {
1042
+ display: flex;
1043
+ justify-content: space-between;
1044
+ margin-top: 8px;
1045
+ font: var(--axi-t-micro);
1046
+ letter-spacing: var(--axi-ls-micro);
1047
+ text-transform: uppercase;
1048
+ color: var(--axi-text-faint);
1049
+ }
1050
+ .axi-legend { display: flex; flex-wrap: wrap; gap: 7px 16px; }
1051
+ .axi-legend__key {
1052
+ display: inline-flex; align-items: center; gap: 8px;
1053
+ font: var(--axi-t-micro);
1054
+ letter-spacing: var(--axi-ls-micro);
1055
+ text-transform: uppercase;
1056
+ color: var(--axi-text-dim);
1057
+ }
1058
+
1059
+ /* --- prose.css --- */
1060
+ /* axi design language - prose.
1061
+ A single class wrapping rendered markdown, so a content pipeline can emit
1062
+ plain HTML with no classes at all and still land inside the language.
1063
+
1064
+ This layer has no ancestor in the source site, which was all cards and
1065
+ controls. The restraint it needs is different: long-form reading wants
1066
+ generous leading and quiet rules, and the offset blocks that make a card
1067
+ feel physical would make a paragraph feel shouted. So nothing in here is
1068
+ blocked except the code block and the table, which are genuinely objects
1069
+ dropped into the text rather than part of its flow. */
1070
+ .axi-prose { color: var(--axi-text-dim); font: var(--axi-t-body); }
1071
+
1072
+ .axi-prose > :first-child { margin-top: 0; }
1073
+ .axi-prose > :last-child { margin-bottom: 0; }
1074
+
1075
+ .axi-prose h1 { margin: 0 0 18px; font: var(--axi-t-h1); letter-spacing: var(--axi-ls-h1); color: var(--axi-text); }
1076
+ .axi-prose h2 {
1077
+ margin: 38px 0 14px; padding-top: 22px;
1078
+ border-top: var(--axi-border-control) solid var(--axi-rule);
1079
+ font: var(--axi-t-h2); letter-spacing: var(--axi-ls-h2); color: var(--axi-text);
1080
+ }
1081
+ .axi-prose h3 { margin: 28px 0 10px; font: var(--axi-t-h3); letter-spacing: var(--axi-ls-h3); color: var(--axi-text); }
1082
+ .axi-prose h4 {
1083
+ margin: 22px 0 8px;
1084
+ font: var(--axi-t-eyebrow); letter-spacing: var(--axi-ls-eyebrow);
1085
+ text-transform: uppercase; color: var(--axi-text-faint);
1086
+ }
1087
+
1088
+ .axi-prose p { margin: 0 0 16px; }
1089
+ .axi-prose strong { color: var(--axi-text); font-weight: 800; }
1090
+ /* Prose is the one place a link should announce itself: a reader scanning an
1091
+ article is looking for them, and inheriting the body ink would hide them. */
1092
+ .axi-prose a { color: var(--axi-accent); font-weight: 600; text-underline-offset: 2px; }
1093
+ .axi-prose a:hover { color: var(--axi-text); }
1094
+
1095
+ .axi-prose ul, .axi-prose ol { margin: 0 0 16px; padding-left: 22px; }
1096
+ .axi-prose li { margin: 0 0 7px; }
1097
+ /* The family motif as a bullet. Drawn with ::marker's replacement rather than
1098
+ a background image so it inherits the list's own indentation. */
1099
+ .axi-prose ul { list-style: none; padding-left: 20px; }
1100
+ .axi-prose ul > li { position: relative; }
1101
+ .axi-prose ul > li::before {
1102
+ content: ""; position: absolute; left: -20px; top: .55em;
1103
+ width: 7px; height: 7px; transform: rotate(45deg);
1104
+ background: var(--axi-accent);
1105
+ border: var(--axi-border-hairline) solid var(--axi-ink-line);
1106
+ }
1107
+
1108
+ .axi-prose code {
1109
+ font-family: var(--axi-mono); font-size: .88em;
1110
+ background: var(--axi-ground); color: var(--axi-text);
1111
+ border: var(--axi-border-hairline) solid var(--axi-ink-line);
1112
+ border-radius: 4px; padding: 1px 5px;
1113
+ }
1114
+ .axi-prose pre {
1115
+ margin: 0 0 18px; padding: 14px 16px; overflow-x: auto;
1116
+ background: var(--axi-ground);
1117
+ border: var(--axi-border-control) solid var(--axi-ink-line);
1118
+ border-radius: var(--axi-radius-sm);
1119
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
1120
+ }
1121
+ .axi-prose pre code { background: none; border: 0; padding: 0; font-size: 12.5px; line-height: 1.6; }
1122
+
1123
+ .axi-prose blockquote {
1124
+ margin: 0 0 18px;
1125
+ padding: 11px 13px;
1126
+ border-left: var(--axi-border-panel) solid var(--axi-accent);
1127
+ background: var(--axi-ground);
1128
+ border-radius: 0 var(--axi-radius-sm) var(--axi-radius-sm) 0;
1129
+ }
1130
+ .axi-prose blockquote p { margin: 0; font-style: italic; }
1131
+
1132
+ .axi-prose table {
1133
+ width: 100%; margin: 0 0 18px;
1134
+ border-collapse: collapse;
1135
+ border: var(--axi-border-control) solid var(--axi-ink-line);
1136
+ border-radius: var(--axi-radius-sm);
1137
+ overflow: hidden;
1138
+ font-size: 13.5px;
1139
+ }
1140
+ .axi-prose th {
1141
+ text-align: left; padding: 10px 12px;
1142
+ background: var(--axi-surface-raised); color: var(--axi-text);
1143
+ font: var(--axi-t-micro); letter-spacing: var(--axi-ls-micro); text-transform: uppercase;
1144
+ }
1145
+ .axi-prose td { padding: 10px 12px; border-top: var(--axi-border-hairline) solid var(--axi-rule); }
1146
+
1147
+ .axi-prose hr {
1148
+ margin: 30px 0; height: 0;
1149
+ border: 0; border-top: var(--axi-border-control) solid var(--axi-rule);
1150
+ }
1151
+
1152
+ .axi-prose img { max-width: 100%; height: auto; border-radius: var(--axi-radius-sm); }