Rework the stylesheet into one consistent system

The stylesheet had grown a few rules that pointed at nothing, which is what made elements render differently per browser: - --border and --header were used but never defined, so input and textarea borders fell back to each browser's default - no color-scheme, so native widgets (scrollbars, select popups, the file picker) stayed light inside the dark theme - controls never got `font: inherit`, so select, file inputs and textareas rendered in the browser's own UI font - input[type="text"] was declared twice, the first block dead - `* { font-size }` flattened every heading - img had margin-inline: auto but no display: block, so it never centred - header and footer sat outside <body>, which each browser repaired its own way Reorganise app.css into tokens, reset, typography, layout, controls, components and media. Colours go through light-dark(), spacing goes through the existing modular scale, and there is now one focus style for everything. Drop the rules that matched no markup: the Bootstrap and Blazor template leftovers (.valid.modified, .invalid, .validation-message, .form-floating, .darker-border-checkbox, .blazor-error-boundary) plus .icon, .textarea, .inline-block, a:empty::before and label + input. Restore the #blazor-error-ui element that MainLayout.razor.css was already styling. Move the header and footer into MainLayout as components so they render inside <body>, and extract Panel, StackOp, StackStep and StackOps. The dup/drop/swap docs were duplicated verbatim across Calc and Concat; Concat goes from 135 to 75 lines with identical rendered output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

author
Marijn Besseling <njirambem@gmail.com> · 2026-08-16 17:01 UTC
commit
e1e0ec09b964d72b8fa14146af851e4087479a4e
parent
91b21499b2
tree
browse at this commit

21 files changed +662 -462

Blog/Components/App.razor +2 -43

@@ -1,4 +1,4 @@
1 -<!DOCTYPE html>
1 +<!DOCTYPE html>
2 2 <html lang="en">
3 3
4 4 <head>
@@ -11,28 +11,6 @@
11 11 <HeadOutlet/>
12 12 </head>
13 13
14 -<header>
15 - <svg xmlns="http://www.w3.org/2000/svg" id="logo" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink"
16 - viewBox="0 0 800 400">
17 - <path
18 - d="M54.26008987426758,304.0358581542969C52.801195856730146,265.55753484090167,32.49626341501872,141.97009913126627,47.085201263427734,114.79820251464844C61.67413911183675,87.62630589803061,91.907320950826,171.49774983723958,126.00896453857422,170.40357971191406C160.11060812632243,169.30940958658854,195.1031356048584,84.06875615437826,214.79820251464844,109.41703796386719C234.49326942443847,134.7653197733561,195.51569310506184,269.53661931355794,222.8699493408203,295.0672607421875C250.22420557657878,320.59790217081706,342.2152226257324,258.137511138916,349.32733154296875,234.97756958007812C356.4394404602051,211.81762802124024,257.30044911702475,206.87891296386718,257.8475341796875,181.16590881347656C258.39461924235025,155.45290466308595,360.5889183044434,123.65619580586751,352.0179138183594,108.52017211914062C343.44690933227537,93.38414843241374,243.41403477986654,107.09117195129394,215.69505310058594,106.72644805908203"
19 - fill="none" stroke-width="22" stroke="url(&quot;#SvgjsLinearGradient1001&quot;)" stroke-linecap="round"
20 - stroke-dasharray="125 106"
21 - transform="matrix(1.477455443789063,0,0,1.477455443789063,146.53673293574354,-95.92382431145532)">
22 - <animate attributeName="stroke-dashoffset" values="230;184;138;92;46;0" dur="2s" repeatCount="1"
23 - begin="logo.mouseenter" restart="whenNotActive"/>
24 -
25 - </path>
26 - <defs>
27 - <linearGradient id="SvgjsLinearGradient1001" gradientTransform="rotate(360, 0.5, 0.5)">
28 - <stop stop-color="hsl(37, 99%, 67%)" offset="0"></stop>
29 - <stop stop-color="hsl(316, 73%, 52%)" offset="1"></stop>
30 - </linearGradient>
31 - </defs>
32 - </svg>
33 - <h1>.bes.is</h1>
34 -</header>
35 -
36 14 <body class="center">
37 15 <Routes/>
38 16 <script src="@Assets["_framework/blazor.web.js"]" autostart="false"></script>
@@ -43,23 +21,4 @@
43 21 </script>
44 22 </body>
45 23
46 -<footer>
47 - <ul>
48 - <li><NavLink href="/">Home</NavLink></li>
49 - <li><NavLink href="/calc">Calculator</NavLink></li>
50 - <li><NavLink href="/concat">Concatenator</NavLink></li>
51 - <li><NavLink href="/letterflixd">Netflix to Letterboxd converter</NavLink></li>
52 - <li><NavLink href="/generator">Number generator</NavLink></li>
53 - <li><NavLink href="/note">Note</NavLink></li>
54 - <li><NavLink href="/qrcodeScan">QR-code scanner</NavLink></li>
55 - <li><NavLink href="/qrcodeGenerator">QR-code generator</NavLink></li>
56 - <li><NavLink href="/BRPTestData">BRP Test Data</NavLink></li>
57 - <li><NavLink href="/Query">Query</NavLink></li>
58 - <li><NavLink href="/Storage">Storage</NavLink></li>
59 - <li><NavLink href="https://git.bes.is">Git</NavLink></li>
60 - <!-- <li><a href="/webrtc.html">WebRTC</a></li> -->
61 - <!-- <li><a href="/spotify/index.html">Spotify</a></li> -->
62 - </ul>
63 -</footer>
64 -
65 -</html>
No newline at end of file
24 +</html>

Blog/Components/Layout/MainLayout.razor +11 -1

@@ -1,3 +1,13 @@
1 -@inherits LayoutComponentBase
1 +@inherits LayoutComponentBase
2 +
3 +<SiteHeader/>
2 4
3 5 @Body
6 +
7 +<SiteFooter/>
8 +
9 +<div id="blazor-error-ui" data-nosnippet>
10 + An unhandled error has occurred.
11 + <a href="." class="reload">Reload</a>
12 + <span class="dismiss">x</span>
13 +</div>

Blog/Components/Layout/MainLayout.razor.css +13 -14

@@ -1,20 +1,19 @@
1 +/* Shown by blazor.web.js when an unhandled error reaches the browser. */
1 2 #blazor-error-ui {
2 - color-scheme: light only;
3 - background: lightyellow;
4 - bottom: 0;
5 - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
6 - box-sizing: border-box;
7 3 display: none;
8 - left: 0;
9 - padding: 0.6rem 1.25rem 0.7rem 1.25rem;
10 4 position: fixed;
11 - width: 100%;
5 + inset-block-end: 0;
6 + inset-inline: 0;
12 7 z-index: 1000;
8 + padding: var(--s-1) var(--s1);
9 + background-color: var(--background);
10 + color: var(--color);
11 + border-block-start: 2px solid var(--error);
13 12 }
14 13
15 - #blazor-error-ui .dismiss {
16 - cursor: pointer;
17 - position: absolute;
18 - right: 0.75rem;
19 - top: 0.5rem;
20 - }
14 +#blazor-error-ui .dismiss {
15 + cursor: pointer;
16 + position: absolute;
17 + inset-inline-end: var(--s-1);
18 + inset-block-start: var(--s-1);
19 +}

Blog/Components/Layout/SiteFooter.razor +18 -0

@@ -0,0 +1,18 @@
1 +<footer>
2 + <ul>
3 + <li><NavLink href="/" Match="NavLinkMatch.All">Home</NavLink></li>
4 + <li><NavLink href="/calc">Calculator</NavLink></li>
5 + <li><NavLink href="/concat">Concatenator</NavLink></li>
6 + <li><NavLink href="/letterflixd">Netflix to Letterboxd converter</NavLink></li>
7 + <li><NavLink href="/generator">Number generator</NavLink></li>
8 + <li><NavLink href="/note">Note</NavLink></li>
9 + <li><NavLink href="/qrcodeScan">QR-code scanner</NavLink></li>
10 + <li><NavLink href="/qrcodeGenerator">QR-code generator</NavLink></li>
11 + <li><NavLink href="/BRPTestData">BRP Test Data</NavLink></li>
12 + <li><NavLink href="/Query">Query</NavLink></li>
13 + <li><NavLink href="/Storage">Storage</NavLink></li>
14 + <li><NavLink href="https://git.bes.is">Git</NavLink></li>
15 + <!-- <li><a href="/webrtc.html">WebRTC</a></li> -->
16 + <!-- <li><a href="/spotify/index.html">Spotify</a></li> -->
17 + </ul>
18 +</footer>

Blog/Components/Layout/SiteHeader.razor +21 -0

@@ -0,0 +1,21 @@
1 +<header>
2 + <svg xmlns="http://www.w3.org/2000/svg" id="logo" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink"
3 + viewBox="0 0 800 400">
4 + <path
5 + d="M54.26008987426758,304.0358581542969C52.801195856730146,265.55753484090167,32.49626341501872,141.97009913126627,47.085201263427734,114.79820251464844C61.67413911183675,87.62630589803061,91.907320950826,171.49774983723958,126.00896453857422,170.40357971191406C160.11060812632243,169.30940958658854,195.1031356048584,84.06875615437826,214.79820251464844,109.41703796386719C234.49326942443847,134.7653197733561,195.51569310506184,269.53661931355794,222.8699493408203,295.0672607421875C250.22420557657878,320.59790217081706,342.2152226257324,258.137511138916,349.32733154296875,234.97756958007812C356.4394404602051,211.81762802124024,257.30044911702475,206.87891296386718,257.8475341796875,181.16590881347656C258.39461924235025,155.45290466308595,360.5889183044434,123.65619580586751,352.0179138183594,108.52017211914062C343.44690933227537,93.38414843241374,243.41403477986654,107.09117195129394,215.69505310058594,106.72644805908203"
6 + fill="none" stroke-width="22" stroke="url(&quot;#SvgjsLinearGradient1001&quot;)" stroke-linecap="round"
7 + stroke-dasharray="125 106"
8 + transform="matrix(1.477455443789063,0,0,1.477455443789063,146.53673293574354,-95.92382431145532)">
9 + <animate attributeName="stroke-dashoffset" values="230;184;138;92;46;0" dur="2s" repeatCount="1"
10 + begin="logo.mouseenter" restart="whenNotActive"/>
11 +
12 + </path>
13 + <defs>
14 + <linearGradient id="SvgjsLinearGradient1001" gradientTransform="rotate(360, 0.5, 0.5)">
15 + <stop stop-color="hsl(37, 99%, 67%)" offset="0"></stop>
16 + <stop stop-color="hsl(316, 73%, 52%)" offset="1"></stop>
17 + </linearGradient>
18 + </defs>
19 + </svg>
20 + <h1>.bes.is</h1>
21 +</header>

Blog/Components/Pages/BRP.razor +1 -1

@@ -4,7 +4,7 @@
4 4 <main>
5 5 <div>
6 6 <details>
7 - <summary><h1 class="inline-block">Request</h1></summary>
7 + <summary><h1>Request</h1></summary>
8 8 <JsonRender Element="RequestBody?.RootElement"></JsonRender>
9 9 </details>
10 10 </div>

Blog/Components/Pages/Calc.razor +5 -43

@@ -5,50 +5,12 @@
5 5 <main>
6 6 <p>A rpn calculator</p>
7 7 <form id="form" class="flex-row">
8 - <input id="input" type="text" placeholder="1 2 +" />
8 + <input id="input" type="text" placeholder="1 2 +"/>
9 9 <input type="submit" value="Evaluate">
10 10 </form>
11 11 <Log/>
12 12
13 - <fieldset class="docs">
14 - <legend>Available stack operations</legend>
15 - <details>
16 - <summary>dup</summary>
17 - <p>Duplicates the top value on the stack</p>
18 - <div class="flex-spread">
19 - <span>[1, 2, 3]</span>
20 - <span>dup</span>
21 - </div>
22 - <div class="flex-spread">
23 - <span>[1, 1, 2, 3]</span>
24 - <span></span>
25 - </div>
26 - </details>
27 -
28 - <details>
29 - <summary>drop</summary>
30 - <p>Drops the top value on the stack</p>
31 - <div class="flex-spread">
32 - <span>[1, 2, 3]</span>
33 - <span>drop</span>
34 - </div>
35 - <div class="flex-spread">
36 - <span>[2, 3]</span>
37 - <span></span>
38 - </div>
39 - </details>
40 -
41 - <details>
42 - <summary>swap</summary>
43 - <p>Swaps the top two values on the stack</p>
44 - <div class="flex-spread">
45 - <span>[1, 2, 3]</span>
46 - <span>swap</span>
47 - </div>
48 - <div class="flex-spread">
49 - <span>[2, 1, 3]</span>
50 - <span></span>
51 - </div>
52 - </details>
53 - </fieldset>
54 -</main>
No newline at end of file
13 + <Panel Legend="Available stack operations">
14 + <StackOps/>
15 + </Panel>
16 +</main>

Blog/Components/Pages/Concat.razor +28 -109

@@ -9,117 +9,36 @@
9 9 </form>
10 10 <Log/>
11 11
12 - <fieldset class="docs">
13 - <legend>Predefined operations</legend>
14 - <details>
15 - <summary>dup</summary>
16 - <p>Duplicates the top value on the stack</p>
17 - <div class="flex-spread">
18 - <span>[1, 2, 3]</span>
19 - <span>dup</span>
20 - </div>
21 - <div class="flex-spread">
22 - <span>[1, 1, 2, 3]</span>
23 - <span></span>
24 - </div>
25 - </details>
12 + <Panel Legend="Predefined operations">
13 + <StackOps/>
26 14
27 - <details>
28 - <summary>drop</summary>
29 - <p>Drops the top value on the stack</p>
30 - <div class="flex-spread">
31 - <span>[1, 2, 3]</span>
32 - <span>drop</span>
33 - </div>
34 - <div class="flex-spread">
35 - <span>[2, 3]</span>
36 - <span></span>
37 - </div>
38 - </details>
15 + <StackOp Name="skip"
16 + Description="Skips forward in the program by the amount on the top of the stack. If the value on the top of the stack is 0 or negative, the value is dropped and the program does not skip ahead.">
17 + <StackStep Stack="[1, 2, 3]" Program="skip drop dup"/>
18 + <StackStep Stack="[2, 3]" Program="dup"/>
19 + <StackStep Stack="[2, 2, 3]"/>
20 + <p>Or</p>
21 + <StackStep Stack="[-1, 2, 3]" Program="skip drop dup"/>
22 + <StackStep Stack="[2, 3]" Program="drop dup"/>
23 + <StackStep Stack="[3]" Program="dup"/>
24 + <StackStep Stack="[3, 3]"/>
25 + </StackOp>
39 26
40 - <details>
41 - <summary>swap</summary>
42 - <p>Swaps the top two values on the stack</p>
43 - <div class="flex-spread">
44 - <span>[1, 2, 3]</span>
45 - <span>swap</span>
46 - </div>
47 - <div class="flex-spread">
48 - <span>[2, 1, 3]</span>
49 - <span></span>
50 - </div>
51 - </details>
52 -
53 - <details>
54 - <summary>skip</summary>
55 - <p>Skips forward in the program by the amount on the top of the stack.
56 - If the value on the top of the stack is 0 or negative,
57 - the value is dropped and the program does not skip ahead.
27 + <StackOp Name=": word ... ;" Description="Creates a new definition.">
28 + <p>
29 + Example: ": double 2 * ;" defines a new word called double. Any subsequent mention of double will
30 + replace the word with its definition.
58 31 </p>
59 - <div class="flex-spread">
60 - <span>[1, 2, 3]</span>
61 - <span>skip drop dup</span>
62 - </div>
63 - <div class="flex-spread">
64 - <span>[2, 3]</span>
65 - <span>dup</span>
66 - </div>
67 - <div class="flex-spread">
68 - <span>[2, 2, 3]</span>
69 - <span></span>
70 - </div>
71 - Or
72 - <div class="flex-spread">
73 - <span>[-1, 2, 3]</span>
74 - <span>skip drop dup</span>
75 - </div>
76 - <div class="flex-spread">
77 - <span>[2, 3]</span>
78 - <span>drop dup</span>
79 - </div>
80 - <div class="flex-spread">
81 - <span>[3]</span>
82 - <span>dup</span>
83 - </div>
84 - <div class="flex-spread">
85 - <span>[3, 3]</span>
86 - <span></span>
87 - </div>
88 - </details>
32 + <StackStep Stack="[]" Program=": double 2 * ; 3 double"/>
33 + <StackStep Stack="[]" Program="3 double"/>
34 + <StackStep Stack="[3]" Program="double"/>
35 + <StackStep Stack="[3]" Program="2 *"/>
36 + <StackStep Stack="[2, 3]" Program="*"/>
37 + <StackStep Stack="[6] <=="/>
38 + </StackOp>
39 + </Panel>
89 40
90 - <details>
91 - <summary>: word ... ;</summary>
92 - <p>Creates a new definition.</p>
93 - <p>Example: ": double 2 * ;" defines a new word called double. Any subsequent mention of double will
94 - replace the word with its definition.</p>
95 - <div class="flex-spread">
96 - <span>[]</span>
97 - <span>: double 2 * ; 3 double</span>
98 - </div>
99 - <div class="flex-spread">
100 - <span>[]</span>
101 - <span>3 double</span>
102 - </div>
103 - <div class="flex-spread">
104 - <span>[3]</span>
105 - <span>double</span>
106 - </div>
107 - <div class="flex-spread">
108 - <span>[3]</span>
109 - <span>2 *</span>
110 - </div>
111 - <div class="flex-spread">
112 - <span>[2, 3]</span>
113 - <span>*</span>
114 - </div>
115 - <div class="flex-spread">
116 - <span>[6] &lt;==</span>
117 - <span></span>
118 - </div>
119 - </details>
120 - </fieldset>
121 - <fieldset class="docs">
122 - <legend>Example programs</legend>
41 + <Panel Legend="Example programs">
123 42 <details>
124 43 <summary>Factorial</summary>
125 44 <NavLink
@@ -132,5 +51,5 @@
132 51 7 !
133 52 </pre>
134 53 </details>
135 - </fieldset>
136 -</main>
No newline at end of file
54 + </Panel>
55 +</main>

Blog/Components/Pages/Error.razor +21 -19

@@ -1,29 +1,31 @@
1 -@page "/Error"
1 +@page "/Error"
2 2 @using System.Diagnostics
3 3 @using Microsoft.AspNetCore.Http
4 4
5 5 <PageTitle>Error</PageTitle>
6 6
7 -<h1 class="text-danger">Error.</h1>
8 -<h2 class="text-danger">An error occurred while processing your request.</h2>
7 +<main>
8 + <h1>Error.</h1>
9 + <h2>An error occurred while processing your request.</h2>
9 10
10 -@if (ShowRequestId)
11 -{
11 + @if (ShowRequestId)
12 + {
13 + <p>
14 + <strong>Request ID:</strong> <code>@RequestId</code>
15 + </p>
16 + }
17 +
18 + <h3>Development Mode</h3>
12 19 <p>
13 - <strong>Request ID:</strong> <code>@RequestId</code>
20 + Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
14 21 </p>
15 -}
16 -
17 -<h3>Development Mode</h3>
18 -<p>
19 - Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
20 -</p>
21 -<p>
22 - <strong>The Development environment shouldn't be enabled for deployed applications.</strong>
23 - It can result in displaying sensitive information from exceptions to end users.
24 - For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
25 - and restarting the app.
26 -</p>
22 + <p>
23 + <strong>The Development environment shouldn't be enabled for deployed applications.</strong>
24 + It can result in displaying sensitive information from exceptions to end users.
25 + For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
26 + and restarting the app.
27 + </p>
28 +</main>
27 29
28 30 @code{
29 31 [CascadingParameter] private HttpContext? HttpContext { get; set; }
@@ -34,4 +36,4 @@
34 36 protected override void OnInitialized() =>
35 37 RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
36 38
37 -}
No newline at end of file
39 +}

Blog/Components/Pages/Letterflixd.razor +3 -3

@@ -8,7 +8,7 @@
8 8 <p>Import the resulting Letterboxd file at <a href="https://letterboxd.com/import/"
9 9 target="_blank">https://letterboxd.com/import/</a></p>
10 10 <form id="form">
11 - <fieldset class="docs">
11 + <Panel>
12 12 <div class="row">
13 13 <label for="netflix-file">
14 14 Netflix viewing history file:
@@ -18,7 +18,7 @@
18 18 <div class="row">
19 19 <input type="submit" value="Convert">
20 20 </div>
21 - </fieldset>
21 + </Panel>
22 22 </form>
23 23 <Log/>
24 -</main>
No newline at end of file
24 +</main>

Blog/Components/Pages/NotFound.razor +7 -3

@@ -1,4 +1,8 @@
1 -@page "/not-found"
1 +@page "/not-found"
2 2
3 -<h3>Not Found</h3>
4 -<p>Sorry, the content you are looking for does not exist.</p>
No newline at end of file
3 +<PageTitle>Not Found</PageTitle>
4 +
5 +<main>
6 + <h1>Not Found</h1>
7 + <p>Sorry, the content you are looking for does not exist.</p>
8 +</main>

Blog/Components/Pages/Note.razor +2 -2

@@ -3,6 +3,6 @@
3 3 <PageScript Src="./Components/Pages/Note.razor.js"></PageScript>
4 4
5 5 <main>
6 - <textarea id="input" style="height:60vh" autofocus></textarea>
6 + <textarea id="input" class="editor-tall" autofocus></textarea>
7 7 <Log/>
8 -</main>
No newline at end of file
8 +</main>

Blog/Components/Pages/QRCodeScan.razor.js +0 -2

@@ -74,8 +74,6 @@ export function onLoad() {
74 74 video.setAttribute("playsinline", "");
75 75 video.setAttribute("autoplay", "");
76 76 video.setAttribute("muted", "");
77 - video.style.width = "100%";
78 - // video.style.height = "100%";
79 77
80 78 /* Setting up the constraint */
81 79 const facingMode = "environment"; // Can be 'user' or 'environment' to access back or front camera (NEAT!)

Blog/Components/Pages/Query.razor +5 -6

@@ -3,22 +3,21 @@
3 3 <PageScript Src="./Components/Pages/Query.razor.js"/>
4 4
5 5 <main>
6 - <div class="flex-row" style="align-items:center">
6 + <div class="flex-row">
7 7 <label for="lang">Query language</label>
8 8 <select id="lang">
9 9 <option value="jsonql" selected>jsonql-js</option>
10 10 <option value="dactal">DACTAL</option>
11 11 </select>
12 12 </div>
13 - <textarea id="input" style="height:20vh" autofocus>SELECT *
13 + <textarea id="input" class="editor" autofocus>SELECT *
14 14 FROM personen AS p
15 15 WHERE 'EenhoofdigOuderlijkGezag' IN p.gezag[].type</textarea>
16 16 <div id="results">
17 17 </div>
18 18 <Log/>
19 19
20 - <fieldset class="docs">
21 - <legend>Credits</legend>
20 + <Panel Legend="Credits">
22 21 <div>
23 22 jsonql-js, a pure-JS SQL-like query language for JSON
24 23 </div>
@@ -29,5 +28,5 @@ WHERE 'EenhoofdigOuderlijkGezag' IN p.gezag[].type</textarea>
29 28 personen dataset from <a href="https://github.com/BRP-API/Haal-Centraal-BRP-bevragen">Haal-Centraal-BRP-bevragen
30 29 mock-data</a>
31 30 </div>
32 - </fieldset>
33 -</main>
No newline at end of file
31 + </Panel>
32 +</main>

Blog/Components/Pages/Storage.razor +21 -21

@@ -3,45 +3,45 @@
3 3 <PageScript Src="./Components/Pages/Storage.razor.js"></PageScript>
4 4
5 5 <main>
6 - <fieldset class="docs">
7 - <legend>Stores <button id="newStoreButton">New</button></legend>
8 - <div id="storeList" class="grid-stores">
9 -
10 - </div>
11 - </fieldset>
12 -
6 + <Panel>
7 + <LegendContent>
8 + Stores
9 + <button id="newStoreButton">New</button>
10 + </LegendContent>
11 + <ChildContent>
12 + <div id="storeList" class="grid-stores">
13 + </div>
14 + </ChildContent>
15 + </Panel>
16 +
13 17 <dialog id="inputDialog">
14 - <fieldset class="docs">
15 - <legend id="inputTitle"></legend>
18 + <Panel LegendId="inputTitle">
16 19 <form method="dialog" id="inputForm">
17 20 <p>
18 - <input id="inputDialogInput" type="text" />
21 + <input id="inputDialogInput" type="text"/>
19 22 </p>
20 - <div>
23 + <div class="flex-row">
21 24 <button type="submit">Confirm</button>
22 25 <button id="cancelInputButton" type="reset">Cancel</button>
23 26 </div>
24 27 </form>
25 - </fieldset>
28 + </Panel>
26 29 </dialog>
27 -
30 +
28 31 <dialog id="confirmDialog">
29 - <fieldset class="docs">
30 - <legend>Are you sure?</legend>
32 + <Panel Legend="Are you sure?">
31 33 <form method="dialog" id="confirmForm">
32 34 <p id="confirmMessage">
33 -
34 35 </p>
35 - <div>
36 + <div class="flex-row">
36 37 <button type="submit">Confirm</button>
37 38 <button id="cancelConfirmButton" type="reset">Cancel</button>
38 39 </div>
39 40 </form>
40 - </fieldset>
41 + </Panel>
41 42 </dialog>
42 -
43 +
43 44 <div id="openedStores">
44 -
45 45 </div>
46 46 <Log/>
47 -</main>
No newline at end of file
47 +</main>

Blog/Components/Pages/Storage.razor.js +1 -1

@@ -110,7 +110,7 @@ function openStore(storeName) {
110 110 const openedStores = getById("openedStores");
111 111
112 112 const addButton = h("button", "Add");
113 - const storeElement = h("fieldset", {class: "docs"}, [h("legend", [storeName + " ", addButton])]);
113 + const storeElement = h("fieldset", {class: "panel"}, [h("legend", [storeName + " ", addButton])]);
114 114 const closeStore = () => {
115 115 openedStores.removeChild(storeElement);
116 116 };

Blog/Components/_Shared/Panel.razor +36 -0

@@ -0,0 +1,36 @@
1 +@* A bordered box with a title: docs, dialog bodies, grouped controls. *@
2 +
3 +<fieldset class="panel">
4 + @if (Legend is not null || LegendContent is not null || LegendId is not null)
5 + {
6 + <legend id="@LegendId">
7 + @if (LegendContent is not null)
8 + {
9 + @LegendContent
10 + }
11 + else
12 + {
13 + @Legend
14 + }
15 + </legend>
16 + }
17 + @ChildContent
18 +</fieldset>
19 +
20 +@code {
21 + /// <summary>Plain text title. Ignored when <see cref="LegendContent"/> is set.</summary>
22 + [Parameter]
23 + public string? Legend { get; set; }
24 +
25 + /// <summary>Title markup, for when the legend holds more than text.</summary>
26 + [Parameter]
27 + public RenderFragment? LegendContent { get; set; }
28 +
29 + /// <summary>Set when script needs to fill the legend in at runtime.</summary>
30 + [Parameter]
31 + public string? LegendId { get; set; }
32 +
33 + [Parameter]
34 + public RenderFragment? ChildContent { get; set; }
35 +
36 +}

Blog/Components/_Shared/StackOp.razor +24 -0

@@ -0,0 +1,24 @@
1 +@* Documentation for a single stack operation: name, what it does, and worked examples. *@
2 +
3 +<details>
4 + <summary>@Name</summary>
5 + @if (Description is not null)
6 + {
7 + <p>@Description</p>
8 + }
9 + @ChildContent
10 +</details>
11 +
12 +@code {
13 + [Parameter]
14 + [EditorRequired]
15 + public string Name { get; set; } = null!;
16 +
17 + [Parameter]
18 + public string? Description { get; set; }
19 +
20 + /// <summary>Worked examples, as <see cref="StackStep"/> elements.</summary>
21 + [Parameter]
22 + public RenderFragment? ChildContent { get; set; }
23 +
24 +}

Blog/Components/_Shared/StackOps.razor +16 -0

@@ -0,0 +1,16 @@
1 +@* The stack operations the calculator and the concatenative language share. *@
2 +
3 +<StackOp Name="dup" Description="Duplicates the top value on the stack">
4 + <StackStep Stack="[1, 2, 3]" Program="dup"/>
5 + <StackStep Stack="[1, 1, 2, 3]"/>
6 +</StackOp>
7 +
8 +<StackOp Name="drop" Description="Drops the top value on the stack">
9 + <StackStep Stack="[1, 2, 3]" Program="drop"/>
10 + <StackStep Stack="[2, 3]"/>
11 +</StackOp>
12 +
13 +<StackOp Name="swap" Description="Swaps the top two values on the stack">
14 + <StackStep Stack="[1, 2, 3]" Program="swap"/>
15 + <StackStep Stack="[2, 1, 3]"/>
16 +</StackOp>

Blog/Components/_Shared/StackStep.razor +16 -0

@@ -0,0 +1,16 @@
1 +@* One line of a worked stack example: the stack on the left, the program left to run on the right. *@
2 +
3 +<div class="flex-spread">
4 + <span>@Stack</span>
5 + <span>@Program</span>
6 +</div>
7 +
8 +@code {
9 + [Parameter]
10 + [EditorRequired]
11 + public string Stack { get; set; } = null!;
12 +
13 + [Parameter]
14 + public string? Program { get; set; }
15 +
16 +}

Blog/wwwroot/app.css +411 -194

@@ -1,310 +1,491 @@
1 -h1:focus {
2 - outline: none;
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);
3 51 }
4 52
5 -.valid.modified:not([type=checkbox]) {
6 - outline: 1px solid #26b050;
53 +
54 +/* 2. Reset & base ========================================================== */
55 +
56 +*,
57 +*::before,
58 +*::after {
59 + box-sizing: border-box;
7 60 }
8 61
9 -.invalid {
10 - outline: 1px solid #e50000;
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;
11 68 }
12 69
13 -.validation-message {
14 - color: #e50000;
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);
15 79 }
16 80
17 -.blazor-error-boundary {
18 - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
19 - padding: 1rem 1rem 1rem 3.7rem;
20 - color: white;
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;
21 87 }
22 88
23 - .blazor-error-boundary::after {
24 - content: "An error has occurred."
25 - }
89 +:focus:not(:focus-visible) {
90 + outline: none;
91 +}
26 92
27 -.darker-border-checkbox.form-check-input {
28 - border-color: #929292;
93 +::selection {
94 + background-color: var(--color);
95 + color: var(--background);
29 96 }
30 97
31 -.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
32 - color: var(--bs-secondary-color);
33 - text-align: end;
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;
34 111 }
35 112
36 -.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
37 - text-align: start;
113 +h1 {
114 + font-size: calc(1em * var(--ratio));
38 115 }
39 116
40 -* {
41 - box-sizing: border-box;
42 - font-size: calc(1rem + 0.4vw);
43 - font-family: monospace;
117 +h2 {
118 + font-size: calc(1em * 1.25);
44 119 }
45 120
46 -:root {
47 - --black: #050505;
48 - --white: #fafafa;
49 - --error: #da0000;
50 - --info: #669aba;
51 - --green: #5fbb5d;
121 +h3,
122 +h4,
123 +h5,
124 +h6 {
125 + font-size: 1em;
126 +}
52 127
53 - --ratio: 1.5;
54 - --s0: 1rem;
55 - --s1: calc(var(--s0) * var(--ratio));
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);
56 137 }
57 138
58 -@media (prefers-color-scheme: dark) {
59 - :root {
60 - --background: var(--black);
61 - --color: var(--white);
62 - }
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;
63 144 }
64 145
65 -@media (prefers-color-scheme: light) {
66 - :root {
67 - --background: var(--white);
68 - --color: var(--black);
69 - }
146 +small {
147 + font-size: 0.75em;
70 148 }
71 149
72 -body {
73 - background-color: var(--background);
74 - color: var(--color);
150 +a {
151 + color: inherit;
152 + text-decoration-line: underline;
153 + text-decoration-thickness: var(--border-width);
154 + text-underline-offset: 0.2em;
75 155 }
76 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 +
77 167 .center {
78 168 box-sizing: content-box;
79 - max-inline-size: 80ch;
169 + max-inline-size: var(--measure);
80 170 margin-inline: auto;
81 - padding-inline-start: var(--s1);
82 - padding-inline-end: var(--s1);
171 + padding-inline: var(--s1);
83 172 }
84 173
85 -#logo {
86 - height: 5em;
87 - display: inline-block;
174 +main {
175 + flex: 1;
176 + border-block-start: var(--rule);
177 + border-block-end: var(--rule);
178 + padding-block: var(--s0);
88 179 }
89 180
90 -header > h1 {
91 - color: var(--header);
92 - display: inline-block;
181 +.flex-row {
182 + display: flex;
183 + flex-wrap: wrap;
184 + align-items: center;
185 + gap: var(--s0);
93 186 }
94 187
95 -footer li:has(a:focus)::marker {
96 - content: "==> ";
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;
97 191 }
98 192
99 -footer li:has(a:hover)::marker {
100 - content: " >";
193 +.flex-spread {
194 + display: flex;
195 + flex-wrap: wrap;
196 + gap: var(--s0);
197 + justify-content: space-between;
101 198 }
102 199
103 -footer li:has(a:hover:focus)::marker {
104 - content: "==>";
200 +.row + .row {
201 + margin-block-start: var(--s0);
105 202 }
106 203
107 -footer li::marker {
108 - content: "> ";
204 +/* Two columns, but only while they stay wide enough to read. */
205 +.two-column {
206 + columns: 2 24ch;
207 + gap: var(--s1);
109 208 }
110 209
111 -a {
112 - color: var(--color);
210 +.noselect {
211 + user-select: none;
212 + -webkit-user-select: none;
113 213 }
114 214
115 -a:empty::before {
116 - content: attr(href);
117 -}
118 215
119 -input[type="text"] {
120 - width: 80%;
121 - display: inline-block;
122 - background-color: var(--background);
123 - color: var(--color);
124 - border-color: var(--color);
125 - border-radius: 0.5em;
126 -}
216 +/* 5. Controls ============================================================== */
127 217
128 -input[type="text"] {
129 - width: 100%;
130 - display: inline-block;
131 - margin-inline: 0 1em;
132 - font-family: monospace;
133 - white-space: pre-wrap;
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;
134 226 background-color: var(--background);
135 - color: var(--color);
136 - border-color: var(--border);
137 - border-radius: 0.5em;
138 -}
139 -
140 -label + input {
141 - margin-block-end: 1em;
227 + border: var(--border-width) solid var(--border);
228 + border-radius: var(--radius);
229 + padding: 0.1em 0.5em;
230 + max-inline-size: 100%;
142 231 }
143 232
144 233 label {
145 234 display: block;
146 235 }
147 236
148 -.textarea,
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 +
149 243 textarea {
150 - width: 100%;
151 244 display: block;
152 - margin-inline: 0 1em;
153 - font-family: monospace;
245 + inline-size: 100%;
246 + min-block-size: 4lh;
247 + resize: vertical;
154 248 white-space: pre-wrap;
155 - background-color: var(--background);
156 - color: var(--color);
157 - border-color: var(--border);
158 - border-radius: 0.5em;
159 249 }
160 250
161 -pre {
162 - font-family: monospace;
163 - white-space: pre-line;
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;
164 268 }
165 269
166 270 button,
167 271 input[type="button"],
168 272 input[type="submit"],
169 -input[type="file"]::file-selector-button {
170 - min-width: 20%;
171 - display: inline-block;
172 - color: var(--color);
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;
173 308 background-color: var(--background);
174 - border: 1px solid var(--color);
175 - border-radius: 0.5em;
176 - padding: 0.1em 1em;
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);
177 314 cursor: pointer;
178 - font-family: monospace;
179 315 }
180 316
317 +::file-selector-button:hover {
318 + background-color: var(--color);
319 + color: var(--background);
320 +}
181 321
182 -.flex-row {
183 - display: flex;
184 - gap: 1em;
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;
185 330 }
186 331
187 -.flex-spread {
188 - display: flex;
189 - gap: 1em;
190 - justify-content: space-between;
332 +input[type="checkbox"]::after {
333 + content: "[ ]";
191 334 }
192 335
193 -.history {
194 - filter: opacity(30%);
336 +input[type="checkbox"]:checked::after {
337 + content: "[x]";
195 338 }
196 339
197 -div + .history {
198 - margin-block-start: 2em;
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);
199 348 }
200 349
201 -main {
202 - border-block-start: 2px dotted var(--color);
203 - border-block-end: 2px dotted var(--color);
204 - padding-block: 1em;
350 +legend {
351 + padding-inline: var(--s-2);
205 352 }
206 353
207 -.docs {
208 - margin: 2em 0;
209 - border: 2px dashed var(--color);
210 - padding: 0.5em 1em;
211 - border-radius: 0.5em;
354 +legend::before {
355 + content: "( ";
212 356 }
213 357
214 -.error::before {
215 - content: "ERR: ";
216 - color: var(--error);
358 +legend::after {
359 + content: " )";
217 360 }
218 361
219 -.info::before {
220 - content: "INF: ";
221 - color: var(--info);
362 +summary {
363 + cursor: help;
222 364 }
223 365
224 -label:has(> #debug:not(:checked)) + #log .debug {
225 - display: none;
366 +summary::marker {
367 + content: "> ";
226 368 }
227 369
228 -.debug::before {
229 - content: "DBG: ";
230 - color: var(--info);
370 +details[open] > summary::marker {
371 + content: "x ";
231 372 }
232 373
233 -.row + .row {
234 - margin-block-start: 1em;
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;
235 377 }
236 378
237 -.noselect {
238 - user-select: none;
239 - -moz-user-select: -moz-none;
240 - -webkit-user-select: none;
241 - -ms-user-select: none;
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);
242 389 }
243 390
244 -img {
245 - margin-inline: auto;
246 - margin-block: 1em;
391 +header > h1 {
392 + margin-block: 0;
247 393 }
248 394
249 -footer > ul {
250 - columns: 2;
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;
251 405 }
252 406
253 -.two-column {
254 - columns: 2;
407 +footer li::marker {
408 + content: "> ";
255 409 }
256 410
257 -legend::before {
258 - content: "( ";
411 +footer li:has(a:hover)::marker {
412 + content: " >";
259 413 }
260 414
261 -legend::after {
262 - content: " )";
415 +footer li:has(a:focus)::marker {
416 + content: "==> ";
263 417 }
264 418
265 -summary {
266 - cursor: help;
419 +footer li:has(a:hover:focus)::marker {
420 + content: "==>";
267 421 }
268 422
269 -summary::marker {
270 - content: "> ";
423 +/* NavLink marks the current page for us. */
424 +footer li:has(a.active)::marker {
425 + content: "* ";
271 426 }
272 427
273 -details[open] summary::marker {
274 - content: "x ";
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);
275 434 }
276 435
277 -input[type="checkbox"] {
278 - appearance: none;
436 +/* -- Log ------------------------------------------------------------------ */
437 +
438 +#log {
439 + margin-block-start: var(--s0);
279 440 }
280 -input[type="checkbox"]:checked::after {
281 - content: "[x]";
282 - color: var(--color);
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;
283 448 }
284 449
285 -input[type="checkbox"]::after {
286 - content: "[ ]";
287 - color: var(--color);
450 +.error::before {
451 + content: "ERR: ";
452 + color: var(--error);
288 453 }
289 454
290 -small {
291 - font-size: 60%;
455 +.info::before {
456 + content: "INF: ";
457 + color: var(--info);
292 458 }
293 459
294 -.icon {
295 - color: var(--color);
296 - width: 1em;
460 +.debug::before {
461 + content: "DBG: ";
462 + color: var(--info);
297 463 }
298 464
299 -span.name {
300 - color: var(--color);
301 - filter: opacity(50%);
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;
302 468 }
303 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 +
304 481 li.dash {
305 482 list-style-type: "- ";
306 483 }
307 484
485 +span.name {
486 + opacity: 50%;
487 +}
488 +
308 489 .json-true {
309 490 color: var(--green);
310 491 }
@@ -317,32 +498,68 @@ li.dash {
317 498 color: var(--info);
318 499 }
319 500
320 -.inline-block {
321 - display: inline-block;
322 -}
323 -
324 -dialog::backdrop {
325 - backdrop-filter: blur(calc(var(--s1) / 20));
326 -}
501 +/* -- Dialogs -------------------------------------------------------------- */
327 502
328 503 dialog {
329 504 background-color: transparent;
330 505 color: var(--color);
331 506 border: none;
332 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));
333 513 }
334 514
335 -dialog > fieldset {
515 +/* The panel inside supplies the surface, so the dialog itself can stay bare. */
516 +dialog > .panel {
517 + margin-block: 0;
336 518 background-color: var(--background);
337 519 }
338 520
339 -dialog > fieldset > legend {
521 +dialog > .panel > legend {
340 522 background-color: var(--background);
341 - border-radius: 1rem;
523 + border-radius: var(--radius);
342 524 }
343 525
526 +/* -- Storage ------------------------------------------------------------- */
527 +
344 528 .grid-stores {
345 529 display: grid;
346 - grid-gap: 1em;
347 - grid-template-columns: 1fr 0.4fr 0.4fr;
348 -}
No newline at end of file
530 + grid-template-columns: 1fr auto auto;
531 + align-items: center;
532 + gap: var(--s-1) var(--s0);
533 +}
534 +
535 +
536 +/* 7. Media ================================================================= */
537 +
538 +#logo {
539 + block-size: 5em;
540 + flex: none;
541 +}
542 +
543 +img {
544 + display: block;
545 + max-inline-size: 100%;
546 + block-size: auto;
547 + margin-inline: auto;
548 + margin-block: var(--s0);
549 +}
550 +
551 +video {
552 + display: block;
553 + inline-size: 100%;
554 + block-size: auto;
555 + border-radius: var(--radius);
556 +}
557 +
558 +#qrcode {
559 + margin-block: var(--s0);
560 + text-align: center;
561 +}
562 +
563 +#qrcode :is(canvas, table) {
564 + margin-inline: auto;
565 +}