Blog/wwwroot/app.css 12.1 K · 586 lines · raw · history

1 /* ==========================================================================
2 bes.is — a small terminal-flavoured stylesheet.
3
4 1. Tokens design decisions, in one place
5 2. Reset & base the same starting point in every browser
6 3. Typography headings, text, lists, links
7 4. Layout page frame and layout utilities
8 5. Controls buttons, fields, everything the browser styles for us
9 6. Components header, footer, log, panels, json, dialogs
10 7. Media logo, images, video
11 ========================================================================== */
12
13
14 /* 1. Tokens ================================================================ */
15
16 :root {
17 /* Tells the browser to theme native UI (scrollbars, select popups, focus
18 rings, spinners) to match the page. Without it those stay light in a
19 dark page, which is the main reason controls look different per
20 browser. It also enables light-dark() below. */
21 color-scheme: light dark;
22
23 --black: #050505;
24 --white: #fafafa;
25
26 --background: light-dark(var(--white), var(--black));
27 --color: light-dark(var(--black), var(--white));
28 --border: var(--color);
29
30 --error: light-dark(#b00000, #ff6b6b);
31 --info: light-dark(#3d6f8e, #86b6d3);
32 --green: light-dark(#2f7a2d, #6fce6d);
33
34 /* Modular scale. Every gap, margin and padding comes from here so
35 spacing stays proportional to the fluid type size. */
36 --ratio: 1.5;
37 --s-2: calc(var(--s-1) / var(--ratio));
38 --s-1: calc(var(--s0) / var(--ratio));
39 --s0: 1rem;
40 --s1: calc(var(--s0) * var(--ratio));
41 --s2: calc(var(--s1) * var(--ratio));
42
43 /* A concrete stack instead of bare `monospace`: browsers disagree both on
44 which font that is and on its default size. */
45 --font: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
46
47 --measure: 80ch;
48 --radius: 0.5em;
49 --border-width: 1px;
50 --rule: 2px dotted var(--border);
51 }
52
53
54 /* 2. Reset & base ========================================================== */
55
56 *,
57 *::before,
58 *::after {
59 box-sizing: border-box;
60 }
61
62 html {
63 font-family: var(--font);
64 font-size: calc(1rem + 0.4vw);
65 line-height: 1.5;
66 -webkit-text-size-adjust: 100%;
67 tab-size: 4;
68 }
69
70 body {
71 margin: 0;
72 min-block-size: 100dvh;
73 display: flex;
74 flex-direction: column;
75 gap: var(--s0);
76 padding-block: var(--s0);
77 background-color: var(--background);
78 color: var(--color);
79 }
80
81 /* One focus style for every focusable thing, instead of six browser defaults.
82 `:focus:not(:focus-visible)` keeps programmatic focus (FocusOnNavigate
83 lands on the <h1> after every navigation) from flashing a ring. */
84 :focus-visible {
85 outline: 2px solid var(--color);
86 outline-offset: 2px;
87 }
88
89 :focus:not(:focus-visible) {
90 outline: none;
91 }
92
93 ::selection {
94 background-color: var(--color);
95 color: var(--background);
96 }
97
98
99 /* 3. Typography ============================================================ */
100
101 h1,
102 h2,
103 h3,
104 h4,
105 h5,
106 h6 {
107 margin-block: 0 var(--s-1);
108 font-weight: 700;
109 line-height: 1.2;
110 text-wrap: balance;
111 }
112
113 h1 {
114 font-size: calc(1em * var(--ratio));
115 }
116
117 h2 {
118 font-size: calc(1em * 1.25);
119 }
120
121 h3,
122 h4,
123 h5,
124 h6 {
125 font-size: 1em;
126 }
127
128 p {
129 margin-block: var(--s-1);
130 text-wrap: pretty;
131 }
132
133 ul,
134 ol {
135 margin-block: var(--s-1);
136 padding-inline-start: var(--s1);
137 }
138
139 pre {
140 margin-block: var(--s-1);
141 /* pre-line, so examples can stay indented in the .razor source */
142 white-space: pre-line;
143 overflow-wrap: anywhere;
144 }
145
146 small {
147 font-size: 0.75em;
148 }
149
150 a {
151 color: inherit;
152 text-decoration-line: underline;
153 text-decoration-thickness: var(--border-width);
154 text-underline-offset: 0.2em;
155 }
156
157 a:hover,
158 a:focus-visible {
159 background-color: var(--color);
160 color: var(--background);
161 text-decoration: none;
162 }
163
164
165 /* 4. Layout ================================================================ */
166
167 .center {
168 box-sizing: content-box;
169 max-inline-size: var(--measure);
170 margin-inline: auto;
171 padding-inline: var(--s1);
172 }
173
174 main {
175 flex: 1;
176 border-block-start: var(--rule);
177 border-block-end: var(--rule);
178 padding-block: var(--s0);
179 }
180
181 .flex-row {
182 display: flex;
183 flex-wrap: wrap;
184 align-items: center;
185 gap: var(--s0);
186 }
187
188 /* A field shares the row instead of claiming a whole one to itself. */
189 .flex-row > :is(input[type="text"], textarea) {
190 flex: 1;
191 }
192
193 .flex-spread {
194 display: flex;
195 flex-wrap: wrap;
196 gap: var(--s0);
197 justify-content: space-between;
198 }
199
200 .row + .row {
201 margin-block-start: var(--s0);
202 }
203
204 /* Two columns, but only while they stay wide enough to read. */
205 .two-column {
206 columns: 2 24ch;
207 gap: var(--s1);
208 }
209
210 .noselect {
211 user-select: none;
212 -webkit-user-select: none;
213 }
214
215
216 /* 5. Controls ============================================================== */
217
218 /* Controls do not inherit the page font on their own — this is what keeps
219 fields and buttons from turning into system UI in each browser. */
220 button,
221 input,
222 select,
223 textarea {
224 font: inherit;
225 color: inherit;
226 background-color: var(--background);
227 border: var(--border-width) solid var(--border);
228 border-radius: var(--radius);
229 padding: 0.1em 0.5em;
230 max-inline-size: 100%;
231 }
232
233 label {
234 display: block;
235 }
236
237 input[type="text"] {
238 /* min-inline-size lets it shrink inside a .flex-row instead of overflowing */
239 inline-size: 100%;
240 min-inline-size: 0;
241 }
242
243 textarea {
244 display: block;
245 inline-size: 100%;
246 min-block-size: 4lh;
247 resize: vertical;
248 white-space: pre-wrap;
249 }
250
251 /* A block textarea leaves its button hard against it otherwise. */
252 textarea + button,
253 textarea + input[type="submit"] {
254 margin-block-start: var(--s-1);
255 }
256
257 /* Editors sized against the viewport rather than a row count. */
258 .editor {
259 block-size: 20vh;
260 }
261
262 .editor-tall {
263 block-size: 60vh;
264 }
265
266 select {
267 cursor: pointer;
268 }
269
270 button,
271 input[type="button"],
272 input[type="submit"],
273 input[type="reset"] {
274 min-inline-size: 8ch;
275 padding-inline: var(--s0);
276 text-align: center;
277 cursor: pointer;
278 }
279
280 button:hover,
281 input[type="button"]:hover,
282 input[type="submit"]:hover,
283 input[type="reset"]:hover {
284 background-color: var(--color);
285 color: var(--background);
286 }
287
288 button:active,
289 input[type="button"]:active,
290 input[type="submit"]:active,
291 input[type="reset"]:active {
292 translate: 0 1px;
293 }
294
295 /* The file input itself is a bare label for its button. */
296 input[type="file"] {
297 border: none;
298 background: none;
299 padding: 0;
300 cursor: pointer;
301 }
302
303 /* Kept in its own rule: an unsupported pseudo-element would invalidate the
304 whole selector list if it shared one with the buttons above. */
305 ::file-selector-button {
306 font: inherit;
307 color: inherit;
308 background-color: var(--background);
309 border: var(--border-width) solid var(--border);
310 border-radius: var(--radius);
311 min-inline-size: 8ch;
312 padding: 0.1em var(--s0);
313 margin-inline-end: var(--s-1);
314 cursor: pointer;
315 }
316
317 ::file-selector-button:hover {
318 background-color: var(--color);
319 color: var(--background);
320 }
321
322 input[type="checkbox"] {
323 appearance: none;
324 inline-size: 3ch;
325 padding: 0;
326 border: none;
327 background: none;
328 text-align: center;
329 cursor: pointer;
330 }
331
332 input[type="checkbox"]::after {
333 content: "[ ]";
334 }
335
336 input[type="checkbox"]:checked::after {
337 content: "[x]";
338 }
339
340 fieldset {
341 /* min-inline-size overrides the min-content floor browsers give fieldsets,
342 which otherwise makes them overflow flex and grid containers. */
343 min-inline-size: 0;
344 margin: 0;
345 border: var(--border-width) solid var(--border);
346 border-radius: var(--radius);
347 padding: var(--s-1) var(--s0);
348 }
349
350 legend {
351 padding-inline: var(--s-2);
352 }
353
354 legend::before {
355 content: "( ";
356 }
357
358 legend::after {
359 content: " )";
360 }
361
362 summary {
363 cursor: help;
364 }
365
366 summary::marker {
367 content: "> ";
368 }
369
370 details[open] > summary::marker {
371 content: "x ";
372 }
373
374 /* A heading is allowed to be a summary without breaking the line. */
375 summary > :is(h1, h2, h3, h4, h5, h6) {
376 display: inline;
377 }
378
379
380 /* 6. Components ============================================================ */
381
382 /* -- Site header --------------------------------------------------------- */
383
384 header {
385 display: flex;
386 flex-wrap: wrap;
387 align-items: center;
388 gap: var(--s0);
389 }
390
391 header > h1 {
392 margin-block: 0;
393 }
394
395 /* -- Site footer --------------------------------------------------------- */
396
397 /* Markers stay outside the text column, in a gutter wide enough for the
398 longest of them ("==> "). That way the arrow slides right on hover and
399 focus while the link itself never moves. The column gap is measured in the
400 same unit, so the second column's markers cannot reach the first. */
401 footer ul {
402 columns: 2 20ch;
403 padding-inline-start: 4ch;
404 column-gap: 5ch;
405 }
406
407 footer li::marker {
408 content: "> ";
409 }
410
411 footer li:has(a:hover)::marker {
412 content: " >";
413 }
414
415 footer li:has(a:focus)::marker {
416 content: "==> ";
417 }
418
419 footer li:has(a:hover:focus)::marker {
420 content: "==>";
421 }
422
423 /* NavLink marks the current page for us. */
424 footer li:has(a.active)::marker {
425 content: "* ";
426 }
427
428 /* -- Panels (bordered boxes with a title) -------------------------------- */
429
430 .panel {
431 margin-block: var(--s1);
432 border: 2px dashed var(--border);
433 padding: var(--s-1) var(--s0);
434 }
435
436 /* -- Log ------------------------------------------------------------------ */
437
438 #log {
439 margin-block-start: var(--s0);
440 }
441
442 .error::before,
443 .info::before,
444 .debug::before {
445 /* not selectable, so copying a log line copies the message only */
446 user-select: none;
447 -webkit-user-select: none;
448 }
449
450 .error::before {
451 content: "ERR: ";
452 color: var(--error);
453 }
454
455 .info::before {
456 content: "INF: ";
457 color: var(--info);
458 }
459
460 .debug::before {
461 content: "DBG: ";
462 color: var(--info);
463 }
464
465 /* Debug lines stay hidden until the checkbox next to the log is ticked. */
466 label:has(> #debug:not(:checked)) + #log .debug {
467 display: none;
468 }
469
470 /* Earlier runs, folded away and dimmed. */
471 .history {
472 opacity: 30%;
473 }
474
475 * + .history {
476 margin-block-start: var(--s1);
477 }
478
479 /* -- Rendered JSON -------------------------------------------------------- */
480
481 li.dash {
482 list-style-type: "- ";
483 }
484
485 span.name {
486 opacity: 50%;
487 }
488
489 .json-true {
490 color: var(--green);
491 }
492
493 .json-false {
494 color: var(--error);
495 }
496
497 .json-number {
498 color: var(--info);
499 }
500
501 /* -- Dialogs -------------------------------------------------------------- */
502
503 dialog {
504 background-color: transparent;
505 color: var(--color);
506 border: none;
507 padding: 0;
508 max-inline-size: min(100% - var(--s2), 60ch);
509 }
510
511 dialog::backdrop {
512 backdrop-filter: blur(calc(var(--s1) / 20));
513 }
514
515 /* The panel inside supplies the surface, so the dialog itself can stay bare. */
516 dialog > .panel {
517 margin-block: 0;
518 background-color: var(--background);
519 }
520
521 dialog > .panel > legend {
522 background-color: var(--background);
523 border-radius: var(--radius);
524 }
525
526 /* -- Storage ------------------------------------------------------------- */
527
528 .grid-stores {
529 display: grid;
530 grid-template-columns: 1fr auto auto;
531 align-items: center;
532 gap: var(--s-1) var(--s0);
533 }
534
535 /* -- Scroll shortcuts ------------------------------------------------------ */
536
537 /* Docked to the viewport edge rather than the page flow, so it stays
538 reachable no matter how far down a long page (e.g. Query's history and
539 favorites) you've scrolled. */
540 .scroll-nav {
541 position: fixed;
542 inset-inline-end: var(--s0);
543 inset-block-start: 50%;
544 transform: translateY(-50%);
545 display: flex;
546 flex-direction: column;
547 gap: var(--s-1);
548 z-index: 100;
549 }
550
551 .scroll-nav button {
552 line-height: 1;
553 padding: var(--s-1);
554 }
555
556
557 /* 7. Media ================================================================= */
558
559 #logo {
560 block-size: 5em;
561 flex: none;
562 }
563
564 img {
565 display: block;
566 max-inline-size: 100%;
567 block-size: auto;
568 margin-inline: auto;
569 margin-block: var(--s0);
570 }
571
572 video {
573 display: block;
574 inline-size: 100%;
575 block-size: auto;
576 border-radius: var(--radius);
577 }
578
579 #qrcode {
580 margin-block: var(--s0);
581 text-align: center;
582 }
583
584 #qrcode :is(canvas, table) {
585 margin-inline: auto;
586 }