main
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <title>Flux — Design Sandbox</title>
7 <link rel="stylesheet" href="style.css">
8</head>
9<body>
10 <div class="site-controls"><button class="theme-toggle" id="theme-toggle" title="Toggle dark/light mode">☀️</button></div>
11 <article>
12
13 <!-- ============================================================
14 SECTION 1: TYPOGRAPHY & DESIGN FUNDAMENTALS
15 ============================================================ -->
16
17 <h1>Flux — Design Sandbox</h1>
18 <p class="subtitle">Exploring typography, rhythm, and stream design for vincent.demeester.fr</p>
19
20 <nav class="nav-toc">
21 <a href="#typography">Typography</a> ·
22 <a href="#rhythm">Rhythm</a> ·
23 <a href="#components">Components</a> ·
24 <a href="#stream">Stream</a> ·
25 <a href="#tufte">Tufte</a> ·
26 <a href="#colors">Colors</a>
27 </nav>
28
29 <h2 id="typography"><a class="anchor" href="#typography">§</a>Typography Specimen</h2>
30
31 <div class="specimen specimen-serif">
32 <div class="label">Body — Serif (Iowan Old Style / Palatino)</div>
33 <p class="text-base">The quick brown fox jumps over the lazy dog. 0123456789</p>
34 <p class="text-base text-italic">The quick brown fox jumps over the lazy dog. — italic</p>
35 <p class="text-base text-bold">The quick brown fox jumps over the lazy dog. — bold</p>
36 </div>
37
38 <div class="specimen specimen-sans">
39 <div class="label">Headings — Sans (Seravek / Gill Sans)</div>
40 <p class="text-base">The quick brown fox jumps over the lazy dog. 0123456789</p>
41 <p class="text-base text-semibold">The quick brown fox jumps over the lazy dog. — semibold</p>
42 </div>
43
44 <div class="specimen specimen-mono">
45 <div class="label">Code — Mono (Cascadia Code / Source Code Pro)</div>
46 <p class="text-sm">func main() { fmt.Println("Hello, flux!") } // 0xDEAD</p>
47 </div>
48
49 <h3>Heading Sizes</h3>
50
51 <div class="specimen specimen-headings">
52 <h1>H1 — Page Title (2rem)</h1>
53 <h2>H2 — Section Heading (1.4rem)</h2>
54 <h3>H3 — Subsection (1.15rem, italic)</h3>
55 <p>Body text at 18px base size, 1.6 line-height = 28.8px rhythm unit.</p>
56 </div>
57
58 <h3>Dropcap</h3>
59
60 <p class="dropcap">Gardens can be very personal and full of whimsy or a garden can be a source of food and substance. This is my personal space on the World Wide Web. It is meant to be simple, <em>modest</em> and <strong>persistent</strong> — by persistent, it means that I am trying to not break URIs. The list below is a "selection" of some content.</p>
61
62 <h3>Small Caps</h3>
63
64 <p>This website uses <span class="smallcaps">Tufte CSS</span> as its foundation, taking cues from <span class="smallcaps">Edward Tufte</span>'s principles of data-ink ratio and information density. The <span class="smallcaps">HTML</span> is semantic and the <span class="smallcaps">CSS</span> is minimal.</p>
65
66 <!-- ============================================================
67 SECTION 2: LINKS
68 ============================================================ -->
69
70 <h2 id="links"><a class="anchor" href="#links">§</a>Link Styles</h2>
71
72 <p>Links are styled following <a href="https://adactio.com/journal/22084">Adactio's guidance</a>: subtle underline offset, thin decoration, translucent color. Hover reveals full underline. Compare:</p>
73
74 <ul>
75 <li><a href="#">Default styled link</a> — <code>text-underline-offset: 0.15em</code></li>
76 <li><a href="#" class="link-thick">Thicker underline</a> — <code>text-decoration-thickness: 2px</code></li>
77 <li><a href="#" class="link-offset">More offset</a> — <code>text-underline-offset: 0.3em</code></li>
78 <li><a href="#" class="link-accent">Accent-colored underline</a> — <code>text-decoration-color: accent</code></li>
79 <li><a href="#" class="link-dotted">Dotted underline</a> — <code>text-decoration-style: dotted</code></li>
80 </ul>
81
82 <!-- ============================================================
83 SECTION 3: VERTICAL RHYTHM
84 ============================================================ -->
85
86 <h2 id="rhythm"><a class="anchor" href="#rhythm">§</a>Vertical Rhythm</h2>
87
88 <p>All spacing derives from <code>--lh</code> (one line-height unit = 28.8px at 18px/1.6). Following <a href="https://pawelgrzybek.com/vertical-rhythm-using-css-lh-and-rlh-units/">Pawel Grzybek's approach</a> with <code>lh</code> units, and <a href="https://christiantietze.de">Christian Tietze's</a> CSS custom properties for consistency.</p>
89
90 <p>Paragraph spacing: <code>1lh</code> (one line). Heading top margin: <code>2lh</code>. This keeps text on a consistent baseline grid, which your eye perceives as calm and ordered even without consciously noticing it.</p>
91
92 <p><a href="https://practicaltypography.com/line-spacing.html">Butterick says</a>: optimal line spacing is 120–145% of point size. We use 160% (1.6) — generous but not loose, good for long-form reading on screens.</p>
93
94 <!-- ============================================================
95 SECTION 4: BLOCK ELEMENTS
96 ============================================================ -->
97
98 <h2 id="components"><a class="anchor" href="#components">§</a>Block Elements</h2>
99
100 <h3>Blockquote — Standard</h3>
101
102 <blockquote>
103 <p>While not everybody has or works in a dirt garden, we all share a familiarity with the idea of what a garden is. A garden is usually a place where things grow.</p>
104 <cite>Joel Hooks</cite>
105 </blockquote>
106
107 <h3>Blockquote — Epigraph</h3>
108
109 <blockquote class="epigraph">
110 <p>“The phrase “digital garden” is a metaphor for thinking about writing and creating that focuses less on the resulting “content”, and more on the process, care, and craft it takes to get there.”</p>
111 <cite>Joel Hooks</cite>
112 </blockquote>
113
114 <h3>Blockquote — Pullquote</h3>
115
116 <blockquote class="pullquote">
117 <p>If everything is highlighted, nothing is highlighted.</p>
118 <cite>Nikita Tonsky</cite>
119 </blockquote>
120
121 <h3>Blockquote — Nested / Multi-paragraph</h3>
122
123 <blockquote>
124 <p>Minimalism helps one focus on the content. Anything besides the content is distraction and not design.</p>
125 <p>‘Attention!’, as Ikkyu would say.</p>
126 <cite>Gwern</cite>
127 </blockquote>
128
129 <h3>Code — Tonsky-style Syntax Highlighting</h3>
130
131 <p>Following <a href="https://tonsky.me/blog/syntax-highlighting/">Tonsky’s principles</a>: only 4 semantic categories get color. <span class="hl-pill hl-pill-string">Strings (green bg)</span>, <span class="hl-pill hl-pill-comment">comments (warm bg)</span>, <span class="hl-pill hl-pill-def">definitions (blue bg)</span>, and <span class="hl-pill-constant">constants (purple text)</span>. Everything else stays default. No bold.</p>
132
133 <h4>Go</h4>
134
135 <pre><code><span class="cmt">// Entry represents a single item in the flux stream.</span>
136<span class="cmt">// It can be a GitHub PR, a bookmark, a note, etc.</span>
137<span class="k">type</span> <span class="fn-def">Entry</span> <span class="k">struct</span> {
138 ID <span class="typ">string</span> <span class="str">`json:"id"`</span>
139 Kind <span class="typ">string</span> <span class="str">`json:"kind"`</span>
140 Title <span class="typ">string</span> <span class="str">`json:"title"`</span>
141 URL <span class="typ">string</span> <span class="str">`json:"url"`</span>
142 Date <span class="typ">time.Time</span> <span class="str">`json:"date"`</span>
143 Tags []<span class="typ">string</span> <span class="str">`json:"tags"`</span>
144 Body <span class="typ">string</span> <span class="str">`json:"body,omitempty"`</span>
145}
146
147<span class="k">func</span> <span class="fn-def">NewEntry</span>(kind <span class="typ">string</span>, title <span class="typ">string</span>, url <span class="typ">string</span>) *<span class="typ">Entry</span> {
148 <span class="k">return</span> &<span class="typ">Entry</span>{
149 ID: generateID(kind, url),
150 Kind: kind,
151 Title: title,
152 URL: url,
153 Date: time.Now(),
154 }
155}
156
157<span class="k">const</span> (
158 KindGitHubPR = <span class="str">"github-pr"</span>
159 KindBookmark = <span class="str">"bookmark"</span>
160 MaxRetries = <span class="num">3</span>
161 DefaultTimeout = <span class="num">30</span> * time.Second
162 EnableCache = <span class="const">true</span>
163)</code></pre>
164
165 <h4>Python</h4>
166
167 <pre><code><span class="k">import</span> json
168<span class="k">from</span> pathlib <span class="k">import</span> Path
169<span class="k">from</span> datetime <span class="k">import</span> datetime
170
171<span class="cmt"># Tonsky: comments deserve bright color.</span>
172<span class="cmt"># Good comments ADD to code.</span>
173
174<span class="k">class</span> <span class="fn-def">FluxAggregator</span>:
175 <span class="str">"""Merge entries from all sources into a single feed."""</span>
176
177 <span class="k">def</span> <span class="fn-def">__init__</span>(self, cache_dir: <span class="typ">Path</span>, max_entries: <span class="typ">int</span> = <span class="num">500</span>):
178 self.cache_dir = cache_dir
179 self.max_entries = max_entries
180 self.sources: list[<span class="typ">Source</span>] = []
181 self._last_run: <span class="typ">datetime</span> | <span class="const">None</span> = <span class="const">None</span>
182
183 <span class="k">def</span> <span class="fn-def">generate</span>(self) -> list[<span class="typ">dict</span>]:
184 <span class="str">"""Fetch from all sources, merge, deduplicate, sort."""</span>
185 entries = []
186 <span class="k">for</span> source <span class="k">in</span> self.sources:
187 new = source.fetch(since=self._last_run)
188 entries.extend(new)
189
190 <span class="cmt"># Sort reverse-chronological, deduplicate by ID</span>
191 seen = set()
192 result = []
193 <span class="k">for</span> e <span class="k">in</span> sorted(entries, key=<span class="k">lambda</span> x: x[<span class="str">"date"</span>], reverse=<span class="const">True</span>):
194 <span class="k">if</span> e[<span class="str">"id"</span>] <span class="k">not</span> <span class="k">in</span> seen:
195 seen.add(e[<span class="str">"id"</span>])
196 result.append(e)
197
198 <span class="k">return</span> result[:<span class="num">500</span>]</code></pre>
199
200 <h4>Nix</h4>
201
202 <pre><code>{ pkgs, lib, config, ... }:
203
204<span class="k">let</span>
205 <span class="cmt"># Build the flux binary from source</span>
206 <span class="fn-def">flux</span> = pkgs.buildGoModule <span class="k">rec</span> {
207 pname = <span class="str">"flux"</span>;
208 version = <span class="str">"0.1.0"</span>;
209 src = ../.;
210 vendorHash = <span class="str">"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="</span>;
211 };
212<span class="k">in</span> {
213 systemd.services.flux-generate = {
214 description = <span class="str">"Generate flux stream"</span>;
215 serviceConfig = {
216 Type = <span class="str">"oneshot"</span>;
217 ExecStart = <span class="str">"${flux}/bin/flux generate"</span>;
218 User = <span class="str">"www"</span>;
219 };
220 };
221
222 systemd.timers.flux-generate = {
223 wantedBy = [ <span class="str">"timers.target"</span> ];
224 timerConfig.OnCalendar = <span class="str">"hourly"</span>;
225 timerConfig.Persistent = <span class="const">true</span>;
226 };
227}</code></pre>
228
229 <div class="callout">
230 <div class="callout-title">Tonsky’s 4-color rule</div>
231 <p>Notice how scannable the code becomes. Your eye immediately finds strings, comments, definitions, and constants. Keywords like <code>func</code>, <code>if</code>, <code>return</code> are just code — you never search for a keyword, you search for the name after it.</p>
232 </div>
233
234 <h3>Callouts</h3>
235
236 <div class="callout callout-note">
237 <div class="callout-title">Note</div>
238 <p>A general note or annotation. Use for supplementary information the reader might find useful but isn’t essential to the main flow.</p>
239 </div>
240
241 <div class="callout callout-tip">
242 <div class="callout-title">Tip</div>
243 <p>Use <code>flux generate --dry-run</code> to preview what entries would be added without writing any files. Handy when adding a new source.</p>
244 </div>
245
246 <div class="callout callout-info">
247 <div class="callout-title">Info</div>
248 <p>The JSON Feed spec v1.1 allows an <code>_extension</code> namespace for custom fields. We use <code>_flux</code> to store entry kind and source metadata.</p>
249 </div>
250
251 <div class="callout callout-warning">
252 <div class="callout-title">Warning</div>
253 <p>GitHub’s search API is limited to 30 requests/minute even with authentication. The cache mechanism avoids hitting this on every run, but a full re-fetch (<code>flux cache clear</code>) will be slow.</p>
254 </div>
255
256 <div class="callout callout-danger">
257 <div class="callout-title">Danger</div>
258 <p>Never commit your <code>GITHUB_TOKEN</code> to the repository. Use environment variables or a secrets manager. The token has read access to all your public activity.</p>
259 </div>
260
261 <div class="callout callout-design">
262 <div class="callout-title">Design Note</div>
263 <p>This callout style is borrowed from <a href="https://craftinginterpreters.com">Crafting Interpreters</a>. Each type has a left border, subtle background wash, and icon prefix. Colors follow the same CSS variable system — they adapt to light and dark mode automatically.</p>
264 </div>
265
266 <h3>Table</h3>
267
268 <table>
269 <thead>
270 <tr>
271 <th>Reference</th>
272 <th>Key Takeaway</th>
273 <th>Applied Here</th>
274 </tr>
275 </thead>
276 <tbody>
277 <tr>
278 <td>Gwern</td>
279 <td>Aesthetically-pleasing minimalism</td>
280 <td>Grayscale palette, dropcaps</td>
281 </tr>
282 <tr>
283 <td>Tufte CSS</td>
284 <td>Margins are for thinking</td>
285 <td>Sidenotes in margin</td>
286 </tr>
287 <tr>
288 <td>Crafting Interpreters</td>
289 <td>Book-quality web typography</td>
290 <td>Serif body, § anchors</td>
291 </tr>
292 <tr>
293 <td>Miessler</td>
294 <td>Warm parchment, premium fonts</td>
295 <td>Color palette</td>
296 </tr>
297 <tr>
298 <td>Tietze</td>
299 <td>CSS custom props for rhythm</td>
300 <td><code>--lh</code> system</td>
301 </tr>
302 <tr>
303 <td>Larlet</td>
304 <td>Seasonal list markers</td>
305 <td><code>body:has(time)</code> trick</td>
306 </tr>
307 </tbody>
308 </table>
309
310 <h3>Figures & Images</h3>
311
312 <h4>Standard figure with caption</h4>
313
314 <figure>
315 <img src="placeholder-wide.svg" alt="A landscape placeholder showing layout elements">
316 <figcaption><strong>Figure 1.</strong> A standard figure occupies the full content width. The caption uses the heading font, italic, with a bold label prefix. Good for diagrams, charts, and photographs.</figcaption>
317 </figure>
318
319 <h4>Margin figure (Tufte-style)</h4>
320
321 <figure class="margin">
322 <img src="placeholder-square.svg" alt="A small square placeholder">
323 <figcaption><strong>Fig. 2.</strong> A margin figure floats in the right margin alongside the text, just like sidenotes. On mobile, it becomes full-width.</figcaption>
324 </figure>
325
326 <p>This paragraph has a margin figure floating next to it. The margin figure is a Tufte signature — small supporting images placed in the margin where the reader’s eye can find them without breaking the reading flow. They share the same space as sidenotes, so you should avoid placing both on the same paragraph.</p>
327
328 <p>The margin figure works especially well for small diagrams, icons, logos, or thumbnail images that support but don’t dominate the text. On narrow screens, it collapses inline.</p>
329
330 <h4>Bordered figure (framed)</h4>
331
332 <figure class="bordered">
333 <img src="placeholder-wide.svg" alt="A framed placeholder">
334 <figcaption><strong>Figure 3.</strong> A bordered figure adds a subtle frame — useful for screenshots or images that blend into the background without a border.</figcaption>
335 </figure>
336
337 <h4>Screenshot (drop shadow)</h4>
338
339 <figure class="screenshot">
340 <img src="placeholder-wide.svg" alt="A screenshot-style placeholder">
341 <figcaption><strong>Figure 4.</strong> Screenshots get a drop shadow and rounded corners, making them feel like floating windows. The shadow adapts to light/dark mode.</figcaption>
342 </figure>
343
344 <h4>Full-width figure</h4>
345
346 <figure class="fullwidth">
347 <img src="placeholder-wide.svg" alt="A full-width placeholder">
348 <figcaption><strong>Figure 5.</strong> A full-width figure stretches across the content column <em>and</em> the margin area. Good for panoramic images, wide diagrams, or data visualizations that need horizontal space.</figcaption>
349 </figure>
350
351 <h4>Figure grid — 2 columns</h4>
352
353 <div class="figure-grid cols-2">
354 <figure>
355 <img src="placeholder-square.svg" alt="Grid image 1">
356 <figcaption>Before</figcaption>
357 </figure>
358 <figure>
359 <img src="placeholder-square.svg" alt="Grid image 2">
360 <figcaption>After</figcaption>
361 </figure>
362 </div>
363
364 <h4>Figure grid — 3 columns</h4>
365
366 <div class="figure-grid cols-3">
367 <figure>
368 <img src="placeholder-tall.svg" alt="Grid image A">
369 <figcaption>v1.0</figcaption>
370 </figure>
371 <figure>
372 <img src="placeholder-tall.svg" alt="Grid image B">
373 <figcaption>v2.0</figcaption>
374 </figure>
375 <figure>
376 <img src="placeholder-tall.svg" alt="Grid image C">
377 <figcaption>v3.0 (current)</figcaption>
378 </figure>
379 </div>
380
381 <h4>No caption (bare image)</h4>
382
383 <figure>
384 <img src="placeholder-wide.svg" alt="An image without a caption">
385 </figure>
386
387 <div class="callout callout-design">
388 <div class="callout-title">Design Note</div>
389 <p>Org-mode exports images as <code><figure></code> + <code><figcaption></code> via <code>#+CAPTION:</code>. The margin figure maps to Tufte’s <code>#+ATTR_HTML: :class margin</code>. The grid layout would need manual HTML or a Soupault transform.</p>
390 </div>
391
392 <h3>Lists — Marker Options</h3>
393
394 <p>Pick a list marker style. Current is ✧ (white star).</p>
395
396 <ul>
397 <li>This is the current marker (✧ white star)</li>
398 <li>A second item to see the rhythm</li>
399 <li>And a third with <a href="#">a link</a> and <code>inline code</code></li>
400 </ul>
401
402 <h4>Alternatives</h4>
403
404 <ul class="marker-reference">
405 <li>Reference mark (※)</li>
406 <li>Japanese-influenced</li>
407 <li>Unusual but elegant</li>
408 </ul>
409
410 <ul class="marker-hyphen">
411 <li>Hyphen bullet (⁃)</li>
412 <li>Like a dash but shorter</li>
413 <li>Understated</li>
414 </ul>
415
416 <ul class="marker-endash">
417 <li>En dash (–)</li>
418 <li>Typographically clean</li>
419 <li>Common in European typography</li>
420 </ul>
421
422 <ul class="marker-hedera">
423 <li>Hedera (❧)</li>
424 <li>Typographic fleuron</li>
425 <li>Distinctive, bookish</li>
426 </ul>
427
428 <ol>
429 <li>Ordered lists stay numbered</li>
430 <li>Nothing fancy here</li>
431 <li>Just clean spacing</li>
432 </ol>
433
434 <!-- ============================================================
435 SECTION 5: TUFTE SIDENOTES
436 ============================================================ -->
437
438 <h2 id="tufte"><a class="anchor" href="#tufte">§</a>Tufte Sidenotes</h2>
439
440 <p>
441 Edward Tufte's distinctive style places supplementary information in the margin
442 rather than in footnotes at the bottom of the page.<span class="sidenote-number"></span>
443 <span class="sidenote">This is a numbered sidenote. On wide screens, it floats in the right margin. On narrow screens, it becomes an inline block below the paragraph. The approach follows <a href="https://gwern.net/sidenote">Gwern's sidenote analysis</a>.</span>
444 This keeps the reader's eye on the page instead of bouncing back and forth.
445 The idea is simple: if something is worth saying, say it nearby.
446 </p>
447
448 <p>
449 Margin notes are similar but unnumbered — useful for brief asides
450 or contextual links.<span class="marginnote">This is an unnumbered margin note. Kenneth Reitz calls this "margins are for thinking." These work beautifully with Tufte CSS and ox-tufte.</span>
451 They provide a visual rhythm to the page, filling what would otherwise
452 be empty whitespace with relevant context.
453 </p>
454
455 <p>
456 A paragraph without sidenotes, for contrast. Notice how the text column stays at a comfortable reading width of about 640px (roughly 65 characters per line), which typographers consider optimal.<span class="sidenote-number"></span>
457 <span class="sidenote">Robert Bringhurst recommends 45–75 characters per line. We aim for ~65. This is also what Gwern targets.</span>
458 The margin area is a bonus, not a crutch.
459 </p>
460
461 <!-- ============================================================
462 SECTION 6: COLORS
463 ============================================================ -->
464
465 <h2 id="colors"><a class="anchor" href="#colors">§</a>Color Palette</h2>
466
467 <p>Light mode draws from Daniel Miessler's warm parchment. Dark mode from Beat's stream. Both shift the same CSS variables.</p>
468
469 <h3>Accent Color Picker</h3>
470
471 <p>Click a swatch to preview it as the accent color across the whole page — dropcaps, blockquote borders, callout labels, pullquotes, release cards, sidenote numbers, and § anchors all update live.</p>
472
473 <div class="accent-picker">
474
475 <div class="accent-group">
476 <div class="accent-group-label">Greens — Deep</div>
477 <div class="palette">
478 <div class="accent-swatch" data-light="#1b4332" data-dark="#40916c" title="Evergreen"></div>
479 <div class="accent-swatch" data-light="#2d6a4f" data-dark="#52b788" title="Forest"></div>
480 <div class="accent-swatch" data-light="#245e3a" data-dark="#4a9e6a" title="Pine"></div>
481 <div class="accent-swatch" data-light="#2b5f3a" data-dark="#5aad70" title="Spruce"></div>
482 <div class="accent-swatch" data-light="#1a5c3a" data-dark="#3da870" title="Hunter"></div>
483 <div class="accent-swatch" data-light="#2e4a3e" data-dark="#5a8a72" title="Holly"></div>
484 </div>
485 </div>
486
487 <div class="accent-group">
488 <div class="accent-group-label">Greens — Mid</div>
489 <div class="palette">
490 <div class="accent-swatch" data-light="#4a7c59" data-dark="#6aab7b" title="Sage"></div>
491 <div class="accent-swatch" data-light="#588157" data-dark="#7dba6d" title="Fern"></div>
492 <div class="accent-swatch" data-light="#3a7d5e" data-dark="#5cb88a" title="Jade"></div>
493 <div class="accent-swatch" data-light="#4d7a4d" data-dark="#72b572" title="Moss"></div>
494 <div class="accent-swatch" data-light="#527a52" data-dark="#7ab87a" title="Clover"></div>
495 <div class="accent-swatch" data-light="#457b5e" data-dark="#68b088" title="Basil"></div>
496 <div class="accent-swatch" data-light="#3d7c47" data-dark="#60b06a" title="Leaf"></div>
497 <div class="accent-swatch" data-light="#4f7942" data-dark="#7ab568" title="Ivy"></div>
498 </div>
499 </div>
500
501 <div class="accent-group">
502 <div class="accent-group-label">Greens — Soft</div>
503 <div class="palette">
504 <div class="accent-swatch" data-light="#6b8f71" data-dark="#95b89a" title="Lichen"></div>
505 <div class="accent-swatch" data-light="#7a9e7a" data-dark="#a0c8a0" title="Celadon"></div>
506 <div class="accent-swatch" data-light="#6a8e6a" data-dark="#90b890" title="Laurel"></div>
507 <div class="accent-swatch" data-light="#698b69" data-dark="#8cb88c" title="Pistachio"></div>
508 <div class="accent-swatch" data-light="#789878" data-dark="#a2c2a2" title="Mint"></div>
509 <div class="accent-swatch" data-light="#839e76" data-dark="#aac89a" title="Willow"></div>
510 </div>
511 </div>
512
513 <div class="accent-group">
514 <div class="accent-group-label">Teal & Cyan</div>
515 <div class="palette">
516 <div class="accent-swatch" data-light="#006d5b" data-dark="#40b89a" title="Teal"></div>
517 <div class="accent-swatch" data-light="#005f73" data-dark="#4aabb8" title="Petrol"></div>
518 <div class="accent-swatch" data-light="#2a7a6a" data-dark="#52baa5" title="Verdigris"></div>
519 <div class="accent-swatch" data-light="#0a6c6c" data-dark="#42a8a8" title="Deep Teal"></div>
520 <div class="accent-swatch" data-light="#3a8a7a" data-dark="#60c0aa" title="Seafoam"></div>
521 <div class="accent-swatch" data-light="#1a7a7a" data-dark="#50b5b5" title="Lagoon"></div>
522 <div class="accent-swatch" data-light="#2b6b6b" data-dark="#5aadad" title="Patina"></div>
523 <div class="accent-swatch" data-light="#3d8888" data-dark="#6abfbf" title="Aqua"></div>
524 </div>
525 </div>
526
527 <div class="accent-group">
528 <div class="accent-group-label">Blues — Cool</div>
529 <div class="palette">
530 <div class="accent-swatch" data-light="#2a6496" data-dark="#7daacd" title="Steel"></div>
531 <div class="accent-swatch" data-light="#3a5a8c" data-dark="#7a9abf" title="Denim"></div>
532 <div class="accent-swatch" data-light="#4a6fa5" data-dark="#8aadd0" title="Wedgwood"></div>
533 <div class="accent-swatch" data-light="#2c5282" data-dark="#63a0d4" title="Navy"></div>
534 <div class="accent-swatch" data-light="#3f578f" data-dark="#7a92c2" title="Slate"></div>
535 <div class="accent-swatch" data-light="#1a5276" data-dark="#5a98c0" title="Marine"></div>
536 </div>
537 </div>
538
539 <div class="accent-group">
540 <div class="accent-group-label">Blues — Warm</div>
541 <div class="palette">
542 <div class="accent-swatch" data-light="#3b6b8a" data-dark="#70a5c5" title="Fjord"></div>
543 <div class="accent-swatch" data-light="#4a7a95" data-dark="#80acc5" title="Sky"></div>
544 <div class="accent-swatch" data-light="#3a7ca5" data-dark="#6aafcf" title="Cornflower"></div>
545 <div class="accent-swatch" data-light="#456a82" data-dark="#7aa0b8" title="Horizon"></div>
546 <div class="accent-swatch" data-light="#506a7e" data-dark="#85a0b5" title="Storm"></div>
547 <div class="accent-swatch" data-light="#4a6878" data-dark="#80a0b2" title="Rainy"></div>
548 </div>
549 </div>
550
551 <div class="accent-group">
552 <div class="accent-group-label">Warm Earth — Gold & Amber</div>
553 <div class="palette">
554 <div class="accent-swatch" data-light="#6f5500" data-dark="#d4aa3c" title="Gold"></div>
555 <div class="accent-swatch" data-light="#9a7b2a" data-dark="#d4aa3c" title="Ochre"></div>
556 <div class="accent-swatch" data-light="#8a6d3b" data-dark="#c4a265" title="Honey"></div>
557 <div class="accent-swatch" data-light="#7a6520" data-dark="#c0a040" title="Brass"></div>
558 <div class="accent-swatch" data-light="#8a7030" data-dark="#c8a850" title="Wheat"></div>
559 <div class="accent-swatch" data-light="#a08028" data-dark="#d8b848" title="Saffron"></div>
560 <div class="accent-swatch" data-light="#7a6838" data-dark="#b8a060" title="Khaki"></div>
561 </div>
562 </div>
563
564 <div class="accent-group">
565 <div class="accent-group-label">Warm Earth — Brown & Umber</div>
566 <div class="palette">
567 <div class="accent-swatch" data-light="#7a5c3a" data-dark="#b89060" title="Umber"></div>
568 <div class="accent-swatch" data-light="#6b4c30" data-dark="#a88058" title="Walnut"></div>
569 <div class="accent-swatch" data-light="#8b4513" data-dark="#c0763a" title="Saddle"></div>
570 <div class="accent-swatch" data-light="#704828" data-dark="#a87850" title="Bark"></div>
571 <div class="accent-swatch" data-light="#5c3d2e" data-dark="#9a7560" title="Chestnut"></div>
572 <div class="accent-swatch" data-light="#6a5040" data-dark="#a58870" title="Truffle"></div>
573 </div>
574 </div>
575
576 <div class="accent-group">
577 <div class="accent-group-label">Orange & Terracotta</div>
578 <div class="palette">
579 <div class="accent-swatch" data-light="#c05a30" data-dark="#e08858" title="Terracotta"></div>
580 <div class="accent-swatch" data-light="#a0522d" data-dark="#d48a5a" title="Sienna"></div>
581 <div class="accent-swatch" data-light="#b86830" data-dark="#e09858" title="Copper"></div>
582 <div class="accent-swatch" data-light="#c46a28" data-dark="#e8a050" title="Tangerine"></div>
583 <div class="accent-swatch" data-light="#a05040" data-dark="#d08070" title="Clay"></div>
584 <div class="accent-swatch" data-light="#9a5535" data-dark="#cc8860" title="Cinnamon"></div>
585 <div class="accent-swatch" data-light="#b8602a" data-dark="#e09050" title="Pumpkin"></div>
586 </div>
587 </div>
588
589 <div class="accent-group">
590 <div class="accent-group-label">Reds — Deep</div>
591 <div class="palette">
592 <div class="accent-swatch" data-light="#8a3a3a" data-dark="#c07070" title="Wine"></div>
593 <div class="accent-swatch" data-light="#b44" data-dark="#dc755c" title="Rust"></div>
594 <div class="accent-swatch" data-light="#a0413a" data-dark="#d47a6a" title="Brick"></div>
595 <div class="accent-swatch" data-light="#7a2e2e" data-dark="#b86060" title="Garnet"></div>
596 <div class="accent-swatch" data-light="#8b3040" data-dark="#c06878" title="Cranberry"></div>
597 <div class="accent-swatch" data-light="#6b2a2a" data-dark="#a86060" title="Maroon"></div>
598 <div class="accent-swatch" data-light="#943838" data-dark="#cc7070" title="Carmine"></div>
599 </div>
600 </div>
601
602 <div class="accent-group">
603 <div class="accent-group-label">Reds — Soft & Rose</div>
604 <div class="palette">
605 <div class="accent-swatch" data-light="#9a5a60" data-dark="#c89098" title="Rose"></div>
606 <div class="accent-swatch" data-light="#8a5068" data-dark="#c08898" title="Dusty Rose"></div>
607 <div class="accent-swatch" data-light="#a06070" data-dark="#d098a0" title="Blush"></div>
608 <div class="accent-swatch" data-light="#906060" data-dark="#c09090" title="Mauve Rose"></div>
609 <div class="accent-swatch" data-light="#985a68" data-dark="#c89098" title="Peony"></div>
610 </div>
611 </div>
612
613 <div class="accent-group">
614 <div class="accent-group-label">Purples</div>
615 <div class="palette">
616 <div class="accent-swatch" data-light="#6c5b7b" data-dark="#a090b0" title="Mauve"></div>
617 <div class="accent-swatch" data-light="#5b4a8a" data-dark="#8a7ab8" title="Iris"></div>
618 <div class="accent-swatch" data-light="#7a5080" data-dark="#b080b8" title="Plum"></div>
619 <div class="accent-swatch" data-light="#531ab6" data-dark="#b6a0ff" title="Violet"></div>
620 <div class="accent-swatch" data-light="#6a4c8a" data-dark="#9a80b8" title="Amethyst"></div>
621 <div class="accent-swatch" data-light="#5a3a7a" data-dark="#9070b0" title="Grape"></div>
622 <div class="accent-swatch" data-light="#7a5a8a" data-dark="#aa90b8" title="Lavender"></div>
623 <div class="accent-swatch" data-light="#604880" data-dark="#9580b5" title="Wisteria"></div>
624 </div>
625 </div>
626
627 <div class="accent-group">
628 <div class="accent-group-label">Neutrals & Greys</div>
629 <div class="palette">
630 <div class="accent-swatch" data-light="#5a6a6a" data-dark="#90a0a0" title="Graphite"></div>
631 <div class="accent-swatch" data-light="#4a5568" data-dark="#8090a0" title="Pewter"></div>
632 <div class="accent-swatch" data-light="#5f6a60" data-dark="#90a090" title="Sage Grey"></div>
633 <div class="accent-swatch" data-light="#606068" data-dark="#9898a0" title="Charcoal"></div>
634 <div class="accent-swatch" data-light="#5a5a68" data-dark="#9090a0" title="Flint"></div>
635 <div class="accent-swatch" data-light="#555" data-dark="#999" title="Iron"></div>
636 </div>
637 </div>
638
639 <p class="accent-current">Current accent: <span id="accent-label">#4a7c59</span></p>
640
641 </div>
642
643 <h3>Light Mode</h3>
644
645 <div class="palette">
646 <div class="swatch" data-bg="#f4f1eb">bg</div>
647 <div class="swatch" data-bg="#eae7e0">bg-alt</div>
648 <div class="swatch" data-bg="#fff">card</div>
649 <div class="swatch" data-bg="#2c2c2c">text</div>
650 <div class="swatch" data-bg="#6b6b6b">muted</div>
651 <div class="swatch" data-bg="#999">faint</div>
652 <div class="swatch" data-bg="#4a7c59">accent</div>
653 <div class="swatch" data-bg="#2a6496">link</div>
654 <div class="swatch" data-bg="#2d8a56">new</div>
655 <div class="swatch" data-bg="#7a6c2a">updated</div>
656 </div>
657
658 <h3>Dark Mode</h3>
659
660 <div class="palette">
661 <div class="swatch" data-bg="#1a1a1f">bg</div>
662 <div class="swatch" data-bg="#222228">bg-alt</div>
663 <div class="swatch" data-bg="rgb(51,51,57)">card</div>
664 <div class="swatch" data-bg="#d4d4d8">text</div>
665 <div class="swatch" data-bg="#9ca3af">muted</div>
666 <div class="swatch" data-bg="#6b7280">faint</div>
667 <div class="swatch" data-bg="#6aab7b">accent</div>
668 <div class="swatch" data-bg="#7daacd">link</div>
669 <div class="swatch" data-bg="#4ade80">new</div>
670 <div class="swatch" data-bg="#d4aa3c">updated</div>
671 </div>
672
673 <div class="callout">
674 <div class="callout-title">Solarized Alternative</div>
675 <p>Christian Tietze uses Solarized colors: <code>#002b36</code> dark, <code>#fdf5e6</code> cream, <code>#268bd2</code> blue. Could be an alternative palette — more muted, less warm than the Miessler-inspired one above.</p>
676 </div>
677
678 <!-- ============================================================
679 SECTION 7: FLUX STREAM — THE MAIN EVENT
680 ============================================================ -->
681
682 <hr>
683
684 <div class="flux">
685 <div class="flux-header">
686 <h1 id="stream">Flux</h1>
687 <p class="description">What I'm working on, reading, and thinking about.</p>
688 <div class="flux-feed-links">
689 <a href="#">JSON Feed</a>
690 <a href="#">Atom</a>
691 </div>
692 </div>
693
694 <div class="year-separator">2026</div>
695
696 <!-- === Release Card === -->
697
698 <div class="release-card">
699 <span class="release-project">tektoncd/pipeline</span>
700 <span class="release-version">v1.11.0</span>
701 <span class="release-date"> · March 30, 2026</span>
702 </div>
703
704 <!-- === Stream Card (note/TIL) === -->
705
706 <div class="stream-card" id="00115">
707 <a class="card-anchor" href="#00115">§</a>
708 <div class="card-date">
709 <time datetime="2026-03-28T14:30:00Z">March 28, 2026</time>
710 </div>
711 <div class="card-title">Nix flakes and reproducible Go builds</div>
712 <div class="card-body">
713 <p>Today I spent some time figuring out how to pin Go module dependencies
714 inside a Nix flake. The trick is using <code>buildGoModule</code> with
715 <code>vendorHash</code> — it fetches and vendors dependencies deterministically.</p>
716 <p>The <a href="https://nixos.wiki/wiki/Go">NixOS wiki Go page</a> is helpful
717 but incomplete. Had to read the nixpkgs source to understand
718 <code>proxyVendor</code>.</p>
719 </div>
720 <div class="tags">
721 <span class="tag">NixOS</span>
722 <span class="tag">Go</span>
723 <span class="tag">Flakes</span>
724 </div>
725 </div>
726
727 <!-- === Page Updates === -->
728
729 <a class="page-entry" href="#">
730 <span class="marker new">+</span>
731 <span class="page-title">Screen Freezes on Framework 16 with AMD and GNOME</span>
732 <span class="page-date">Mar 26</span>
733 <span class="arrow">↗</span>
734 </a>
735
736 <a class="page-entry" href="#">
737 <span class="marker updated">~</span>
738 <span class="page-title">Setting Up A Repo In Under 5 Minutes With Nix</span>
739 <span class="page-date">Mar 25</span>
740 <span class="arrow">↗</span>
741 </a>
742
743 <!-- === Another Stream Card === -->
744
745 <div class="stream-card" id="00114">
746 <a class="card-anchor" href="#00114">§</a>
747 <div class="card-date">
748 <time datetime="2026-03-22T10:00:00Z">March 22, 2026</time>
749 </div>
750 <div class="card-title">selfhostblocks</div>
751 <div class="card-body">
752 <p>Found <a href="https://github.com/ibizaman/selfhostblocks">selfhostblocks</a>
753 while browsing the Clan gitea. An interesting alternative approach to NixOS
754 service modules — more opinionated, batteries-included.</p>
755 <p>Though I'm pretty convinced by Clan so far.</p>
756 </div>
757 <div class="tags">
758 <span class="tag">NixOS</span>
759 <span class="tag">Selfhosting</span>
760 <span class="tag">Clan</span>
761 </div>
762 </div>
763
764 <!-- === GitHub Entries === -->
765
766 <div class="stream-card stream-card-compact">
767 <div class="card-date">
768 <time datetime="2026-03-20">March 20, 2026</time> · GitHub
769 </div>
770
771 <div class="github-entry">
772 <span class="gh-icon pr">⬡</span>
773 <div class="gh-content">
774 <div class="gh-title"><a href="#">Add matrix parameters support in PipelineRuns</a></div>
775 <div class="gh-repo">tektoncd/pipeline#8234</div>
776 </div>
777 <div class="gh-date">merged</div>
778 </div>
779
780 <div class="github-entry">
781 <span class="gh-icon pr">⬡</span>
782 <div class="gh-content">
783 <div class="gh-title"><a href="#">Fix dashboard routing for nested resources</a></div>
784 <div class="gh-repo">tektoncd/dashboard#3102</div>
785 </div>
786 <div class="gh-date">merged</div>
787 </div>
788
789 <div class="github-entry">
790 <span class="gh-icon issue">◈</span>
791 <div class="gh-content">
792 <div class="gh-title"><a href="#">Catalog v2 resolver: support for remote bundles</a></div>
793 <div class="gh-repo">tektoncd/pipeline#8240</div>
794 </div>
795 <div class="gh-date">opened</div>
796 </div>
797 </div>
798
799 <!-- === Bookmark Entry === -->
800
801 <div class="stream-card stream-card-compact">
802 <div class="card-date">
803 <time datetime="2026-03-18">March 18, 2026</time> · Bookmarks
804 </div>
805
806 <div class="bookmark-entry">
807 <div class="bm-title"><a href="https://stagex.tools">StageX — Minimal, reproducible container base images</a></div>
808 <div class="bm-note">Interesting approach to base images, similar philosophy to distroless but with better reproducibility.</div>
809 <div class="bm-meta">stagex.tools</div>
810 </div>
811
812 <div class="bookmark-entry">
813 <div class="bm-title"><a href="#">The Bitter Lesson — Rich Sutton</a></div>
814 <div class="bm-note">The essay that keeps coming back: general methods that leverage computation win over domain-specific engineering.</div>
815 <div class="bm-meta">incompleteideas.net</div>
816 </div>
817 </div>
818
819 <!-- === More page updates === -->
820
821 <a class="page-entry" href="#">
822 <span class="marker new">+</span>
823 <span class="page-title">Domain Name System</span>
824 <span class="page-date">Mar 15</span>
825 <span class="arrow">↗</span>
826 </a>
827
828 <a class="page-entry" href="#">
829 <span class="marker updated">~</span>
830 <span class="page-title">(Doom) Emacs Config Bits</span>
831 <span class="page-date">Mar 14</span>
832 <span class="arrow">↗</span>
833 </a>
834
835 <a class="page-entry" href="#">
836 <span class="marker updated">~</span>
837 <span class="page-title">Window Switching Made Simple</span>
838 <span class="page-date">Mar 12</span>
839 <span class="arrow">↗</span>
840 </a>
841
842 <!-- === Another note === -->
843
844 <div class="stream-card" id="00113">
845 <a class="card-anchor" href="#00113">§</a>
846 <div class="card-date">
847 <time datetime="2026-03-10T18:00:00Z">March 10, 2026</time>
848 </div>
849 <div class="card-title">Using <code>dig</code></div>
850 <div class="card-body">
851 <p>Just had a small DNS dispute again, and remembered the
852 <a href="https://jvns.ca/blog/2021/12/04/how-to-use-dig/">post by Julia Evans
853 on using <code>dig</code></a>. Pretty helpful!</p>
854 <p>She also wrote a <a href="https://dns-lookup.jvns.ca/">simple DNS lookup tool</a>.</p>
855 </div>
856 <div class="tags">
857 <span class="tag">DNS</span>
858 <span class="tag">TIL</span>
859 <span class="tag">Julia Evans</span>
860 </div>
861 </div>
862
863 <!-- === Release Card === -->
864
865 <div class="release-card">
866 <span class="release-project">tektoncd/triggers</span>
867 <span class="release-version">v0.30.0</span>
868 <span class="release-date"> · March 5, 2026</span>
869 </div>
870
871 <!-- === GitHub batch === -->
872
873 <div class="stream-card stream-card-compact">
874 <div class="card-date">
875 <time datetime="2026-03-04">March 4, 2026</time> · GitHub
876 </div>
877
878 <div class="github-entry">
879 <span class="gh-icon pr">⬡</span>
880 <div class="gh-content">
881 <div class="gh-title"><a href="#">nixos/navidrome: init module</a></div>
882 <div class="gh-repo">NixOS/nixpkgs#298412</div>
883 </div>
884 <div class="gh-date">merged</div>
885 </div>
886
887 <div class="github-entry">
888 <span class="gh-icon pr">⬡</span>
889 <div class="gh-content">
890 <div class="gh-title"><a href="#">pruner: add configurable retention policies</a></div>
891 <div class="gh-repo">tektoncd/pruner#87</div>
892 </div>
893 <div class="gh-date">merged</div>
894 </div>
895 </div>
896
897 <!-- === Longer note with code === -->
898
899 <div class="stream-card" id="00112">
900 <a class="card-anchor" href="#00112">§</a>
901 <div class="card-date">
902 <time datetime="2026-03-01T11:00:00Z">March 1, 2026</time>
903 </div>
904 <div class="card-title">Bun ❤️ SQLite</div>
905 <div class="card-body">
906 <p>Today I learned that Bun has a
907 <a href="https://bun.com/docs/runtime/sqlite">SQLite driver built in</a>. That's pretty cool:</p>
908
909 <pre><code>import { Database } from 'bun:sqlite'
910
911const db = new Database('mydb.sqlite', { create: true })
912
913const query = db.query(`
914 SELECT * FROM users WHERE name = ?
915`)
916
917console.log(query.all('vincent'))</code></pre>
918 </div>
919 <div class="tags">
920 <span class="tag">TIL</span>
921 <span class="tag">Bun</span>
922 <span class="tag">JavaScript</span>
923 </div>
924 </div>
925
926 <div class="year-separator">Earlier</div>
927
928 <a class="page-entry" href="#">
929 <span class="marker new">+</span>
930 <span class="page-title">A Digital Garden</span>
931 <span class="page-date">2024</span>
932 <span class="arrow">↗</span>
933 </a>
934
935 <a class="page-entry" href="#">
936 <span class="marker new">+</span>
937 <span class="page-title">Random thoughts after 2 years</span>
938 <span class="page-date">2022</span>
939 <span class="arrow">↗</span>
940 </a>
941
942 </div>
943
944 <!-- ============================================================
945 SECTION 8: PROSE EXAMPLE (full article feel)
946 ============================================================ -->
947
948 <hr>
949
950 <h2 id="prose"><a class="anchor" href="#prose">§</a>Prose Example</h2>
951
952 <p class="dropcap">This section demonstrates how a full article or journal entry would look with this design system. The warm background, serif body text, and generous line-height create a reading experience closer to a book than a typical blog. Every decision — from the font choice to the margin width — serves readability.</p>
953
954 <p>As I said in "Random thoughts after 2 years," I've been inspired by <a href="https://joelhooks.com/digital-garden">Joel's digital garden</a> article. This space is inspired by <a href="https://www.la-grange.net/">a</a> <a href="https://larlet.fr/david/">lot</a> <a href="https://tomcritchlow.com/">of</a> <a href="https://joelhooks.com/">other</a> spaces, but adapted to my vision.</p>
955
956 <p>I really like the way Joel speaks about it:<span class="sidenote-number"></span>
957 <span class="sidenote">Joel Hooks, <em>My blog is a digital garden, not a blog</em>. This framing changed how I think about publishing online.</span>
958 </p>
959
960 <blockquote>
961 <p>The phrase "digital garden" is a metaphor for thinking about writing and creating that focuses less on the resulting "content", and more on the process, care, and craft it takes to get there.</p>
962 </blockquote>
963
964 <p>I think I've always struggled with the blog approach. I sometimes want to publish things that are not related to time. That can be true no matter when you read it. The opposite is true as well — some things are deeply temporal, like a <span class="smallcaps">TIL</span> or a stream entry.</p>
965
966 <p>That's why the <em>flux</em> works alongside the garden: the garden holds evergreen pages; the flux captures the flow of time.<span class="sidenote-number"></span>
967 <span class="sidenote">Gwern makes a similar distinction with "tags" vs "essays." Some content is timeless reference, some is a snapshot of thinking at a moment.</span>
968 </p>
969
970 <h3>Technical Detail</h3>
971
972 <p>The flux tool itself is simple Go. The core type is an <code>Entry</code> struct with a <code>Kind</code> discriminator. Each source implements a <code>Fetch</code> method:</p>
973
974 <pre><code><span class="cmt">// Source is the interface all flux providers implement.</span>
975<span class="k">type</span> <span class="fn-def">Source</span> <span class="k">interface</span> {
976 <span class="fn-def">Name</span>() <span class="typ">string</span>
977 <span class="fn-def">Fetch</span>(ctx <span class="typ">context.Context</span>, since <span class="typ">time.Time</span>) ([]<span class="typ">Entry</span>, <span class="typ">error</span>)
978}</code></pre>
979
980 <p>The aggregator merges entries, sorts by date, deduplicates by <code>ID</code>, and feeds the result to renderers (JSON Feed, Atom, HTML template). No framework. No JavaScript. Just static files deployed via <code>rsync</code>.</p>
981
982 <div class="callout">
983 <div class="callout-title">Why no framework?</div>
984 <p>As Kenneth Reitz puts it: "If Fly.io disappears tomorrow, I can have this running on a Raspberry Pi in 10 minutes. Portability is a form of independence." Same principle here. The output is pure HTML+CSS that any web server can host. The Go binary is the only moving part.</p>
985 </div>
986
987 <!-- ============================================================
988 FOOTER
989 ============================================================ -->
990
991 <footer>
992 <p>Vincent Demeester · <a href="https://vincent.demeester.fr">vincent.demeester.fr</a></p>
993 <p class="mt-sm">Design sandbox — combining ideas from
994 <a href="https://beathagenlocher.com/stream/">Beat Hagenlocher</a>,
995 <a href="https://gwern.net/design">Gwern</a>,
996 <a href="https://danielmiessler.com">Daniel Miessler</a>,
997 <a href="https://christiantietze.de">Christian Tietze</a>,
998 <a href="https://craftinginterpreters.com">Crafting Interpreters</a>,
999 <a href="https://edwardtufte.github.io/tufte-css/">Tufte CSS</a>,
1000 <a href="https://larlet.fr/david/">David Larlet</a>,
1001 <a href="https://adactio.com">Adactio</a>,
1002 <a href="https://practicaltypography.com">Butterick</a>.
1003 </p>
1004 </footer>
1005
1006 </article>
1007
1008 <script src="flux.js"></script>
1009 <!-- removed inline script, see flux.js -->
1010 <!-- DEAD
1011 const toggle = document.getElementById('theme-toggle')
1012 const STORAGE_KEY = 'flux-theme'
1013
1014 // --- Helpers ---
1015 function getPreferredTheme() {
1016 const stored = localStorage.getItem(STORAGE_KEY)
1017 if (stored) return stored
1018 return window.matchMedia('(prefers-color-scheme: dark)').matches
1019 ? 'dark' : 'light'
1020 }
1021
1022 function hexToRgba(hex, alpha) {
1023 hex = hex.replace('#', '')
1024 if (hex.length === 3) hex = hex.split('').map(c => c + c).join('')
1025 const r = parseInt(hex.substring(0, 2), 16)
1026 const g = parseInt(hex.substring(2, 4), 16)
1027 const b = parseInt(hex.substring(4, 6), 16)
1028 return `rgba(${r}, ${g}, ${b}, ${alpha})`
1029 }
1030
1031 // --- Theme ---
1032 function applyTheme(theme) {
1033 document.documentElement.setAttribute('data-theme', theme)
1034 toggle.textContent = theme === 'dark' ? '☀️' : '🌙'
1035 localStorage.setItem(STORAGE_KEY, theme)
1036
1037 // Update swatch previews to show correct variant
1038 document.querySelectorAll('.accent-swatch').forEach(s => {
1039 s.style.background = theme === 'dark' ? s.dataset.dark : s.dataset.light
1040 })
1041
1042 // Re-apply accent for the new theme
1043 const l = localStorage.getItem('flux-accent-light')
1044 const d = localStorage.getItem('flux-accent-dark')
1045 if (l && d) applyAccent(l, d)
1046 }
1047
1048 // --- Accent ---
1049 function applyAccent(light, dark) {
1050 const root = document.documentElement
1051 const isDark = root.getAttribute('data-theme') === 'dark'
1052 const color = isDark ? dark : light
1053
1054 root.style.setProperty('--color-accent', color)
1055 root.style.setProperty('--color-accent-soft', hexToRgba(color, 0.08))
1056 root.style.setProperty('--callout-design-color', color)
1057 root.style.setProperty('--callout-design-bg', hexToRgba(color, 0.06))
1058
1059 localStorage.setItem('flux-accent-light', light)
1060 localStorage.setItem('flux-accent-dark', dark)
1061
1062 const label = document.getElementById('accent-label')
1063 if (label) label.textContent = `${light} / ${dark}`
1064
1065 document.querySelectorAll('.accent-swatch').forEach(s => {
1066 s.classList.toggle('active', s.dataset.light === light)
1067 })
1068 }
1069
1070 // --- Init theme ---
1071 applyTheme(getPreferredTheme())
1072
1073 // --- Init accent ---
1074 const storedLight = localStorage.getItem('flux-accent-light')
1075 const storedDark = localStorage.getItem('flux-accent-dark')
1076 if (storedLight && storedDark) {
1077 applyAccent(storedLight, storedDark)
1078 } else {
1079 document.querySelectorAll('.accent-swatch').forEach(s => {
1080 if (s.dataset.light === '#4a7c59') s.classList.add('active')
1081 })
1082 }
1083
1084 // --- Event listeners ---
1085 toggle.addEventListener('click', () => {
1086 const current = document.documentElement.getAttribute('data-theme')
1087 applyTheme(current === 'dark' ? 'light' : 'dark')
1088 })
1089
1090 document.querySelectorAll('.accent-swatch').forEach(swatch => {
1091 swatch.addEventListener('click', () => {
1092 applyAccent(swatch.dataset.light, swatch.dataset.dark)
1093 })
1094 })
1095
1096 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
1097 if (!localStorage.getItem(STORAGE_KEY)) {
1098 applyTheme(e.matches ? 'dark' : 'light')
1099 }
1100 })
1101DEAD -->
1102</body>
1103</html>