Blog/wwwroot/app.css 16.6 K · 767 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 /* A stack on the left, the program left to run on the right (the log rows and
201 the worked examples in the docs). A grid rather than a spread flex row, so a
202 long program wraps inside its own column instead of taking a full-width line
203 of its own and leaving its stack stranded on the line above. */
204 .stack-step {
205 display: grid;
206 grid-template-columns: minmax(0, max-content) 1fr;
207 column-gap: var(--s0);
208 }
209
210 /* break-word, not anywhere: it keeps the column's minimum at one whole word, so
211 a long stack can never squeeze the program down to a letter per line. */
212 .stack-step > :last-child {
213 text-align: right;
214 overflow-wrap: break-word;
215 }
216
217 .row + .row {
218 margin-block-start: var(--s0);
219 }
220
221 /* Two columns, but only while they stay wide enough to read. */
222 .two-column {
223 columns: 2 24ch;
224 gap: var(--s1);
225 }
226
227 .noselect {
228 user-select: none;
229 -webkit-user-select: none;
230 }
231
232
233 /* 5. Controls ============================================================== */
234
235 /* Controls do not inherit the page font on their own — this is what keeps
236 fields and buttons from turning into system UI in each browser. */
237 button,
238 input,
239 select,
240 textarea {
241 font: inherit;
242 color: inherit;
243 background-color: var(--background);
244 border: var(--border-width) solid var(--border);
245 border-radius: var(--radius);
246 padding: 0.1em 0.5em;
247 max-inline-size: 100%;
248 }
249
250 label {
251 display: block;
252 }
253
254 input[type="text"] {
255 /* min-inline-size lets it shrink inside a .flex-row instead of overflowing */
256 inline-size: 100%;
257 min-inline-size: 0;
258 }
259
260 textarea {
261 display: block;
262 inline-size: 100%;
263 min-block-size: 4lh;
264 resize: vertical;
265 white-space: pre-wrap;
266 }
267
268 /* A block textarea leaves its button hard against it otherwise. */
269 textarea + button,
270 textarea + input[type="submit"] {
271 margin-block-start: var(--s-1);
272 }
273
274 /* Editors sized against the viewport rather than a row count. */
275 .editor {
276 block-size: 20vh;
277 }
278
279 .editor-tall {
280 block-size: 60vh;
281 }
282
283 select {
284 cursor: pointer;
285 }
286
287 button,
288 input[type="button"],
289 input[type="submit"],
290 input[type="reset"] {
291 min-inline-size: 8ch;
292 padding-inline: var(--s0);
293 text-align: center;
294 cursor: pointer;
295 }
296
297 button:hover,
298 input[type="button"]:hover,
299 input[type="submit"]:hover,
300 input[type="reset"]:hover {
301 background-color: var(--color);
302 color: var(--background);
303 }
304
305 button:active,
306 input[type="button"]:active,
307 input[type="submit"]:active,
308 input[type="reset"]:active {
309 translate: 0 1px;
310 }
311
312 /* The file input itself is a bare label for its button. */
313 input[type="file"] {
314 border: none;
315 background: none;
316 padding: 0;
317 cursor: pointer;
318 }
319
320 /* Kept in its own rule: an unsupported pseudo-element would invalidate the
321 whole selector list if it shared one with the buttons above. */
322 ::file-selector-button {
323 font: inherit;
324 color: inherit;
325 background-color: var(--background);
326 border: var(--border-width) solid var(--border);
327 border-radius: var(--radius);
328 min-inline-size: 8ch;
329 padding: 0.1em var(--s0);
330 margin-inline-end: var(--s-1);
331 cursor: pointer;
332 }
333
334 ::file-selector-button:hover {
335 background-color: var(--color);
336 color: var(--background);
337 }
338
339 input[type="checkbox"] {
340 appearance: none;
341 inline-size: 3ch;
342 padding: 0;
343 border: none;
344 background: none;
345 text-align: center;
346 cursor: pointer;
347 }
348
349 input[type="checkbox"]::after {
350 content: "[ ]";
351 }
352
353 input[type="checkbox"]:checked::after {
354 content: "[x]";
355 }
356
357 fieldset {
358 /* min-inline-size overrides the min-content floor browsers give fieldsets,
359 which otherwise makes them overflow flex and grid containers. */
360 min-inline-size: 0;
361 margin: 0;
362 border: var(--border-width) solid var(--border);
363 border-radius: var(--radius);
364 padding: var(--s-1) var(--s0);
365 }
366
367 legend {
368 padding-inline: var(--s-2);
369 }
370
371 legend::before {
372 content: "( ";
373 }
374
375 legend::after {
376 content: " )";
377 }
378
379 summary {
380 cursor: help;
381 }
382
383 summary::marker {
384 content: "> ";
385 }
386
387 details[open] > summary::marker {
388 content: "x ";
389 }
390
391 /* A heading is allowed to be a summary without breaking the line. */
392 summary > :is(h1, h2, h3, h4, h5, h6) {
393 display: inline;
394 }
395
396
397 /* 6. Components ============================================================ */
398
399 /* -- Site header --------------------------------------------------------- */
400
401 header {
402 display: flex;
403 flex-wrap: wrap;
404 align-items: center;
405 gap: var(--s0);
406 }
407
408 header > h1 {
409 margin-block: 0;
410 }
411
412 /* -- Site footer --------------------------------------------------------- */
413
414 /* Markers stay outside the text column, in a gutter wide enough for the
415 longest of them ("==> "). That way the arrow slides right on hover and
416 focus while the link itself never moves. The column gap is measured in the
417 same unit, so the second column's markers cannot reach the first. */
418 footer ul {
419 columns: 2 20ch;
420 padding-inline-start: 4ch;
421 column-gap: 5ch;
422 }
423
424 footer li::marker {
425 content: "> ";
426 }
427
428 footer li:has(a:hover)::marker {
429 content: " >";
430 }
431
432 footer li:has(a:focus)::marker {
433 content: "==> ";
434 }
435
436 footer li:has(a:hover:focus)::marker {
437 content: "==>";
438 }
439
440 /* NavLink marks the current page for us. */
441 footer li:has(a.active)::marker {
442 content: "* ";
443 }
444
445 /* -- Panels (bordered boxes with a title) -------------------------------- */
446
447 .panel {
448 margin-block: var(--s1);
449 border: 2px dashed var(--border);
450 padding: var(--s-1) var(--s0);
451 }
452
453 /* -- Log ------------------------------------------------------------------ */
454
455 #log {
456 margin-block-start: var(--s0);
457 }
458
459 .error::before,
460 .info::before,
461 .debug::before {
462 /* not selectable, so copying a log line copies the message only */
463 user-select: none;
464 -webkit-user-select: none;
465 }
466
467 .error::before {
468 content: "ERR: ";
469 color: var(--error);
470 }
471
472 .info::before {
473 content: "INF: ";
474 color: var(--info);
475 }
476
477 .debug::before {
478 content: "DBG: ";
479 color: var(--info);
480 }
481
482 /* Debug lines stay hidden until the checkbox next to the log is ticked. */
483 label:has(> #debug:not(:checked)) + #log .debug {
484 display: none;
485 }
486
487 /* Earlier runs, folded away and dimmed. */
488 .history {
489 opacity: 30%;
490 }
491
492 * + .history {
493 margin-block-start: var(--s1);
494 }
495
496 /* -- Rendered JSON -------------------------------------------------------- */
497
498 li.dash {
499 list-style-type: "- ";
500 }
501
502 span.name {
503 opacity: 50%;
504 }
505
506 .json-true {
507 color: var(--green);
508 }
509
510 .json-false {
511 color: var(--error);
512 }
513
514 .json-number {
515 color: var(--info);
516 }
517
518 .json-null {
519 opacity: 50%;
520 }
521
522 /* -- Dialogs -------------------------------------------------------------- */
523
524 dialog {
525 background-color: transparent;
526 color: var(--color);
527 border: none;
528 padding: 0;
529 max-inline-size: min(100% - var(--s2), 60ch);
530 }
531
532 dialog::backdrop {
533 backdrop-filter: blur(calc(var(--s1) / 20));
534 }
535
536 /* The panel inside supplies the surface, so the dialog itself can stay bare. */
537 dialog > .panel {
538 margin-block: 0;
539 background-color: var(--background);
540 }
541
542 dialog > .panel > legend {
543 background-color: var(--background);
544 border-radius: var(--radius);
545 }
546
547 /* -- Tables --------------------------------------------------------------- */
548
549 table {
550 inline-size: 100%;
551 border-collapse: collapse;
552 }
553
554 :is(th, td) {
555 text-align: start;
556 padding: var(--s-2) var(--s-1);
557 padding-inline-start: 0;
558 border-block-end: var(--border-width) solid var(--border);
559 vertical-align: baseline;
560 }
561
562 tbody tr:last-child :is(th, td) {
563 border-block-end: none;
564 }
565
566 /* A table with more columns than the page has room for scrolls inside its own
567 box rather than pushing the whole page sideways. */
568 .table-scroll {
569 overflow-x: auto;
570 }
571
572
573 /* -- rvrb stats ----------------------------------------------------------- */
574
575 .status-up {
576 color: var(--green);
577 }
578
579 .status-idle {
580 color: var(--info);
581 }
582
583 .status-down {
584 color: var(--error);
585 }
586
587 .muted {
588 opacity: 50%;
589 }
590
591 .now-playing {
592 display: flex;
593 flex-wrap: wrap;
594 gap: var(--s0);
595 align-items: start;
596 }
597
598 /* Overrides the block margin and auto-centring the base `img` rule gives every
599 image, which would otherwise push the art away from the track it belongs to. */
600 .now-playing .album-art {
601 margin: 0;
602 flex: none;
603 inline-size: 8em;
604 block-size: 8em;
605 border-radius: var(--radius);
606 }
607
608 .now-playing > div {
609 flex: 1 1 20ch;
610 }
611
612 progress {
613 inline-size: 100%;
614 block-size: var(--s-1);
615 }
616
617 /* Counts big enough to read at a glance, laid out as many per row as fit. */
618 .stats {
619 display: grid;
620 grid-template-columns: repeat(auto-fit, minmax(10ch, 1fr));
621 gap: var(--s-1);
622 margin-block: 0;
623 }
624
625 .stats dt {
626 opacity: 60%;
627 }
628
629 .stats dd {
630 margin-inline-start: 0;
631 font-size: calc(1em * var(--ratio));
632 }
633
634 /* Plays per day. One <li> per day, so a quiet day is an empty column rather
635 than a gap - see Rvrb.Stats.plays_per_day/1.
636
637 A grid rather than a flex row, because a bar's height is a percentage and
638 can only resolve against a track with a height of its own. Sharing one 6em
639 box with the labels made the two compete for it: flex shrank whichever bar
640 was tall enough to overflow *and* the labels under it, so the tallest bars
641 ended up hanging below the baseline the rest stood on. */
642 .bars {
643 /* A container, so the labels below can react to how much room each column
644 actually got rather than to the width of the viewport. */
645 container-type: inline-size;
646 display: grid;
647 grid-auto-flow: column;
648 grid-auto-columns: 1fr;
649 /* the bar track, then a row each for the count and the weekday */
650 grid-template-rows: 6em auto auto;
651 /* Column gap only: the labels belong against the bar they describe. */
652 gap: 0 var(--border-width);
653 margin-block: 0;
654 padding-inline: 0;
655 list-style: none;
656 }
657
658 /* subgrid, so every column's count and weekday line up on the same two rows
659 however tall the bar above them is. */
660 .bars li {
661 display: grid;
662 grid-row: span 3;
663 grid-template-rows: subgrid;
664 align-items: end;
665 justify-items: center;
666 }
667
668 .bars .bar {
669 inline-size: 100%;
670 background-color: var(--color);
671 }
672
673 .bars :is(.bar-label, .bar-day) {
674 font-size: 0.7em;
675 opacity: 60%;
676 }
677
678 /* Two weeks of columns in one text column: the day has to give way before the
679 count does, since the count is the number and the day is the axis. */
680 .bars .bar-day {
681 overflow: hidden;
682 max-inline-size: 100%;
683 /* Clipped rather than wrapped: three letters over two lines would push the
684 bars up and misalign every column. */
685 white-space: nowrap;
686 }
687
688 .bars .day-initial {
689 display: none;
690 }
691
692 /* "Thu" wants about 3.2ch, so a fortnight of them needs roughly this much
693 chart before the columns stop being wide enough to hold three letters. On a
694 phone they don't, and the day falls back to its initial rather than being
695 clipped mid-letter. */
696 @container (width < 21rem) {
697 .bars .day-abbrev {
698 display: none;
699 }
700
701 .bars .day-initial {
702 display: inline;
703 }
704 }
705
706
707 /* -- Storage ------------------------------------------------------------- */
708
709 .grid-stores {
710 display: grid;
711 grid-template-columns: 1fr auto auto;
712 align-items: center;
713 gap: var(--s-1) var(--s0);
714 }
715
716 /* -- Scroll shortcuts ------------------------------------------------------ */
717
718 /* Docked to the viewport edge rather than the page flow, so it stays
719 reachable no matter how far down a long page (e.g. Query's history and
720 favorites) you've scrolled. */
721 .scroll-nav {
722 position: fixed;
723 inset-inline-end: var(--s0);
724 inset-block-start: 50%;
725 transform: translateY(-50%);
726 display: flex;
727 flex-direction: column;
728 gap: var(--s-1);
729 z-index: 100;
730 }
731
732 .scroll-nav button {
733 line-height: 1;
734 padding: var(--s-1);
735 }
736
737
738 /* 7. Media ================================================================= */
739
740 #logo {
741 block-size: 5em;
742 flex: none;
743 }
744
745 img {
746 display: block;
747 max-inline-size: 100%;
748 block-size: auto;
749 margin-inline: auto;
750 margin-block: var(--s0);
751 }
752
753 video {
754 display: block;
755 inline-size: 100%;
756 block-size: auto;
757 border-radius: var(--radius);
758 }
759
760 #qrcode {
761 margin-block: var(--s0);
762 text-align: center;
763 }
764
765 #qrcode :is(canvas, table) {
766 margin-inline: auto;
767 }