@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/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/axi.css +1152 -0
- package/docs/RULES.md +275 -0
- package/package.json +35 -0
- package/src/base.css +66 -0
- package/src/data.css +243 -0
- package/src/layout.css +47 -0
- package/src/primitives.css +301 -0
- package/src/prose.css +93 -0
- package/src/shells.css +303 -0
- package/src/tokens.css +83 -0
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); }
|