mykiwi/mykiwi.blogpublic Fork 0
1bdbd06
Commits
Clone
git clone https://git.rickub.com/mykiwi/mykiwi.blog.git
git clone ssh://git@rickub.com/mykiwi/mykiwi.blog.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Port fzakaria.com design to Hugo and add a homepage profile

Reimplements the fzakaria.com layout, typography, and color palette
(MIT-licensed) as Hugo templates and SCSS, pins dart-sass alongside
Hugo in flake.nix, and adds a data-driven bio card (colophon) to the
homepage sourced from params.profile in hugo.toml: avatar, name, a
markdown bio with links, and a links list. The <meta description> tag
now derives from that same bio automatically unless overridden.

Also ignores Hugo's local build artifacts (resources/_gen cache,
.hugo_build.lock), which are not source.
Romain Gautier committed 2026-09-18T11:18:56+02:00 Browse files
1bdbd06 parent: 1328429
modified .gitignore +2 -0
@@ -1,2 +1,4 @@
11 result
22 public/
3+resources/
4+.hugo_build.lock
@@ -1,2 +1,4 @@
1 result1 result
2 public/2 public/
3+resources/
4+.hugo_build.lock
modified README.md +12 -1
@@ -1,12 +1,14 @@
11 # mykiwi.blog
22
3+
34 ## Development shell
45
56 ```sh
67 nix develop
78 ```
89
9-Gives you `hugo` on `PATH`, pinned via `hugoVersion` in `flake.nix`.
10+Gives you `hugo` on `PATH`, pinned in `flake.nix`.
11+
1012
1113 ## Building
1214
@@ -15,3 +17,12 @@ nix build
1517 ```
1618
1719 Builds the site with Hugo and outputs it to `./result`.
20+
21+
22+## Design
23+
24+The typography, color palette, and layout (`blog/assets/css/style.scss`,
25+`blog/layouts/`) are ported from [fzakaria.com](https://github.com/fzakaria/fzakaria.com)
26+(MIT-licensed), reimplemented as Hugo templates. The Newsreader and
27+JetBrains Mono font files under `blog/static/fonts/` are open source (SIL OFL
28+/ Apache 2.0 respectively).
@@ -1,12 +1,14 @@
1 # mykiwi.blog1 # mykiwi.blog
2 2
3+
3 ## Development shell4 ## Development shell
4 5
5 ```sh6 ```sh
6 nix develop7 nix develop
7 ```8 ```
8 9
9-Gives you `hugo` on `PATH`, pinned via `hugoVersion` in `flake.nix`.10+Gives you `hugo` on `PATH`, pinned in `flake.nix`.
11+
10 12
11 ## Building13 ## Building
12 14
@@ -15,3 +17,12 @@ nix build
15 ```17 ```
16 18
17 Builds the site with Hugo and outputs it to `./result`.19 Builds the site with Hugo and outputs it to `./result`.
20+
21+
22+## Design
23+
24+The typography, color palette, and layout (`blog/assets/css/style.scss`,
25+`blog/layouts/`) are ported from [fzakaria.com](https://github.com/fzakaria/fzakaria.com)
26+(MIT-licensed), reimplemented as Hugo templates. The Newsreader and
27+JetBrains Mono font files under `blog/static/fonts/` are open source (SIL OFL
28+/ Apache 2.0 respectively).
added blog/assets/css/style.scss +1512 -0
new file mode 100644
@@ -0,0 +1,1512 @@
1+// ---------------------------------------------------------------------------
2+// mykiwi.blog — core design ported from fzakaria.com (MIT-licensed).
3+// Prose in Newsreader, structural chrome in JetBrains Mono, warm paper/ink
4+// palette with a dark-mode swap. Personal-only sections (bio colophon,
5+// readership/talks pages, WordPress archive) were left out.
6+// ---------------------------------------------------------------------------
7+
8+@use "sass:list";
9+
10+$wide: 67.5625rem;
11+
12+%chrome {
13+ font-family: var(--mono);
14+ font-size: 0.72rem;
15+ font-weight: 500;
16+ letter-spacing: 0.11em;
17+ text-transform: uppercase;
18+ font-variant-numeric: tabular-nums;
19+ color: var(--ink-faint);
20+}
21+
22+// --- Fonts -----------------------------------------------------------------
23+
24+@font-face {
25+ font-family: "Newsreader";
26+ font-style: normal;
27+ font-weight: 400 700;
28+ font-display: swap;
29+ src: url("/fonts/Newsreader-latin.woff2") format("woff2");
30+ unicode-range:
31+ u+0000-00ff, u+0131, u+0152-0153, u+02bb-02bc, u+02c6, u+02da, u+02dc,
32+ u+0304, u+0308, u+0329, u+2000-206f, u+20ac, u+2122, u+2191, u+2193, u+2212,
33+ u+2215, u+feff, u+fffd;
34+}
35+
36+@font-face {
37+ font-family: "Newsreader";
38+ font-style: normal;
39+ font-weight: 400 700;
40+ font-display: swap;
41+ src: url("/fonts/Newsreader-latin-ext.woff2") format("woff2");
42+ unicode-range:
43+ u+0100-02ba, u+02bd-02c5, u+02c7-02cc, u+02ce-02d7, u+02dd-02ff, u+0304,
44+ u+0308, u+0329, u+1d00-1dbf, u+1e00-1e9f, u+1ef2-1eff, u+2020, u+20a0-20ab,
45+ u+20ad-20c0, u+2113, u+2c60-2c7f, u+a720-a7ff;
46+}
47+
48+@font-face {
49+ font-family: "Newsreader";
50+ font-style: italic;
51+ font-weight: 400 700;
52+ font-display: swap;
53+ src: url("/fonts/Newsreader-italic-latin.woff2") format("woff2");
54+ unicode-range:
55+ u+0000-00ff, u+0131, u+0152-0153, u+02bb-02bc, u+02c6, u+02da, u+02dc,
56+ u+0304, u+0308, u+0329, u+2000-206f, u+20ac, u+2122, u+2191, u+2193, u+2212,
57+ u+2215, u+feff, u+fffd;
58+}
59+
60+@font-face {
61+ font-family: "Newsreader";
62+ font-style: italic;
63+ font-weight: 400 700;
64+ font-display: swap;
65+ src: url("/fonts/Newsreader-italic-latin-ext.woff2") format("woff2");
66+ unicode-range:
67+ u+0100-02ba, u+02bd-02c5, u+02c7-02cc, u+02ce-02d7, u+02dd-02ff, u+0304,
68+ u+0308, u+0329, u+1d00-1dbf, u+1e00-1e9f, u+1ef2-1eff, u+2020, u+20a0-20ab,
69+ u+20ad-20c0, u+2113, u+2c60-2c7f, u+a720-a7ff;
70+}
71+
72+@font-face {
73+ font-family: "JetBrains Mono";
74+ font-style: normal;
75+ font-weight: 400;
76+ font-display: swap;
77+ src: url("/fonts/JetBrainsMono-Regular.woff2") format("woff2");
78+}
79+
80+// --- Tokens ------------------------------------------------------------
81+
82+:root {
83+ --serif:
84+ "Newsreader", "Iowan Old Style", "Palatino Linotype", Palatino, Georgia,
85+ "Times New Roman", serif;
86+ --mono:
87+ "JetBrains Mono", ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo,
88+ Consolas, "Liberation Mono", monospace;
89+
90+ --paper: #f5f2ec;
91+ --paper-raised: #ebe6da;
92+ --ink: #1a1815;
93+ --ink-muted: #565046;
94+ --ink-faint: #6f685b;
95+ --rule: #dcd5c5;
96+ --rule-strong: #c2b9a6;
97+ --accent: #9e3413;
98+ --accent-wash: #9e34131f;
99+ --glow: #fffdf6;
100+ --image-mat: #ebe6da;
101+
102+ --measure: 36rem;
103+ --gutter: 0rem;
104+ --gap: 0rem;
105+ --margin-col: 0rem;
106+ --pad: 1.5rem;
107+
108+ --root-max: 19.5px;
109+ --step: 1.6rem;
110+
111+ color-scheme: light dark;
112+}
113+
114+@media (prefers-color-scheme: dark) {
115+ :root {
116+ --paper: #15171a;
117+ --paper-raised: #1d2025;
118+ --ink: #ddd8cd;
119+ --ink-muted: #9b958a;
120+ --ink-faint: #8a8377;
121+ --rule: #2b2f35;
122+ --rule-strong: #3f454d;
123+ --accent: #e08a45;
124+ --accent-wash: #e08a4526;
125+ --glow: #1f242b;
126+ }
127+}
128+
129+@media (min-width: $wide) {
130+ :root {
131+ --gutter: 4.5rem;
132+ --gap: 2rem;
133+ --margin-col: 13.5rem;
134+ }
135+}
136+
137+@media (min-width: 87.5rem) {
138+ :root {
139+ --gutter: 6rem;
140+ --gap: 2.5rem;
141+ --margin-col: 16rem;
142+ }
143+}
144+
145+@media (max-width: 34rem) {
146+ :root {
147+ --pad: 1.15rem;
148+ --step: 1.35rem;
149+ }
150+}
151+
152+// --- Reset -----------------------------------------------------------------
153+
154+*,
155+*::before,
156+*::after {
157+ box-sizing: border-box;
158+}
159+
160+body,
161+h1,
162+h2,
163+h3,
164+h4,
165+h5,
166+h6,
167+p,
168+ul,
169+ol,
170+dl,
171+dd,
172+figure,
173+blockquote,
174+pre {
175+ margin: 0;
176+}
177+
178+html {
179+ font-size: clamp(17px, 0.95rem + 0.2vw, var(--root-max));
180+ -webkit-text-size-adjust: 100%;
181+ scroll-behavior: smooth;
182+}
183+
184+@media (prefers-reduced-motion: reduce) {
185+ html {
186+ scroll-behavior: auto;
187+ }
188+}
189+
190+body {
191+ font-family: var(--serif);
192+ font-weight: 400;
193+ font-size: 1rem;
194+ line-height: 1.62;
195+ color: var(--ink);
196+ background-color: var(--paper);
197+ background-image: radial-gradient(
198+ 75rem 32rem at 14% -6%,
199+ var(--glow) 0%,
200+ transparent 68%
201+ );
202+ background-repeat: no-repeat;
203+ text-rendering: optimizeLegibility;
204+ -webkit-font-smoothing: antialiased;
205+ min-height: 100vh;
206+ overflow-wrap: break-word;
207+}
208+
209+::selection {
210+ background: var(--accent);
211+ color: var(--paper);
212+}
213+
214+// --- Layout ------------------------------------------------------------
215+
216+.wrap {
217+ width: 100%;
218+ max-width: calc(
219+ var(--gutter) + var(--measure) + var(--gap) + var(--margin-col) + 2 *
220+ var(--pad)
221+ );
222+ margin-inline: auto;
223+ padding-inline: var(--pad);
224+ padding-bottom: calc(var(--step) * 2);
225+}
226+
227+.column {
228+ max-width: var(--measure);
229+ margin-left: var(--gutter);
230+}
231+
232+.skip-link {
233+ position: absolute;
234+ left: -9999px;
235+ top: 0;
236+ padding: 0.5rem 0.9rem;
237+ background: var(--accent);
238+ color: var(--paper);
239+ font-family: var(--mono);
240+ font-size: 0.8rem;
241+ z-index: 10;
242+
243+ &:focus {
244+ left: 0.5rem;
245+ top: 0.5rem;
246+ }
247+}
248+
249+:focus-visible {
250+ outline: 2px solid var(--accent);
251+ outline-offset: 3px;
252+ border-radius: 1px;
253+}
254+
255+.visually-hidden {
256+ position: absolute;
257+ width: 1px;
258+ height: 1px;
259+ margin: -1px;
260+ padding: 0;
261+ overflow: hidden;
262+ clip-path: inset(50%);
263+ white-space: nowrap;
264+ border: 0;
265+}
266+
267+// --- Masthead ----------------------------------------------------------
268+
269+.masthead {
270+ padding-top: calc(var(--step) * 1.6);
271+ padding-bottom: var(--step);
272+ margin-bottom: calc(var(--step) * 1.4);
273+ position: relative;
274+ display: flex;
275+ align-items: baseline;
276+ justify-content: space-between;
277+ gap: 0 1rem;
278+
279+ &__signal {
280+ position: absolute;
281+ left: 0;
282+ right: 0;
283+ bottom: 0;
284+ height: 26px;
285+ -webkit-mask-image: linear-gradient(
286+ to right,
287+ #000 0%,
288+ #000 62%,
289+ transparent 96%
290+ );
291+ mask-image: linear-gradient(to right, #000 0%, #000 62%, transparent 96%);
292+
293+ svg {
294+ display: block;
295+ width: 100%;
296+ height: 100%;
297+ }
298+
299+ path {
300+ fill: none;
301+ stroke: url("#signal-ramp");
302+ stroke-width: 1.5;
303+ stroke-linecap: round;
304+ stroke-linejoin: round;
305+ vector-effect: non-scaling-stroke;
306+ }
307+ }
308+
309+ .signal-ramp__lead {
310+ stop-color: var(--accent);
311+ }
312+
313+ .signal-ramp__body,
314+ .signal-ramp__tail {
315+ stop-color: var(--rule-strong);
316+ }
317+}
318+
319+.masthead__title {
320+ font-size: 1.5rem;
321+ font-weight: 600;
322+ line-height: 1.1;
323+ letter-spacing: -0.018em;
324+
325+ a {
326+ color: inherit;
327+ text-decoration: none;
328+ }
329+
330+ a:hover {
331+ color: var(--accent);
332+ }
333+}
334+
335+// Signpost to the colophon, shown only at widths where the colophon is
336+// reordered below the post list and nothing on first screen hints it is
337+// there. Turned on alongside that reorder, further down.
338+.masthead__about {
339+ @extend %chrome;
340+ display: none;
341+ flex: none;
342+ color: var(--ink-muted);
343+ text-decoration: none;
344+ border-bottom: 1px solid var(--rule);
345+
346+ &:hover {
347+ color: var(--accent);
348+ border-bottom-color: var(--accent);
349+ }
350+}
351+
352+// --- Prose ---------------------------------------------------------------
353+
354+.prose {
355+ > * + * {
356+ margin-top: var(--step);
357+ }
358+
359+ p,
360+ ul,
361+ ol,
362+ dl {
363+ line-height: 1.65;
364+ }
365+
366+ div > * + *,
367+ details > * + *,
368+ li > * + *,
369+ td > * + *,
370+ th > * + * {
371+ margin-top: 0.7rem;
372+ }
373+
374+ h1,
375+ h2,
376+ h3,
377+ h4,
378+ h5,
379+ h6 {
380+ font-weight: 600;
381+ line-height: 1.2;
382+ letter-spacing: -0.012em;
383+ margin-bottom: 0;
384+ position: relative;
385+ scroll-margin-top: 1.5rem;
386+ }
387+
388+ :is(h1, h2, h3, h4, h5, h6) + :not(h1, h2, h3, h4, h5, h6) {
389+ margin-top: 0.55rem;
390+ }
391+
392+ :is(h1, h2, h3, h4, h5, h6) + :is(h1, h2, h3, h4, h5, h6) {
393+ margin-top: calc(var(--step) * 1.1);
394+ }
395+
396+ h1 {
397+ font-size: 1.62rem;
398+ margin-top: calc(var(--step) * 2);
399+ }
400+ h2 {
401+ font-size: 1.4rem;
402+ margin-top: calc(var(--step) * 1.7);
403+ }
404+ h3 {
405+ font-size: 1.18rem;
406+ margin-top: calc(var(--step) * 1.35);
407+ }
408+ h4,
409+ h5,
410+ h6 {
411+ font-size: 1rem;
412+ font-family: var(--mono);
413+ font-weight: 600;
414+ letter-spacing: 0.02em;
415+ margin-top: calc(var(--step) * 1.1);
416+ }
417+
418+ .hanchor {
419+ position: absolute;
420+ right: calc(100% + 0.9rem);
421+ top: 0.06em;
422+ font-family: var(--mono);
423+ font-size: 0.78em;
424+ font-weight: 400;
425+ color: var(--accent);
426+ opacity: 0.4;
427+ text-decoration: none;
428+ transition: opacity 0.15s ease;
429+
430+ &:hover,
431+ &:focus-visible {
432+ opacity: 1;
433+ }
434+ }
435+
436+ @media (max-width: $wide) {
437+ .hanchor {
438+ position: static;
439+ margin-right: 0.4em;
440+ font-size: 0.7em;
441+ vertical-align: 0.1em;
442+ }
443+ }
444+
445+ ul,
446+ ol {
447+ padding-left: 1.35rem;
448+ }
449+
450+ li + li {
451+ margin-top: 0.35rem;
452+ }
453+
454+ li > ul,
455+ li > ol {
456+ margin-top: 0.35rem;
457+ }
458+
459+ ul {
460+ list-style: none;
461+ padding-left: 1.15rem;
462+ }
463+
464+ ul > li {
465+ position: relative;
466+
467+ &::before {
468+ content: "";
469+ position: absolute;
470+ left: -1.05rem;
471+ top: 0.66em;
472+ width: 0.38rem;
473+ height: 1px;
474+ background: var(--accent);
475+ opacity: 0.75;
476+ }
477+ }
478+
479+ ul ul > li::before {
480+ background: var(--rule-strong);
481+ }
482+
483+ ol {
484+ list-style: none;
485+ counter-reset: ol;
486+ padding-left: 1.85rem;
487+ }
488+
489+ ol > li {
490+ counter-increment: ol;
491+ position: relative;
492+
493+ &::before {
494+ content: counter(ol) ".";
495+ position: absolute;
496+ right: calc(100% + 0.5rem);
497+ top: 0.06em;
498+ font-family: var(--mono);
499+ font-size: 0.75em;
500+ color: var(--ink-faint);
501+ font-variant-numeric: tabular-nums;
502+ }
503+ }
504+
505+ dt {
506+ font-weight: 600;
507+ }
508+
509+ dd {
510+ margin-left: 1.35rem;
511+ }
512+
513+ strong,
514+ b {
515+ font-weight: 700;
516+ }
517+
518+ hr {
519+ border: 0;
520+ height: 1px;
521+ background: linear-gradient(
522+ to right,
523+ var(--rule-strong),
524+ var(--rule) 45%,
525+ transparent
526+ );
527+ margin-block: calc(var(--step) * 1.6);
528+ }
529+
530+ sup,
531+ sub {
532+ line-height: 0;
533+ font-size: 0.7em;
534+ }
535+}
536+
537+a {
538+ color: inherit;
539+ text-decoration: underline;
540+ text-decoration-color: var(--accent);
541+ text-decoration-thickness: 1px;
542+ text-underline-offset: 0.17em;
543+ transition:
544+ color 0.15s ease,
545+ text-decoration-color 0.15s ease;
546+
547+ &:hover {
548+ color: var(--accent);
549+ text-decoration-thickness: 2px;
550+ }
551+}
552+
553+// --- Blockquote --------------------------------------------------------
554+
555+blockquote {
556+ padding-left: 1.15rem;
557+ border-left: 2px solid var(--rule-strong);
558+ color: var(--ink-muted);
559+
560+ > * + * {
561+ margin-top: 0.7rem;
562+ }
563+}
564+
565+// --- Code ----------------------------------------------------------------
566+
567+code,
568+kbd,
569+samp {
570+ font-family: var(--mono);
571+ font-variant-ligatures: none;
572+ font-feature-settings:
573+ "liga" 0,
574+ "calt" 0;
575+}
576+
577+:not(pre) > code {
578+ font-size: 0.82em;
579+ background: var(--paper-raised);
580+ border: 1px solid var(--rule);
581+ border-radius: 3px;
582+ padding: 0.08em 0.26em;
583+ color: var(--ink);
584+ white-space: break-spaces;
585+ word-break: break-word;
586+}
587+
588+a :not(pre) > code,
589+:not(pre) > code:has(a) {
590+ color: inherit;
591+}
592+
593+kbd {
594+ font-size: 0.78em;
595+ background: var(--paper);
596+ border: 1px solid var(--rule-strong);
597+ border-bottom-width: 2px;
598+ border-radius: 3px;
599+ padding: 0.08em 0.36em;
600+ color: var(--ink);
601+ white-space: nowrap;
602+}
603+
604+// Hugo/Chroma wraps a fenced code block in <div class="highlight"><pre
605+// class="chroma"><code class="language-x" data-lang="x">. The language tag
606+// is read off that data-lang attribute and floats over the block like a
607+// fieldset legend.
608+.highlight {
609+ position: relative;
610+
611+ code[data-lang]::before {
612+ @extend %chrome;
613+ content: attr(data-lang);
614+ position: absolute;
615+ z-index: 1;
616+ top: 0;
617+ right: 0.85rem;
618+ transform: translateY(-50%);
619+ padding-inline: 0.4rem;
620+ background: var(--paper);
621+ font-size: 0.6rem;
622+ line-height: 1;
623+ color: var(--ink-faint);
624+ pointer-events: none;
625+ }
626+}
627+
628+pre {
629+ font-family: var(--mono);
630+ font-size: 0.78rem;
631+ line-height: 1.55;
632+ background: var(--paper-raised);
633+ border: 1px solid var(--rule);
634+ border-left: 2px solid var(--rule-strong);
635+ border-radius: 3px;
636+ padding: 0.85rem 1rem;
637+ overflow-x: auto;
638+ tab-size: 4;
639+ color: var(--ink);
640+
641+ code {
642+ background: none;
643+ border: 0;
644+ padding: 0;
645+ font-size: inherit;
646+ color: inherit;
647+ }
648+}
649+
650+// --- Collapsible blocks --------------------------------------------------
651+
652+.prose details {
653+ border: 1px solid var(--rule);
654+ border-left: 2px solid var(--rule-strong);
655+ border-radius: 3px;
656+ padding: 0 0.9rem;
657+
658+ > summary {
659+ font-family: var(--mono);
660+ font-size: 0.78rem;
661+ color: var(--ink);
662+ padding: 0.6rem 0;
663+ cursor: pointer;
664+ list-style: none;
665+ display: flex;
666+ align-items: baseline;
667+ gap: 0.5rem;
668+
669+ &::-webkit-details-marker {
670+ display: none;
671+ }
672+
673+ &::before {
674+ content: "";
675+ width: 0;
676+ height: 0;
677+ border-left: 0.5rem solid var(--accent);
678+ border-top: 0.29rem solid transparent;
679+ border-bottom: 0.29rem solid transparent;
680+ align-self: center;
681+ transition: transform 0.15s ease;
682+ display: inline-block;
683+ }
684+
685+ &:hover {
686+ color: var(--accent);
687+ }
688+ }
689+
690+ &[open] {
691+ padding-bottom: 0.9rem;
692+
693+ > summary {
694+ margin-bottom: 0.9rem;
695+ border-bottom: 1px solid var(--rule);
696+ color: var(--accent);
697+
698+ &::before {
699+ transform: rotate(90deg);
700+ }
701+ }
702+ }
703+
704+ .highlight {
705+ margin-inline: -0.9rem;
706+ }
707+
708+ pre {
709+ border-inline: 0;
710+ border-radius: 0;
711+ }
712+}
713+
714+@media (prefers-reduced-motion: reduce) {
715+ .prose details > summary::before {
716+ transition: none;
717+ }
718+}
719+
720+// --- Tables ----------------------------------------------------------------
721+
722+.table-scroll {
723+ overflow-x: auto;
724+ max-width: 100%;
725+}
726+
727+table {
728+ border-collapse: collapse;
729+ width: 100%;
730+ font-size: 0.85rem;
731+ font-variant-numeric: tabular-nums;
732+
733+ th,
734+ td {
735+ padding: 0.45rem 0.7rem;
736+ text-align: left;
737+ border: 0;
738+ border-bottom: 1px solid var(--rule);
739+ vertical-align: top;
740+ }
741+
742+ th:first-child,
743+ td:first-child {
744+ padding-left: 0;
745+ }
746+
747+ th:last-child,
748+ td:last-child {
749+ padding-right: 0;
750+ }
751+
752+ th {
753+ @extend %chrome;
754+ color: var(--ink-muted);
755+ border-bottom: 1px solid var(--rule-strong);
756+ letter-spacing: 0.055em;
757+ line-height: 1.35;
758+ }
759+
760+ tbody tr:last-child td {
761+ border-bottom: 0;
762+ }
763+
764+ tbody tr:hover td {
765+ background: var(--accent-wash);
766+ }
767+}
768+
769+// --- Figures and media -----------------------------------------------------
770+
771+img,
772+video,
773+canvas,
774+svg {
775+ max-width: 100%;
776+ height: auto;
777+}
778+
779+.prose img,
780+.prose video {
781+ display: block;
782+ max-width: min(var(--image-width, 100%), 100%);
783+ margin-inline: auto;
784+ border: 1px solid var(--rule);
785+ border-radius: 3px;
786+ background: var(--image-mat);
787+}
788+
789+.prose a img {
790+ border-color: var(--rule-strong);
791+}
792+
793+iframe,
794+embed,
795+object {
796+ max-width: 100%;
797+ border: 0;
798+}
799+
800+iframe[src*="youtube"],
801+iframe[src*="vimeo"] {
802+ display: block;
803+ width: 100%;
804+ aspect-ratio: 16 / 9;
805+ height: auto;
806+ margin-inline: auto;
807+ border: 1px solid var(--rule);
808+ border-radius: 3px;
809+ background: var(--image-mat);
810+}
811+
812+// Goldmark's own footnote list, styled plainly — the margin-sidenote
813+// treatment from fzakaria.com needs a custom relocation pass this port
814+// does not carry.
815+.footnotes {
816+ margin-top: calc(var(--step) * 2);
817+ padding-top: var(--step);
818+ border-top: 1px solid var(--rule);
819+ font-size: 0.85rem;
820+ color: var(--ink-muted);
821+}
822+
823+// --- Alerts ------------------------------------------------------------
824+
825+$alerts: (
826+ "note": (
827+ #0b5fa5,
828+ #6fb2e8,
829+ ),
830+ "tip": (
831+ #1a7f37,
832+ #79c98d,
833+ ),
834+ "important": (
835+ #7145c4,
836+ #b79ded,
837+ ),
838+ "warning": (
839+ #8a6100,
840+ #dfb051,
841+ ),
842+ "caution": (
843+ #b1201d,
844+ #ec8b7f,
845+ ),
846+);
847+
848+.alert {
849+ --alert: var(--rule-strong);
850+ border-left: 2px solid var(--alert);
851+ background: var(--paper-raised);
852+ padding: 0.85rem 1.1rem;
853+ border-radius: 0 3px 3px 0;
854+ color: var(--ink);
855+
856+ > * + * {
857+ margin-top: 0.65rem;
858+ }
859+
860+ p:first-child > strong:first-child {
861+ @extend %chrome;
862+ display: block;
863+ color: var(--alert);
864+ margin-bottom: 0.3rem;
865+ }
866+
867+ :not(pre) > code {
868+ background: transparent;
869+ border-color: transparent;
870+ padding: 0;
871+ }
872+
873+ @each $name, $pair in $alerts {
874+ &.alert-#{$name} {
875+ --alert: #{list.nth($pair, 1)};
876+
877+ @media (prefers-color-scheme: dark) {
878+ --alert: #{list.nth($pair, 2)};
879+ }
880+ }
881+ }
882+}
883+
884+blockquote.alert {
885+ border-left-color: var(--alert);
886+ color: var(--ink);
887+}
888+
889+// --- Post header -----------------------------------------------------------
890+
891+.post-header {
892+ margin-bottom: calc(var(--step) * 1.5);
893+}
894+
895+.post-kicker {
896+ @extend %chrome;
897+ display: flex;
898+ flex-wrap: wrap;
899+ gap: 0 0.5rem;
900+ margin-bottom: 0.7rem;
901+
902+ .sep {
903+ color: var(--rule-strong);
904+ }
905+
906+ a {
907+ color: inherit;
908+ text-decoration: none;
909+ border-bottom: 1px solid var(--rule-strong);
910+
911+ &:hover {
912+ color: var(--accent);
913+ border-bottom-color: var(--accent);
914+ }
915+ }
916+}
917+
918+.post-title {
919+ font-size: clamp(1.85rem, 1.3rem + 2.1vw, 2.85rem);
920+ font-weight: 600;
921+ line-height: 1.08;
922+ letter-spacing: -0.025em;
923+ text-wrap: balance;
924+}
925+
926+.post-rule {
927+ width: 3.5rem;
928+ height: 2px;
929+ background: var(--accent);
930+ margin: 1.1rem auto 0 0;
931+ border: 0;
932+}
933+
934+.post-body {
935+ margin-top: calc(var(--step) * 1.5);
936+}
937+
938+// --- Site footer -------------------------------------------------------
939+
940+.site-foot {
941+ @extend %chrome;
942+ margin-top: calc(var(--step) * 2.5);
943+ padding-top: var(--step);
944+ border-top: 1px solid var(--rule);
945+ text-transform: none;
946+ letter-spacing: 0.02em;
947+ font-size: 0.7rem;
948+ line-height: 1.75;
949+ color: var(--ink-faint);
950+ display: flex;
951+ flex-wrap: wrap;
952+ justify-content: center;
953+ text-align: center;
954+ gap: 0 0.5rem;
955+
956+ a {
957+ color: var(--ink-muted);
958+ text-decoration: none;
959+ border-bottom: 1px solid var(--rule);
960+
961+ &:hover {
962+ color: var(--accent);
963+ border-bottom-color: var(--accent);
964+ }
965+ }
966+
967+ .sep {
968+ color: var(--rule-strong);
969+ }
970+
971+ .rss {
972+ display: inline-flex;
973+ align-items: center;
974+ gap: 0.3rem;
975+ border-bottom: 0;
976+
977+ svg {
978+ width: 0.85em;
979+ height: 0.85em;
980+ fill: currentColor;
981+ }
982+ }
983+
984+ // Own row, so the path gets the whole measure rather than whatever is left
985+ // over beside the RSS link.
986+ .store-path-row {
987+ flex-basis: 100%;
988+ }
989+
990+ // A store path is a single 56-character token: broken across lines it is
991+ // unreadable and awkward to select. Hold it on one line and let it shrink
992+ // instead.
993+ .store-path {
994+ font-size: min(0.7rem, 2.3vw);
995+ letter-spacing: 0;
996+ white-space: nowrap;
997+ color: inherit;
998+ background: none;
999+ border: 0;
1000+ padding: 0;
1001+ }
1002+}
1003+
1004+// --- Index ---------------------------------------------------------------
1005+
1006+.page-index {
1007+ display: flow-root;
1008+}
1009+
1010+// Bio card. Floats into the margin column beside the post list; below $wide
1011+// there is no margin column to float into, so it drops below the list
1012+// instead (further down).
1013+.colophon {
1014+ float: right;
1015+ width: var(--margin-col);
1016+ margin-right: calc((var(--gap) + var(--margin-col)) * -1);
1017+ margin-bottom: var(--step);
1018+ // Jumped to from the masthead; leave air above it on arrival.
1019+ scroll-margin-top: var(--step);
1020+ font-size: 0.8rem;
1021+ line-height: 1.55;
1022+ color: var(--ink-muted);
1023+}
1024+
1025+.colophon__avatar {
1026+ width: 100%;
1027+ max-width: 8.5rem;
1028+ border-radius: 3px;
1029+ border: 1px solid var(--rule-strong);
1030+ display: block;
1031+ margin-bottom: 0.85rem;
1032+ filter: grayscale(0.35);
1033+ transition: filter 0.3s ease;
1034+
1035+ &:hover {
1036+ filter: grayscale(0);
1037+ }
1038+}
1039+
1040+.colophon__name {
1041+ font-weight: 600;
1042+ font-size: 0.95rem;
1043+ line-height: 1.3;
1044+ color: var(--ink);
1045+ margin-bottom: 0.35rem;
1046+
1047+ &-last {
1048+ font-weight: 500;
1049+ color: var(--ink-faint);
1050+ }
1051+}
1052+
1053+.colophon__bio {
1054+ margin-bottom: 1rem;
1055+
1056+ a {
1057+ text-decoration-color: var(--rule-strong);
1058+ }
1059+}
1060+
1061+.colophon__links {
1062+ list-style: none;
1063+ margin: 0;
1064+ padding: 0;
1065+ border-top: 1px solid var(--rule);
1066+
1067+ // The key sits above its value rather than beside it: a two-column split
1068+ // leaves too little room for an email address in monospace without
1069+ // breaking it across lines.
1070+ li {
1071+ padding-block: 0.4rem;
1072+ border-bottom: 1px solid var(--rule);
1073+ }
1074+
1075+ .key {
1076+ @extend %chrome;
1077+ display: block;
1078+ font-size: 0.6rem;
1079+ margin-bottom: 0.05rem;
1080+ }
1081+
1082+ .val {
1083+ display: block;
1084+ font-family: var(--mono);
1085+ font-size: 0.68rem;
1086+
1087+ a {
1088+ color: var(--ink-muted);
1089+ text-decoration: none;
1090+ border-bottom: 1px solid var(--rule-strong);
1091+
1092+ &:hover {
1093+ color: var(--accent);
1094+ border-bottom-color: var(--accent);
1095+ }
1096+ }
1097+ }
1098+}
1099+
1100+// Narrow: the float is dropped and the card moves below the article list.
1101+@media (max-width: $wide) {
1102+ .masthead__about {
1103+ display: inline-block;
1104+ }
1105+
1106+ .page-index {
1107+ display: flex;
1108+ flex-direction: column;
1109+ }
1110+
1111+ .colophon {
1112+ float: none;
1113+ width: 100%;
1114+ margin-right: 0;
1115+ margin-bottom: 0;
1116+ margin-top: calc(var(--step) * 2);
1117+ padding-top: var(--step);
1118+ border-top: 1px solid var(--rule);
1119+ order: 3;
1120+ display: grid;
1121+ grid-template-columns: 8.5rem 1fr;
1122+ gap: 0 1.25rem;
1123+ align-items: start;
1124+ }
1125+
1126+ .colophon__avatar {
1127+ margin-bottom: 0;
1128+ grid-row: span 2;
1129+ }
1130+
1131+ .index-body {
1132+ order: 2;
1133+ }
1134+
1135+ @media (max-width: 30rem) {
1136+ .colophon {
1137+ grid-template-columns: 1fr;
1138+ gap: 1rem;
1139+ }
1140+
1141+ .colophon__avatar {
1142+ max-width: 6.5rem;
1143+ }
1144+ }
1145+}
1146+
1147+.index-lead {
1148+ display: block;
1149+ text-decoration: none;
1150+ margin-bottom: calc(var(--step) * 2);
1151+ padding-left: 1rem;
1152+ border-left: 2px solid var(--accent);
1153+ position: relative;
1154+
1155+ .label {
1156+ @extend %chrome;
1157+ color: var(--accent);
1158+ display: block;
1159+ margin-bottom: 0.4rem;
1160+ }
1161+
1162+ .title {
1163+ font-size: 1.5rem;
1164+ font-weight: 600;
1165+ line-height: 1.15;
1166+ letter-spacing: -0.02em;
1167+ text-wrap: balance;
1168+ display: block;
1169+ color: inherit;
1170+ text-decoration: none;
1171+
1172+ &::after {
1173+ content: "";
1174+ position: absolute;
1175+ inset: 0;
1176+ }
1177+ }
1178+
1179+ .excerpt a {
1180+ position: relative;
1181+ z-index: 1;
1182+ }
1183+
1184+ .excerpt {
1185+ display: block;
1186+ color: var(--ink-muted);
1187+ font-size: 0.92rem;
1188+ margin-top: 0.5rem;
1189+ }
1190+
1191+ .more {
1192+ @extend %chrome;
1193+ color: var(--accent);
1194+ display: block;
1195+ margin-top: 0.7rem;
1196+ }
1197+
1198+ &:hover .title {
1199+ color: var(--accent);
1200+ }
1201+}
1202+
1203+.year {
1204+ margin-top: calc(var(--step) * 2);
1205+ position: relative;
1206+
1207+ &:first-of-type {
1208+ margin-top: 0;
1209+ }
1210+}
1211+
1212+.year__label {
1213+ @extend %chrome;
1214+ font-size: 0.78rem;
1215+ color: var(--ink-faint);
1216+ padding-bottom: 0.4rem;
1217+ margin-bottom: 0.6rem;
1218+ border-bottom: 1px solid var(--rule);
1219+ display: flex;
1220+ justify-content: space-between;
1221+ align-items: baseline;
1222+
1223+ .count {
1224+ font-size: 0.66rem;
1225+ color: var(--rule-strong);
1226+ }
1227+}
1228+
1229+@media (min-width: $wide) {
1230+ .year__label {
1231+ position: absolute;
1232+ right: calc(100% + 0.9rem);
1233+ top: 0.1rem;
1234+ width: calc(var(--gutter) - 0.9rem);
1235+ display: block;
1236+ border-bottom: 0;
1237+ padding-bottom: 0;
1238+ margin-bottom: 0;
1239+ text-align: right;
1240+ letter-spacing: 0.04em;
1241+
1242+ .count {
1243+ display: none;
1244+ }
1245+ }
1246+
1247+ .year__list {
1248+ border-top: 1px solid var(--rule);
1249+ padding-top: 0.35rem;
1250+ }
1251+}
1252+
1253+.year__list {
1254+ list-style: none;
1255+ margin: 0;
1256+ padding: 0;
1257+}
1258+
1259+.entry {
1260+ display: grid;
1261+ grid-template-columns: 4.25rem 1fr;
1262+ gap: 0 0.85rem;
1263+ align-items: baseline;
1264+ padding-block: 0.42rem;
1265+
1266+ @media (max-width: 34rem) {
1267+ grid-template-columns: 1fr;
1268+ gap: 0.12rem;
1269+ padding-block: 0.5rem;
1270+ }
1271+
1272+ + .entry {
1273+ border-top: 1px solid var(--rule);
1274+ }
1275+
1276+ .date {
1277+ @extend %chrome;
1278+ font-size: 0.68rem;
1279+ white-space: nowrap;
1280+ }
1281+
1282+ a {
1283+ text-decoration: none;
1284+ line-height: 1.35;
1285+ color: var(--ink);
1286+ text-decoration: underline;
1287+ text-decoration-color: transparent;
1288+ text-underline-offset: 0.17em;
1289+ }
1290+
1291+ &:hover a {
1292+ color: var(--accent);
1293+ text-decoration-color: var(--accent);
1294+ }
1295+
1296+ &:hover .date {
1297+ color: var(--accent);
1298+ }
1299+}
1300+
1301+// --- Motion --------------------------------------------------------------
1302+
1303+@media (prefers-reduced-motion: no-preference) {
1304+ @keyframes rise {
1305+ from {
1306+ opacity: 0;
1307+ transform: translateY(0.5rem);
1308+ }
1309+ to {
1310+ opacity: 1;
1311+ transform: none;
1312+ }
1313+ }
1314+
1315+ .rise {
1316+ animation: rise 0.5s cubic-bezier(0.22, 0.68, 0.35, 1) backwards;
1317+ animation-delay: calc(var(--i, 0) * 60ms);
1318+ }
1319+
1320+ @keyframes signal-sweep {
1321+ to {
1322+ stroke-dashoffset: 0;
1323+ }
1324+ }
1325+
1326+ .masthead__signal path {
1327+ stroke-dasharray: 100;
1328+ stroke-dashoffset: 100;
1329+ animation: signal-sweep 1.1s cubic-bezier(0.32, 0.5, 0.3, 1) 0.18s forwards;
1330+ }
1331+}
1332+
1333+// --- Print -----------------------------------------------------------------
1334+
1335+@media print {
1336+ :root {
1337+ --paper: #fff;
1338+ --paper-raised: #fff;
1339+ --ink: #000;
1340+ --ink-muted: #333;
1341+ --ink-faint: #555;
1342+ --rule: #bbb;
1343+ --rule-strong: #888;
1344+ --accent: #000;
1345+ --gutter: 0rem;
1346+ --margin-col: 0rem;
1347+ --gap: 0rem;
1348+ }
1349+
1350+ body {
1351+ background: #fff;
1352+ }
1353+
1354+ .colophon,
1355+ .skip-link {
1356+ display: none;
1357+ }
1358+
1359+ details {
1360+ border: 0;
1361+ padding: 0;
1362+ }
1363+
1364+ details > summary::before {
1365+ content: none;
1366+ }
1367+
1368+ pre,
1369+ blockquote,
1370+ details,
1371+ .alert {
1372+ break-inside: avoid;
1373+ }
1374+
1375+ a::after {
1376+ content: " (" attr(href) ")";
1377+ font-size: 0.75em;
1378+ color: #555;
1379+ }
1380+
1381+ a[href^="#"]::after {
1382+ content: "";
1383+ }
1384+}
1385+
1386+// ---------------------------------------------------------------------------
1387+// Rouge/Chroma syntax theme — "Ochre" (unchanged from fzakaria.com).
1388+// ---------------------------------------------------------------------------
1389+
1390+:root {
1391+ --syn-comment: #6f685b;
1392+ --syn-keyword: #9a3d05;
1393+ --syn-string: #3f6b32;
1394+ --syn-number: #6b4fa8;
1395+ --syn-func: #1c5570;
1396+ --syn-type: #7a6114;
1397+ --syn-var: #7c4a2d;
1398+ --syn-punct: #6b6558;
1399+ --syn-error: #b1201d;
1400+ --syn-error-bg: #b1201d1a;
1401+ --syn-ins-bg: #1a7f3714;
1402+ --syn-del-bg: #b1201d14;
1403+ --syn-ins-fg: #1a6d33;
1404+ --syn-del-fg: #a3231f;
1405+}
1406+
1407+@media (prefers-color-scheme: dark) {
1408+ :root {
1409+ --syn-comment: #8d867a;
1410+ --syn-keyword: #e08a45;
1411+ --syn-string: #a6c98d;
1412+ --syn-number: #c3a2ec;
1413+ --syn-func: #7fb6d6;
1414+ --syn-type: #ddc477;
1415+ --syn-var: #dda58c;
1416+ --syn-punct: #8b857a;
1417+ --syn-error: #ec8b7f;
1418+ --syn-error-bg: #ec8b7f1f;
1419+ --syn-ins-bg: #79c98d1a;
1420+ --syn-del-bg: #ec8b7f1a;
1421+ --syn-ins-fg: #8fd0a1;
1422+ --syn-del-fg: #ef9a8f;
1423+ }
1424+}
1425+
1426+.highlight {
1427+ .c, .ch, .cd, .cm, .cp, .cpf, .c1, .cs {
1428+ color: var(--syn-comment);
1429+ font-style: italic;
1430+ }
1431+
1432+ .k, .kc, .kd, .kn, .kp, .kr, .kv {
1433+ color: var(--syn-keyword);
1434+ }
1435+
1436+ .kt, .nc, .nn, .no, .ne {
1437+ color: var(--syn-type);
1438+ }
1439+
1440+ .nf, .fm, .nb, .bp, .na, .nt {
1441+ color: var(--syn-func);
1442+ }
1443+
1444+ .nv, .vc, .vg, .vi, .vm, .nl, .py {
1445+ color: var(--syn-var);
1446+ }
1447+
1448+ .s, .sa, .sb, .sc, .dl, .sd, .s2, .se, .sh, .si, .sx, .s1, .ss, .sr {
1449+ color: var(--syn-string);
1450+ }
1451+
1452+ .m, .mb, .mf, .mh, .mi, .mo, .il {
1453+ color: var(--syn-number);
1454+ }
1455+
1456+ .o, .ow, .p, .pi, .ni {
1457+ color: var(--syn-punct);
1458+ }
1459+
1460+ .err {
1461+ color: var(--syn-error);
1462+ background-color: var(--syn-error-bg);
1463+ border-radius: 2px;
1464+ }
1465+
1466+ .gd {
1467+ color: var(--syn-del-fg);
1468+ background-color: var(--syn-del-bg);
1469+ }
1470+
1471+ .gi {
1472+ color: var(--syn-ins-fg);
1473+ background-color: var(--syn-ins-bg);
1474+ }
1475+
1476+ .gh, .gu {
1477+ color: var(--syn-func);
1478+ font-weight: 600;
1479+ }
1480+
1481+ .gp {
1482+ color: var(--syn-comment);
1483+ user-select: none;
1484+ }
1485+
1486+ .go {
1487+ color: var(--ink-muted);
1488+ }
1489+
1490+ .ge {
1491+ font-style: italic;
1492+ }
1493+
1494+ .gs {
1495+ font-weight: 700;
1496+ }
1497+
1498+ .gr, .gt {
1499+ color: var(--syn-error);
1500+ }
1501+
1502+ .w {
1503+ color: var(--rule-strong);
1504+ }
1505+
1506+ .lineno, .gl {
1507+ color: var(--ink-faint);
1508+ opacity: 0.6;
1509+ user-select: none;
1510+ padding-right: 0.85rem;
1511+ }
1512+}
new file mode 100644
@@ -0,0 +1,1512 @@
1+// ---------------------------------------------------------------------------
2+// mykiwi.blog — core design ported from fzakaria.com (MIT-licensed).
3+// Prose in Newsreader, structural chrome in JetBrains Mono, warm paper/ink
4+// palette with a dark-mode swap. Personal-only sections (bio colophon,
5+// readership/talks pages, WordPress archive) were left out.
6+// ---------------------------------------------------------------------------
7+
8+@use "sass:list";
9+
10+$wide: 67.5625rem;
11+
12+%chrome {
13+ font-family: var(--mono);
14+ font-size: 0.72rem;
15+ font-weight: 500;
16+ letter-spacing: 0.11em;
17+ text-transform: uppercase;
18+ font-variant-numeric: tabular-nums;
19+ color: var(--ink-faint);
20+}
21+
22+// --- Fonts -----------------------------------------------------------------
23+
24+@font-face {
25+ font-family: "Newsreader";
26+ font-style: normal;
27+ font-weight: 400 700;
28+ font-display: swap;
29+ src: url("/fonts/Newsreader-latin.woff2") format("woff2");
30+ unicode-range:
31+ u+0000-00ff, u+0131, u+0152-0153, u+02bb-02bc, u+02c6, u+02da, u+02dc,
32+ u+0304, u+0308, u+0329, u+2000-206f, u+20ac, u+2122, u+2191, u+2193, u+2212,
33+ u+2215, u+feff, u+fffd;
34+}
35+
36+@font-face {
37+ font-family: "Newsreader";
38+ font-style: normal;
39+ font-weight: 400 700;
40+ font-display: swap;
41+ src: url("/fonts/Newsreader-latin-ext.woff2") format("woff2");
42+ unicode-range:
43+ u+0100-02ba, u+02bd-02c5, u+02c7-02cc, u+02ce-02d7, u+02dd-02ff, u+0304,
44+ u+0308, u+0329, u+1d00-1dbf, u+1e00-1e9f, u+1ef2-1eff, u+2020, u+20a0-20ab,
45+ u+20ad-20c0, u+2113, u+2c60-2c7f, u+a720-a7ff;
46+}
47+
48+@font-face {
49+ font-family: "Newsreader";
50+ font-style: italic;
51+ font-weight: 400 700;
52+ font-display: swap;
53+ src: url("/fonts/Newsreader-italic-latin.woff2") format("woff2");
54+ unicode-range:
55+ u+0000-00ff, u+0131, u+0152-0153, u+02bb-02bc, u+02c6, u+02da, u+02dc,
56+ u+0304, u+0308, u+0329, u+2000-206f, u+20ac, u+2122, u+2191, u+2193, u+2212,
57+ u+2215, u+feff, u+fffd;
58+}
59+
60+@font-face {
61+ font-family: "Newsreader";
62+ font-style: italic;
63+ font-weight: 400 700;
64+ font-display: swap;
65+ src: url("/fonts/Newsreader-italic-latin-ext.woff2") format("woff2");
66+ unicode-range:
67+ u+0100-02ba, u+02bd-02c5, u+02c7-02cc, u+02ce-02d7, u+02dd-02ff, u+0304,
68+ u+0308, u+0329, u+1d00-1dbf, u+1e00-1e9f, u+1ef2-1eff, u+2020, u+20a0-20ab,
69+ u+20ad-20c0, u+2113, u+2c60-2c7f, u+a720-a7ff;
70+}
71+
72+@font-face {
73+ font-family: "JetBrains Mono";
74+ font-style: normal;
75+ font-weight: 400;
76+ font-display: swap;
77+ src: url("/fonts/JetBrainsMono-Regular.woff2") format("woff2");
78+}
79+
80+// --- Tokens ------------------------------------------------------------
81+
82+:root {
83+ --serif:
84+ "Newsreader", "Iowan Old Style", "Palatino Linotype", Palatino, Georgia,
85+ "Times New Roman", serif;
86+ --mono:
87+ "JetBrains Mono", ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo,
88+ Consolas, "Liberation Mono", monospace;
89+
90+ --paper: #f5f2ec;
91+ --paper-raised: #ebe6da;
92+ --ink: #1a1815;
93+ --ink-muted: #565046;
94+ --ink-faint: #6f685b;
95+ --rule: #dcd5c5;
96+ --rule-strong: #c2b9a6;
97+ --accent: #9e3413;
98+ --accent-wash: #9e34131f;
99+ --glow: #fffdf6;
100+ --image-mat: #ebe6da;
101+
102+ --measure: 36rem;
103+ --gutter: 0rem;
104+ --gap: 0rem;
105+ --margin-col: 0rem;
106+ --pad: 1.5rem;
107+
108+ --root-max: 19.5px;
109+ --step: 1.6rem;
110+
111+ color-scheme: light dark;
112+}
113+
114+@media (prefers-color-scheme: dark) {
115+ :root {
116+ --paper: #15171a;
117+ --paper-raised: #1d2025;
118+ --ink: #ddd8cd;
119+ --ink-muted: #9b958a;
120+ --ink-faint: #8a8377;
121+ --rule: #2b2f35;
122+ --rule-strong: #3f454d;
123+ --accent: #e08a45;
124+ --accent-wash: #e08a4526;
125+ --glow: #1f242b;
126+ }
127+}
128+
129+@media (min-width: $wide) {
130+ :root {
131+ --gutter: 4.5rem;
132+ --gap: 2rem;
133+ --margin-col: 13.5rem;
134+ }
135+}
136+
137+@media (min-width: 87.5rem) {
138+ :root {
139+ --gutter: 6rem;
140+ --gap: 2.5rem;
141+ --margin-col: 16rem;
142+ }
143+}
144+
145+@media (max-width: 34rem) {
146+ :root {
147+ --pad: 1.15rem;
148+ --step: 1.35rem;
149+ }
150+}
151+
152+// --- Reset -----------------------------------------------------------------
153+
154+*,
155+*::before,
156+*::after {
157+ box-sizing: border-box;
158+}
159+
160+body,
161+h1,
162+h2,
163+h3,
164+h4,
165+h5,
166+h6,
167+p,
168+ul,
169+ol,
170+dl,
171+dd,
172+figure,
173+blockquote,
174+pre {
175+ margin: 0;
176+}
177+
178+html {
179+ font-size: clamp(17px, 0.95rem + 0.2vw, var(--root-max));
180+ -webkit-text-size-adjust: 100%;
181+ scroll-behavior: smooth;
182+}
183+
184+@media (prefers-reduced-motion: reduce) {
185+ html {
186+ scroll-behavior: auto;
187+ }
188+}
189+
190+body {
191+ font-family: var(--serif);
192+ font-weight: 400;
193+ font-size: 1rem;
194+ line-height: 1.62;
195+ color: var(--ink);
196+ background-color: var(--paper);
197+ background-image: radial-gradient(
198+ 75rem 32rem at 14% -6%,
199+ var(--glow) 0%,
200+ transparent 68%
201+ );
202+ background-repeat: no-repeat;
203+ text-rendering: optimizeLegibility;
204+ -webkit-font-smoothing: antialiased;
205+ min-height: 100vh;
206+ overflow-wrap: break-word;
207+}
208+
209+::selection {
210+ background: var(--accent);
211+ color: var(--paper);
212+}
213+
214+// --- Layout ------------------------------------------------------------
215+
216+.wrap {
217+ width: 100%;
218+ max-width: calc(
219+ var(--gutter) + var(--measure) + var(--gap) + var(--margin-col) + 2 *
220+ var(--pad)
221+ );
222+ margin-inline: auto;
223+ padding-inline: var(--pad);
224+ padding-bottom: calc(var(--step) * 2);
225+}
226+
227+.column {
228+ max-width: var(--measure);
229+ margin-left: var(--gutter);
230+}
231+
232+.skip-link {
233+ position: absolute;
234+ left: -9999px;
235+ top: 0;
236+ padding: 0.5rem 0.9rem;
237+ background: var(--accent);
238+ color: var(--paper);
239+ font-family: var(--mono);
240+ font-size: 0.8rem;
241+ z-index: 10;
242+
243+ &:focus {
244+ left: 0.5rem;
245+ top: 0.5rem;
246+ }
247+}
248+
249+:focus-visible {
250+ outline: 2px solid var(--accent);
251+ outline-offset: 3px;
252+ border-radius: 1px;
253+}
254+
255+.visually-hidden {
256+ position: absolute;
257+ width: 1px;
258+ height: 1px;
259+ margin: -1px;
260+ padding: 0;
261+ overflow: hidden;
262+ clip-path: inset(50%);
263+ white-space: nowrap;
264+ border: 0;
265+}
266+
267+// --- Masthead ----------------------------------------------------------
268+
269+.masthead {
270+ padding-top: calc(var(--step) * 1.6);
271+ padding-bottom: var(--step);
272+ margin-bottom: calc(var(--step) * 1.4);
273+ position: relative;
274+ display: flex;
275+ align-items: baseline;
276+ justify-content: space-between;
277+ gap: 0 1rem;
278+
279+ &__signal {
280+ position: absolute;
281+ left: 0;
282+ right: 0;
283+ bottom: 0;
284+ height: 26px;
285+ -webkit-mask-image: linear-gradient(
286+ to right,
287+ #000 0%,
288+ #000 62%,
289+ transparent 96%
290+ );
291+ mask-image: linear-gradient(to right, #000 0%, #000 62%, transparent 96%);
292+
293+ svg {
294+ display: block;
295+ width: 100%;
296+ height: 100%;
297+ }
298+
299+ path {
300+ fill: none;
301+ stroke: url("#signal-ramp");
302+ stroke-width: 1.5;
303+ stroke-linecap: round;
304+ stroke-linejoin: round;
305+ vector-effect: non-scaling-stroke;
306+ }
307+ }
308+
309+ .signal-ramp__lead {
310+ stop-color: var(--accent);
311+ }
312+
313+ .signal-ramp__body,
314+ .signal-ramp__tail {
315+ stop-color: var(--rule-strong);
316+ }
317+}
318+
319+.masthead__title {
320+ font-size: 1.5rem;
321+ font-weight: 600;
322+ line-height: 1.1;
323+ letter-spacing: -0.018em;
324+
325+ a {
326+ color: inherit;
327+ text-decoration: none;
328+ }
329+
330+ a:hover {
331+ color: var(--accent);
332+ }
333+}
334+
335+// Signpost to the colophon, shown only at widths where the colophon is
336+// reordered below the post list and nothing on first screen hints it is
337+// there. Turned on alongside that reorder, further down.
338+.masthead__about {
339+ @extend %chrome;
340+ display: none;
341+ flex: none;
342+ color: var(--ink-muted);
343+ text-decoration: none;
344+ border-bottom: 1px solid var(--rule);
345+
346+ &:hover {
347+ color: var(--accent);
348+ border-bottom-color: var(--accent);
349+ }
350+}
351+
352+// --- Prose ---------------------------------------------------------------
353+
354+.prose {
355+ > * + * {
356+ margin-top: var(--step);
357+ }
358+
359+ p,
360+ ul,
361+ ol,
362+ dl {
363+ line-height: 1.65;
364+ }
365+
366+ div > * + *,
367+ details > * + *,
368+ li > * + *,
369+ td > * + *,
370+ th > * + * {
371+ margin-top: 0.7rem;
372+ }
373+
374+ h1,
375+ h2,
376+ h3,
377+ h4,
378+ h5,
379+ h6 {
380+ font-weight: 600;
381+ line-height: 1.2;
382+ letter-spacing: -0.012em;
383+ margin-bottom: 0;
384+ position: relative;
385+ scroll-margin-top: 1.5rem;
386+ }
387+
388+ :is(h1, h2, h3, h4, h5, h6) + :not(h1, h2, h3, h4, h5, h6) {
389+ margin-top: 0.55rem;
390+ }
391+
392+ :is(h1, h2, h3, h4, h5, h6) + :is(h1, h2, h3, h4, h5, h6) {
393+ margin-top: calc(var(--step) * 1.1);
394+ }
395+
396+ h1 {
397+ font-size: 1.62rem;
398+ margin-top: calc(var(--step) * 2);
399+ }
400+ h2 {
401+ font-size: 1.4rem;
402+ margin-top: calc(var(--step) * 1.7);
403+ }
404+ h3 {
405+ font-size: 1.18rem;
406+ margin-top: calc(var(--step) * 1.35);
407+ }
408+ h4,
409+ h5,
410+ h6 {
411+ font-size: 1rem;
412+ font-family: var(--mono);
413+ font-weight: 600;
414+ letter-spacing: 0.02em;
415+ margin-top: calc(var(--step) * 1.1);
416+ }
417+
418+ .hanchor {
419+ position: absolute;
420+ right: calc(100% + 0.9rem);
421+ top: 0.06em;
422+ font-family: var(--mono);
423+ font-size: 0.78em;
424+ font-weight: 400;
425+ color: var(--accent);
426+ opacity: 0.4;
427+ text-decoration: none;
428+ transition: opacity 0.15s ease;
429+
430+ &:hover,
431+ &:focus-visible {
432+ opacity: 1;
433+ }
434+ }
435+
436+ @media (max-width: $wide) {
437+ .hanchor {
438+ position: static;
439+ margin-right: 0.4em;
440+ font-size: 0.7em;
441+ vertical-align: 0.1em;
442+ }
443+ }
444+
445+ ul,
446+ ol {
447+ padding-left: 1.35rem;
448+ }
449+
450+ li + li {
451+ margin-top: 0.35rem;
452+ }
453+
454+ li > ul,
455+ li > ol {
456+ margin-top: 0.35rem;
457+ }
458+
459+ ul {
460+ list-style: none;
461+ padding-left: 1.15rem;
462+ }
463+
464+ ul > li {
465+ position: relative;
466+
467+ &::before {
468+ content: "";
469+ position: absolute;
470+ left: -1.05rem;
471+ top: 0.66em;
472+ width: 0.38rem;
473+ height: 1px;
474+ background: var(--accent);
475+ opacity: 0.75;
476+ }
477+ }
478+
479+ ul ul > li::before {
480+ background: var(--rule-strong);
481+ }
482+
483+ ol {
484+ list-style: none;
485+ counter-reset: ol;
486+ padding-left: 1.85rem;
487+ }
488+
489+ ol > li {
490+ counter-increment: ol;
491+ position: relative;
492+
493+ &::before {
494+ content: counter(ol) ".";
495+ position: absolute;
496+ right: calc(100% + 0.5rem);
497+ top: 0.06em;
498+ font-family: var(--mono);
499+ font-size: 0.75em;
500+ color: var(--ink-faint);
501+ font-variant-numeric: tabular-nums;
502+ }
503+ }
504+
505+ dt {
506+ font-weight: 600;
507+ }
508+
509+ dd {
510+ margin-left: 1.35rem;
511+ }
512+
513+ strong,
514+ b {
515+ font-weight: 700;
516+ }
517+
518+ hr {
519+ border: 0;
520+ height: 1px;
521+ background: linear-gradient(
522+ to right,
523+ var(--rule-strong),
524+ var(--rule) 45%,
525+ transparent
526+ );
527+ margin-block: calc(var(--step) * 1.6);
528+ }
529+
530+ sup,
531+ sub {
532+ line-height: 0;
533+ font-size: 0.7em;
534+ }
535+}
536+
537+a {
538+ color: inherit;
539+ text-decoration: underline;
540+ text-decoration-color: var(--accent);
541+ text-decoration-thickness: 1px;
542+ text-underline-offset: 0.17em;
543+ transition:
544+ color 0.15s ease,
545+ text-decoration-color 0.15s ease;
546+
547+ &:hover {
548+ color: var(--accent);
549+ text-decoration-thickness: 2px;
550+ }
551+}
552+
553+// --- Blockquote --------------------------------------------------------
554+
555+blockquote {
556+ padding-left: 1.15rem;
557+ border-left: 2px solid var(--rule-strong);
558+ color: var(--ink-muted);
559+
560+ > * + * {
561+ margin-top: 0.7rem;
562+ }
563+}
564+
565+// --- Code ----------------------------------------------------------------
566+
567+code,
568+kbd,
569+samp {
570+ font-family: var(--mono);
571+ font-variant-ligatures: none;
572+ font-feature-settings:
573+ "liga" 0,
574+ "calt" 0;
575+}
576+
577+:not(pre) > code {
578+ font-size: 0.82em;
579+ background: var(--paper-raised);
580+ border: 1px solid var(--rule);
581+ border-radius: 3px;
582+ padding: 0.08em 0.26em;
583+ color: var(--ink);
584+ white-space: break-spaces;
585+ word-break: break-word;
586+}
587+
588+a :not(pre) > code,
589+:not(pre) > code:has(a) {
590+ color: inherit;
591+}
592+
593+kbd {
594+ font-size: 0.78em;
595+ background: var(--paper);
596+ border: 1px solid var(--rule-strong);
597+ border-bottom-width: 2px;
598+ border-radius: 3px;
599+ padding: 0.08em 0.36em;
600+ color: var(--ink);
601+ white-space: nowrap;
602+}
603+
604+// Hugo/Chroma wraps a fenced code block in <div class="highlight"><pre
605+// class="chroma"><code class="language-x" data-lang="x">. The language tag
606+// is read off that data-lang attribute and floats over the block like a
607+// fieldset legend.
608+.highlight {
609+ position: relative;
610+
611+ code[data-lang]::before {
612+ @extend %chrome;
613+ content: attr(data-lang);
614+ position: absolute;
615+ z-index: 1;
616+ top: 0;
617+ right: 0.85rem;
618+ transform: translateY(-50%);
619+ padding-inline: 0.4rem;
620+ background: var(--paper);
621+ font-size: 0.6rem;
622+ line-height: 1;
623+ color: var(--ink-faint);
624+ pointer-events: none;
625+ }
626+}
627+
628+pre {
629+ font-family: var(--mono);
630+ font-size: 0.78rem;
631+ line-height: 1.55;
632+ background: var(--paper-raised);
633+ border: 1px solid var(--rule);
634+ border-left: 2px solid var(--rule-strong);
635+ border-radius: 3px;
636+ padding: 0.85rem 1rem;
637+ overflow-x: auto;
638+ tab-size: 4;
639+ color: var(--ink);
640+
641+ code {
642+ background: none;
643+ border: 0;
644+ padding: 0;
645+ font-size: inherit;
646+ color: inherit;
647+ }
648+}
649+
650+// --- Collapsible blocks --------------------------------------------------
651+
652+.prose details {
653+ border: 1px solid var(--rule);
654+ border-left: 2px solid var(--rule-strong);
655+ border-radius: 3px;
656+ padding: 0 0.9rem;
657+
658+ > summary {
659+ font-family: var(--mono);
660+ font-size: 0.78rem;
661+ color: var(--ink);
662+ padding: 0.6rem 0;
663+ cursor: pointer;
664+ list-style: none;
665+ display: flex;
666+ align-items: baseline;
667+ gap: 0.5rem;
668+
669+ &::-webkit-details-marker {
670+ display: none;
671+ }
672+
673+ &::before {
674+ content: "";
675+ width: 0;
676+ height: 0;
677+ border-left: 0.5rem solid var(--accent);
678+ border-top: 0.29rem solid transparent;
679+ border-bottom: 0.29rem solid transparent;
680+ align-self: center;
681+ transition: transform 0.15s ease;
682+ display: inline-block;
683+ }
684+
685+ &:hover {
686+ color: var(--accent);
687+ }
688+ }
689+
690+ &[open] {
691+ padding-bottom: 0.9rem;
692+
693+ > summary {
694+ margin-bottom: 0.9rem;
695+ border-bottom: 1px solid var(--rule);
696+ color: var(--accent);
697+
698+ &::before {
699+ transform: rotate(90deg);
700+ }
701+ }
702+ }
703+
704+ .highlight {
705+ margin-inline: -0.9rem;
706+ }
707+
708+ pre {
709+ border-inline: 0;
710+ border-radius: 0;
711+ }
712+}
713+
714+@media (prefers-reduced-motion: reduce) {
715+ .prose details > summary::before {
716+ transition: none;
717+ }
718+}
719+
720+// --- Tables ----------------------------------------------------------------
721+
722+.table-scroll {
723+ overflow-x: auto;
724+ max-width: 100%;
725+}
726+
727+table {
728+ border-collapse: collapse;
729+ width: 100%;
730+ font-size: 0.85rem;
731+ font-variant-numeric: tabular-nums;
732+
733+ th,
734+ td {
735+ padding: 0.45rem 0.7rem;
736+ text-align: left;
737+ border: 0;
738+ border-bottom: 1px solid var(--rule);
739+ vertical-align: top;
740+ }
741+
742+ th:first-child,
743+ td:first-child {
744+ padding-left: 0;
745+ }
746+
747+ th:last-child,
748+ td:last-child {
749+ padding-right: 0;
750+ }
751+
752+ th {
753+ @extend %chrome;
754+ color: var(--ink-muted);
755+ border-bottom: 1px solid var(--rule-strong);
756+ letter-spacing: 0.055em;
757+ line-height: 1.35;
758+ }
759+
760+ tbody tr:last-child td {
761+ border-bottom: 0;
762+ }
763+
764+ tbody tr:hover td {
765+ background: var(--accent-wash);
766+ }
767+}
768+
769+// --- Figures and media -----------------------------------------------------
770+
771+img,
772+video,
773+canvas,
774+svg {
775+ max-width: 100%;
776+ height: auto;
777+}
778+
779+.prose img,
780+.prose video {
781+ display: block;
782+ max-width: min(var(--image-width, 100%), 100%);
783+ margin-inline: auto;
784+ border: 1px solid var(--rule);
785+ border-radius: 3px;
786+ background: var(--image-mat);
787+}
788+
789+.prose a img {
790+ border-color: var(--rule-strong);
791+}
792+
793+iframe,
794+embed,
795+object {
796+ max-width: 100%;
797+ border: 0;
798+}
799+
800+iframe[src*="youtube"],
801+iframe[src*="vimeo"] {
802+ display: block;
803+ width: 100%;
804+ aspect-ratio: 16 / 9;
805+ height: auto;
806+ margin-inline: auto;
807+ border: 1px solid var(--rule);
808+ border-radius: 3px;
809+ background: var(--image-mat);
810+}
811+
812+// Goldmark's own footnote list, styled plainly — the margin-sidenote
813+// treatment from fzakaria.com needs a custom relocation pass this port
814+// does not carry.
815+.footnotes {
816+ margin-top: calc(var(--step) * 2);
817+ padding-top: var(--step);
818+ border-top: 1px solid var(--rule);
819+ font-size: 0.85rem;
820+ color: var(--ink-muted);
821+}
822+
823+// --- Alerts ------------------------------------------------------------
824+
825+$alerts: (
826+ "note": (
827+ #0b5fa5,
828+ #6fb2e8,
829+ ),
830+ "tip": (
831+ #1a7f37,
832+ #79c98d,
833+ ),
834+ "important": (
835+ #7145c4,
836+ #b79ded,
837+ ),
838+ "warning": (
839+ #8a6100,
840+ #dfb051,
841+ ),
842+ "caution": (
843+ #b1201d,
844+ #ec8b7f,
845+ ),
846+);
847+
848+.alert {
849+ --alert: var(--rule-strong);
850+ border-left: 2px solid var(--alert);
851+ background: var(--paper-raised);
852+ padding: 0.85rem 1.1rem;
853+ border-radius: 0 3px 3px 0;
854+ color: var(--ink);
855+
856+ > * + * {
857+ margin-top: 0.65rem;
858+ }
859+
860+ p:first-child > strong:first-child {
861+ @extend %chrome;
862+ display: block;
863+ color: var(--alert);
864+ margin-bottom: 0.3rem;
865+ }
866+
867+ :not(pre) > code {
868+ background: transparent;
869+ border-color: transparent;
870+ padding: 0;
871+ }
872+
873+ @each $name, $pair in $alerts {
874+ &.alert-#{$name} {
875+ --alert: #{list.nth($pair, 1)};
876+
877+ @media (prefers-color-scheme: dark) {
878+ --alert: #{list.nth($pair, 2)};
879+ }
880+ }
881+ }
882+}
883+
884+blockquote.alert {
885+ border-left-color: var(--alert);
886+ color: var(--ink);
887+}
888+
889+// --- Post header -----------------------------------------------------------
890+
891+.post-header {
892+ margin-bottom: calc(var(--step) * 1.5);
893+}
894+
895+.post-kicker {
896+ @extend %chrome;
897+ display: flex;
898+ flex-wrap: wrap;
899+ gap: 0 0.5rem;
900+ margin-bottom: 0.7rem;
901+
902+ .sep {
903+ color: var(--rule-strong);
904+ }
905+
906+ a {
907+ color: inherit;
908+ text-decoration: none;
909+ border-bottom: 1px solid var(--rule-strong);
910+
911+ &:hover {
912+ color: var(--accent);
913+ border-bottom-color: var(--accent);
914+ }
915+ }
916+}
917+
918+.post-title {
919+ font-size: clamp(1.85rem, 1.3rem + 2.1vw, 2.85rem);
920+ font-weight: 600;
921+ line-height: 1.08;
922+ letter-spacing: -0.025em;
923+ text-wrap: balance;
924+}
925+
926+.post-rule {
927+ width: 3.5rem;
928+ height: 2px;
929+ background: var(--accent);
930+ margin: 1.1rem auto 0 0;
931+ border: 0;
932+}
933+
934+.post-body {
935+ margin-top: calc(var(--step) * 1.5);
936+}
937+
938+// --- Site footer -------------------------------------------------------
939+
940+.site-foot {
941+ @extend %chrome;
942+ margin-top: calc(var(--step) * 2.5);
943+ padding-top: var(--step);
944+ border-top: 1px solid var(--rule);
945+ text-transform: none;
946+ letter-spacing: 0.02em;
947+ font-size: 0.7rem;
948+ line-height: 1.75;
949+ color: var(--ink-faint);
950+ display: flex;
951+ flex-wrap: wrap;
952+ justify-content: center;
953+ text-align: center;
954+ gap: 0 0.5rem;
955+
956+ a {
957+ color: var(--ink-muted);
958+ text-decoration: none;
959+ border-bottom: 1px solid var(--rule);
960+
961+ &:hover {
962+ color: var(--accent);
963+ border-bottom-color: var(--accent);
964+ }
965+ }
966+
967+ .sep {
968+ color: var(--rule-strong);
969+ }
970+
971+ .rss {
972+ display: inline-flex;
973+ align-items: center;
974+ gap: 0.3rem;
975+ border-bottom: 0;
976+
977+ svg {
978+ width: 0.85em;
979+ height: 0.85em;
980+ fill: currentColor;
981+ }
982+ }
983+
984+ // Own row, so the path gets the whole measure rather than whatever is left
985+ // over beside the RSS link.
986+ .store-path-row {
987+ flex-basis: 100%;
988+ }
989+
990+ // A store path is a single 56-character token: broken across lines it is
991+ // unreadable and awkward to select. Hold it on one line and let it shrink
992+ // instead.
993+ .store-path {
994+ font-size: min(0.7rem, 2.3vw);
995+ letter-spacing: 0;
996+ white-space: nowrap;
997+ color: inherit;
998+ background: none;
999+ border: 0;
1000+ padding: 0;
1001+ }
1002+}
1003+
1004+// --- Index ---------------------------------------------------------------
1005+
1006+.page-index {
1007+ display: flow-root;
1008+}
1009+
1010+// Bio card. Floats into the margin column beside the post list; below $wide
1011+// there is no margin column to float into, so it drops below the list
1012+// instead (further down).
1013+.colophon {
1014+ float: right;
1015+ width: var(--margin-col);
1016+ margin-right: calc((var(--gap) + var(--margin-col)) * -1);
1017+ margin-bottom: var(--step);
1018+ // Jumped to from the masthead; leave air above it on arrival.
1019+ scroll-margin-top: var(--step);
1020+ font-size: 0.8rem;
1021+ line-height: 1.55;
1022+ color: var(--ink-muted);
1023+}
1024+
1025+.colophon__avatar {
1026+ width: 100%;
1027+ max-width: 8.5rem;
1028+ border-radius: 3px;
1029+ border: 1px solid var(--rule-strong);
1030+ display: block;
1031+ margin-bottom: 0.85rem;
1032+ filter: grayscale(0.35);
1033+ transition: filter 0.3s ease;
1034+
1035+ &:hover {
1036+ filter: grayscale(0);
1037+ }
1038+}
1039+
1040+.colophon__name {
1041+ font-weight: 600;
1042+ font-size: 0.95rem;
1043+ line-height: 1.3;
1044+ color: var(--ink);
1045+ margin-bottom: 0.35rem;
1046+
1047+ &-last {
1048+ font-weight: 500;
1049+ color: var(--ink-faint);
1050+ }
1051+}
1052+
1053+.colophon__bio {
1054+ margin-bottom: 1rem;
1055+
1056+ a {
1057+ text-decoration-color: var(--rule-strong);
1058+ }
1059+}
1060+
1061+.colophon__links {
1062+ list-style: none;
1063+ margin: 0;
1064+ padding: 0;
1065+ border-top: 1px solid var(--rule);
1066+
1067+ // The key sits above its value rather than beside it: a two-column split
1068+ // leaves too little room for an email address in monospace without
1069+ // breaking it across lines.
1070+ li {
1071+ padding-block: 0.4rem;
1072+ border-bottom: 1px solid var(--rule);
1073+ }
1074+
1075+ .key {
1076+ @extend %chrome;
1077+ display: block;
1078+ font-size: 0.6rem;
1079+ margin-bottom: 0.05rem;
1080+ }
1081+
1082+ .val {
1083+ display: block;
1084+ font-family: var(--mono);
1085+ font-size: 0.68rem;
1086+
1087+ a {
1088+ color: var(--ink-muted);
1089+ text-decoration: none;
1090+ border-bottom: 1px solid var(--rule-strong);
1091+
1092+ &:hover {
1093+ color: var(--accent);
1094+ border-bottom-color: var(--accent);
1095+ }
1096+ }
1097+ }
1098+}
1099+
1100+// Narrow: the float is dropped and the card moves below the article list.
1101+@media (max-width: $wide) {
1102+ .masthead__about {
1103+ display: inline-block;
1104+ }
1105+
1106+ .page-index {
1107+ display: flex;
1108+ flex-direction: column;
1109+ }
1110+
1111+ .colophon {
1112+ float: none;
1113+ width: 100%;
1114+ margin-right: 0;
1115+ margin-bottom: 0;
1116+ margin-top: calc(var(--step) * 2);
1117+ padding-top: var(--step);
1118+ border-top: 1px solid var(--rule);
1119+ order: 3;
1120+ display: grid;
1121+ grid-template-columns: 8.5rem 1fr;
1122+ gap: 0 1.25rem;
1123+ align-items: start;
1124+ }
1125+
1126+ .colophon__avatar {
1127+ margin-bottom: 0;
1128+ grid-row: span 2;
1129+ }
1130+
1131+ .index-body {
1132+ order: 2;
1133+ }
1134+
1135+ @media (max-width: 30rem) {
1136+ .colophon {
1137+ grid-template-columns: 1fr;
1138+ gap: 1rem;
1139+ }
1140+
1141+ .colophon__avatar {
1142+ max-width: 6.5rem;
1143+ }
1144+ }
1145+}
1146+
1147+.index-lead {
1148+ display: block;
1149+ text-decoration: none;
1150+ margin-bottom: calc(var(--step) * 2);
1151+ padding-left: 1rem;
1152+ border-left: 2px solid var(--accent);
1153+ position: relative;
1154+
1155+ .label {
1156+ @extend %chrome;
1157+ color: var(--accent);
1158+ display: block;
1159+ margin-bottom: 0.4rem;
1160+ }
1161+
1162+ .title {
1163+ font-size: 1.5rem;
1164+ font-weight: 600;
1165+ line-height: 1.15;
1166+ letter-spacing: -0.02em;
1167+ text-wrap: balance;
1168+ display: block;
1169+ color: inherit;
1170+ text-decoration: none;
1171+
1172+ &::after {
1173+ content: "";
1174+ position: absolute;
1175+ inset: 0;
1176+ }
1177+ }
1178+
1179+ .excerpt a {
1180+ position: relative;
1181+ z-index: 1;
1182+ }
1183+
1184+ .excerpt {
1185+ display: block;
1186+ color: var(--ink-muted);
1187+ font-size: 0.92rem;
1188+ margin-top: 0.5rem;
1189+ }
1190+
1191+ .more {
1192+ @extend %chrome;
1193+ color: var(--accent);
1194+ display: block;
1195+ margin-top: 0.7rem;
1196+ }
1197+
1198+ &:hover .title {
1199+ color: var(--accent);
1200+ }
1201+}
1202+
1203+.year {
1204+ margin-top: calc(var(--step) * 2);
1205+ position: relative;
1206+
1207+ &:first-of-type {
1208+ margin-top: 0;
1209+ }
1210+}
1211+
1212+.year__label {
1213+ @extend %chrome;
1214+ font-size: 0.78rem;
1215+ color: var(--ink-faint);
1216+ padding-bottom: 0.4rem;
1217+ margin-bottom: 0.6rem;
1218+ border-bottom: 1px solid var(--rule);
1219+ display: flex;
1220+ justify-content: space-between;
1221+ align-items: baseline;
1222+
1223+ .count {
1224+ font-size: 0.66rem;
1225+ color: var(--rule-strong);
1226+ }
1227+}
1228+
1229+@media (min-width: $wide) {
1230+ .year__label {
1231+ position: absolute;
1232+ right: calc(100% + 0.9rem);
1233+ top: 0.1rem;
1234+ width: calc(var(--gutter) - 0.9rem);
1235+ display: block;
1236+ border-bottom: 0;
1237+ padding-bottom: 0;
1238+ margin-bottom: 0;
1239+ text-align: right;
1240+ letter-spacing: 0.04em;
1241+
1242+ .count {
1243+ display: none;
1244+ }
1245+ }
1246+
1247+ .year__list {
1248+ border-top: 1px solid var(--rule);
1249+ padding-top: 0.35rem;
1250+ }
1251+}
1252+
1253+.year__list {
1254+ list-style: none;
1255+ margin: 0;
1256+ padding: 0;
1257+}
1258+
1259+.entry {
1260+ display: grid;
1261+ grid-template-columns: 4.25rem 1fr;
1262+ gap: 0 0.85rem;
1263+ align-items: baseline;
1264+ padding-block: 0.42rem;
1265+
1266+ @media (max-width: 34rem) {
1267+ grid-template-columns: 1fr;
1268+ gap: 0.12rem;
1269+ padding-block: 0.5rem;
1270+ }
1271+
1272+ + .entry {
1273+ border-top: 1px solid var(--rule);
1274+ }
1275+
1276+ .date {
1277+ @extend %chrome;
1278+ font-size: 0.68rem;
1279+ white-space: nowrap;
1280+ }
1281+
1282+ a {
1283+ text-decoration: none;
1284+ line-height: 1.35;
1285+ color: var(--ink);
1286+ text-decoration: underline;
1287+ text-decoration-color: transparent;
1288+ text-underline-offset: 0.17em;
1289+ }
1290+
1291+ &:hover a {
1292+ color: var(--accent);
1293+ text-decoration-color: var(--accent);
1294+ }
1295+
1296+ &:hover .date {
1297+ color: var(--accent);
1298+ }
1299+}
1300+
1301+// --- Motion --------------------------------------------------------------
1302+
1303+@media (prefers-reduced-motion: no-preference) {
1304+ @keyframes rise {
1305+ from {
1306+ opacity: 0;
1307+ transform: translateY(0.5rem);
1308+ }
1309+ to {
1310+ opacity: 1;
1311+ transform: none;
1312+ }
1313+ }
1314+
1315+ .rise {
1316+ animation: rise 0.5s cubic-bezier(0.22, 0.68, 0.35, 1) backwards;
1317+ animation-delay: calc(var(--i, 0) * 60ms);
1318+ }
1319+
1320+ @keyframes signal-sweep {
1321+ to {
1322+ stroke-dashoffset: 0;
1323+ }
1324+ }
1325+
1326+ .masthead__signal path {
1327+ stroke-dasharray: 100;
1328+ stroke-dashoffset: 100;
1329+ animation: signal-sweep 1.1s cubic-bezier(0.32, 0.5, 0.3, 1) 0.18s forwards;
1330+ }
1331+}
1332+
1333+// --- Print -----------------------------------------------------------------
1334+
1335+@media print {
1336+ :root {
1337+ --paper: #fff;
1338+ --paper-raised: #fff;
1339+ --ink: #000;
1340+ --ink-muted: #333;
1341+ --ink-faint: #555;
1342+ --rule: #bbb;
1343+ --rule-strong: #888;
1344+ --accent: #000;
1345+ --gutter: 0rem;
1346+ --margin-col: 0rem;
1347+ --gap: 0rem;
1348+ }
1349+
1350+ body {
1351+ background: #fff;
1352+ }
1353+
1354+ .colophon,
1355+ .skip-link {
1356+ display: none;
1357+ }
1358+
1359+ details {
1360+ border: 0;
1361+ padding: 0;
1362+ }
1363+
1364+ details > summary::before {
1365+ content: none;
1366+ }
1367+
1368+ pre,
1369+ blockquote,
1370+ details,
1371+ .alert {
1372+ break-inside: avoid;
1373+ }
1374+
1375+ a::after {
1376+ content: " (" attr(href) ")";
1377+ font-size: 0.75em;
1378+ color: #555;
1379+ }
1380+
1381+ a[href^="#"]::after {
1382+ content: "";
1383+ }
1384+}
1385+
1386+// ---------------------------------------------------------------------------
1387+// Rouge/Chroma syntax theme — "Ochre" (unchanged from fzakaria.com).
1388+// ---------------------------------------------------------------------------
1389+
1390+:root {
1391+ --syn-comment: #6f685b;
1392+ --syn-keyword: #9a3d05;
1393+ --syn-string: #3f6b32;
1394+ --syn-number: #6b4fa8;
1395+ --syn-func: #1c5570;
1396+ --syn-type: #7a6114;
1397+ --syn-var: #7c4a2d;
1398+ --syn-punct: #6b6558;
1399+ --syn-error: #b1201d;
1400+ --syn-error-bg: #b1201d1a;
1401+ --syn-ins-bg: #1a7f3714;
1402+ --syn-del-bg: #b1201d14;
1403+ --syn-ins-fg: #1a6d33;
1404+ --syn-del-fg: #a3231f;
1405+}
1406+
1407+@media (prefers-color-scheme: dark) {
1408+ :root {
1409+ --syn-comment: #8d867a;
1410+ --syn-keyword: #e08a45;
1411+ --syn-string: #a6c98d;
1412+ --syn-number: #c3a2ec;
1413+ --syn-func: #7fb6d6;
1414+ --syn-type: #ddc477;
1415+ --syn-var: #dda58c;
1416+ --syn-punct: #8b857a;
1417+ --syn-error: #ec8b7f;
1418+ --syn-error-bg: #ec8b7f1f;
1419+ --syn-ins-bg: #79c98d1a;
1420+ --syn-del-bg: #ec8b7f1a;
1421+ --syn-ins-fg: #8fd0a1;
1422+ --syn-del-fg: #ef9a8f;
1423+ }
1424+}
1425+
1426+.highlight {
1427+ .c, .ch, .cd, .cm, .cp, .cpf, .c1, .cs {
1428+ color: var(--syn-comment);
1429+ font-style: italic;
1430+ }
1431+
1432+ .k, .kc, .kd, .kn, .kp, .kr, .kv {
1433+ color: var(--syn-keyword);
1434+ }
1435+
1436+ .kt, .nc, .nn, .no, .ne {
1437+ color: var(--syn-type);
1438+ }
1439+
1440+ .nf, .fm, .nb, .bp, .na, .nt {
1441+ color: var(--syn-func);
1442+ }
1443+
1444+ .nv, .vc, .vg, .vi, .vm, .nl, .py {
1445+ color: var(--syn-var);
1446+ }
1447+
1448+ .s, .sa, .sb, .sc, .dl, .sd, .s2, .se, .sh, .si, .sx, .s1, .ss, .sr {
1449+ color: var(--syn-string);
1450+ }
1451+
1452+ .m, .mb, .mf, .mh, .mi, .mo, .il {
1453+ color: var(--syn-number);
1454+ }
1455+
1456+ .o, .ow, .p, .pi, .ni {
1457+ color: var(--syn-punct);
1458+ }
1459+
1460+ .err {
1461+ color: var(--syn-error);
1462+ background-color: var(--syn-error-bg);
1463+ border-radius: 2px;
1464+ }
1465+
1466+ .gd {
1467+ color: var(--syn-del-fg);
1468+ background-color: var(--syn-del-bg);
1469+ }
1470+
1471+ .gi {
1472+ color: var(--syn-ins-fg);
1473+ background-color: var(--syn-ins-bg);
1474+ }
1475+
1476+ .gh, .gu {
1477+ color: var(--syn-func);
1478+ font-weight: 600;
1479+ }
1480+
1481+ .gp {
1482+ color: var(--syn-comment);
1483+ user-select: none;
1484+ }
1485+
1486+ .go {
1487+ color: var(--ink-muted);
1488+ }
1489+
1490+ .ge {
1491+ font-style: italic;
1492+ }
1493+
1494+ .gs {
1495+ font-weight: 700;
1496+ }
1497+
1498+ .gr, .gt {
1499+ color: var(--syn-error);
1500+ }
1501+
1502+ .w {
1503+ color: var(--rule-strong);
1504+ }
1505+
1506+ .lineno, .gl {
1507+ color: var(--ink-faint);
1508+ opacity: 0.6;
1509+ user-select: none;
1510+ padding-right: 0.85rem;
1511+ }
1512+}
added blog/assets/images/avatar.jpg +0 -0
new file mode 100644
Binary files /dev/null and b/blog/assets/images/avatar.jpg differ
new file mode 100644
Binary files /dev/null and b/blog/assets/images/avatar.jpg differBinary files /dev/null and b/blog/assets/images/avatar.jpg differ
modified blog/hugo.toml +53 -1
@@ -1,3 +1,55 @@
11 baseURL = 'https://example.org/'
22 locale = 'en-us'
3-title = 'My New Hugo Project'
3+title = 'mykiwi.blog'
4+disableKinds = ['taxonomy', 'term']
5+
6+[params]
7+# Optional: overrides the <meta name="description"> tag with different text
8+# than the colophon bio below. Leave unset (as here) and the meta tag is
9+# derived automatically from params.profile.bio, stripped down to plain text.
10+# description = "..."
11+
12+[params.profile]
13+# Drop a square photo at blog/assets/images/avatar.jpg (or change this path)
14+# to have it show up; the colophon quietly skips itself if the file is missing.
15+avatar = "images/avatar.jpg"
16+avatarAlt = "mykiwi's GitHub avatar"
17+firstName = "Romain"
18+lastName = "GAUTIER"
19+atprotoHandle = 'mykiwi.dev'
20+# Markdown, rendered as-is in the colophon (links work: [text](url)). Also
21+# the source for the <meta name="description"> tag, unless params.description
22+# above is set to override it.
23+bio = "I'm a software engineer, passionate about technology, sharing knowledge and open source like #PHP, #Symfony, #Docker, #Dagger, #Nix and atproto:// Also [🐕️ & 🐈️](https://instagram.com/apy.leon \"Pictures of Apy & Léon\") dad."
24+
25+[[params.profile.links]]
26+key = "bluesky"
27+value = "@mykiwi.dev"
28+url = "https://bsky.app/profile/mykiwi.dev"
29+
30+[[params.profile.links]]
31+key = "tangled"
32+value = "@mykiwi.dev"
33+url = "https://tangled.org/mykiwi.dev"
34+
35+[[params.profile.links]]
36+key = "github"
37+value = "mykiwi"
38+url = "https://github.com/mykiwi"
39+
40+[[params.profile.links]]
41+key = "email"
42+value = "hi@mykiwi.blog"
43+url = "mailto:you@example.com"
44+
45+[markup.highlight]
46+noClasses = false
47+
48+[markup.goldmark.renderer]
49+unsafe = true
50+
51+[markup.goldmark.parser]
52+autoHeadingID = true
53+
54+[markup.goldmark.parser.attribute]
55+title = true
@@ -1,3 +1,55 @@
1 baseURL = 'https://example.org/'1 baseURL = 'https://example.org/'
2 locale = 'en-us'2 locale = 'en-us'
3-title = 'My New Hugo Project'3+title = 'mykiwi.blog'
4+disableKinds = ['taxonomy', 'term']
5+
6+[params]
7+# Optional: overrides the <meta name="description"> tag with different text
8+# than the colophon bio below. Leave unset (as here) and the meta tag is
9+# derived automatically from params.profile.bio, stripped down to plain text.
10+# description = "..."
11+
12+[params.profile]
13+# Drop a square photo at blog/assets/images/avatar.jpg (or change this path)
14+# to have it show up; the colophon quietly skips itself if the file is missing.
15+avatar = "images/avatar.jpg"
16+avatarAlt = "mykiwi's GitHub avatar"
17+firstName = "Romain"
18+lastName = "GAUTIER"
19+atprotoHandle = 'mykiwi.dev'
20+# Markdown, rendered as-is in the colophon (links work: [text](url)). Also
21+# the source for the <meta name="description"> tag, unless params.description
22+# above is set to override it.
23+bio = "I'm a software engineer, passionate about technology, sharing knowledge and open source like #PHP, #Symfony, #Docker, #Dagger, #Nix and atproto:// Also [🐕️ & 🐈️](https://instagram.com/apy.leon \"Pictures of Apy & Léon\") dad."
24+
25+[[params.profile.links]]
26+key = "bluesky"
27+value = "@mykiwi.dev"
28+url = "https://bsky.app/profile/mykiwi.dev"
29+
30+[[params.profile.links]]
31+key = "tangled"
32+value = "@mykiwi.dev"
33+url = "https://tangled.org/mykiwi.dev"
34+
35+[[params.profile.links]]
36+key = "github"
37+value = "mykiwi"
38+url = "https://github.com/mykiwi"
39+
40+[[params.profile.links]]
41+key = "email"
42+value = "hi@mykiwi.blog"
43+url = "mailto:you@example.com"
44+
45+[markup.highlight]
46+noClasses = false
47+
48+[markup.goldmark.renderer]
49+unsafe = true
50+
51+[markup.goldmark.parser]
52+autoHeadingID = true
53+
54+[markup.goldmark.parser.attribute]
55+title = true
added blog/layouts/_default/_markup/render-heading.html +1 -0
new file mode 100644
@@ -0,0 +1 @@
1+<h{{ .Level }} id="{{ .Anchor }}">{{ .Text | safeHTML }} <a class="hanchor" href="#{{ .Anchor }}" aria-label="Permalink to this section">&sect;</a></h{{ .Level }}>
new file mode 100644
@@ -0,0 +1 @@
1+<h{{ .Level }} id="{{ .Anchor }}">{{ .Text | safeHTML }} <a class="hanchor" href="#{{ .Anchor }}" aria-label="Permalink to this section">&sect;</a></h{{ .Level }}>
added blog/layouts/_default/baseof.html +46 -0
new file mode 100644
@@ -0,0 +1,46 @@
1+<!doctype html>
2+<html lang="{{ site.Language.Lang }}">
3+ <head>
4+ <meta charset="utf-8">
5+ <meta name="viewport" content="width=device-width, initial-scale=1">
6+ <meta name="color-scheme" content="light dark">
7+ <meta name="theme-color" content="#f5f2ec" media="(prefers-color-scheme: light)">
8+ <meta name="theme-color" content="#15171a" media="(prefers-color-scheme: dark)">
9+
10+ <title>{{ if .IsHome }}{{ site.Title }}{{ else }}{{ .Title }} &middot; {{ site.Title }}{{ end }}</title>
11+ {{- /*
12+ Priority: this page's own front matter > an explicit site-wide
13+ params.description override > params.profile.bio, stripped of markdown
14+ down to plain text. Set params.description only when the SEO snippet
15+ should say something different from the colophon bio; otherwise bio is
16+ the single source of truth for both.
17+ */}}
18+ {{- $desc := .Description }}
19+ {{- if not $desc }}{{ $desc = site.Params.description }}{{ end }}
20+ {{- if not $desc }}{{ with site.Params.profile.bio }}{{ $desc = . | markdownify | plainify }}{{ end }}{{ end }}
21+ {{ with $desc -}}
22+ <meta name="description" content="{{ . }}">
23+ {{- end }}
24+
25+ <!-- Fonts are self-hosted, so there is no third-party DNS + TLS before first paint. -->
26+ <link rel="preload" href="/fonts/Newsreader-latin.woff2" as="font" type="font/woff2" crossorigin>
27+ <link rel="preload" href="/fonts/JetBrainsMono-Regular.woff2" as="font" type="font/woff2" crossorigin>
28+ {{ with resources.Get "css/style.scss" }}
29+ {{ $opts := dict "transpiler" "dartsass" "outputStyle" "compressed" }}
30+ {{ with . | toCSS $opts }}
31+ <link rel="stylesheet" type="text/css" href="{{ .RelPermalink }}">
32+ {{ end }}
33+ {{ end }}
34+
35+ <link rel="alternate" type="application/rss+xml" title="{{ site.Title }}" href="{{ "index.xml" | absURL }}">
36+ {{ block "head" . }}{{ end }}
37+ </head>
38+ <body>
39+ <a class="skip-link" href="#main">Skip to content</a>
40+ <div class="wrap">
41+ {{ partial "masthead.html" . }}
42+ <main id="main">{{ block "main" . }}{{ end }}</main>
43+ {{ partial "site-foot.html" . }}
44+ </div>
45+ </body>
46+</html>
new file mode 100644
@@ -0,0 +1,46 @@
1+<!doctype html>
2+<html lang="{{ site.Language.Lang }}">
3+ <head>
4+ <meta charset="utf-8">
5+ <meta name="viewport" content="width=device-width, initial-scale=1">
6+ <meta name="color-scheme" content="light dark">
7+ <meta name="theme-color" content="#f5f2ec" media="(prefers-color-scheme: light)">
8+ <meta name="theme-color" content="#15171a" media="(prefers-color-scheme: dark)">
9+
10+ <title>{{ if .IsHome }}{{ site.Title }}{{ else }}{{ .Title }} &middot; {{ site.Title }}{{ end }}</title>
11+ {{- /*
12+ Priority: this page's own front matter > an explicit site-wide
13+ params.description override > params.profile.bio, stripped of markdown
14+ down to plain text. Set params.description only when the SEO snippet
15+ should say something different from the colophon bio; otherwise bio is
16+ the single source of truth for both.
17+ */}}
18+ {{- $desc := .Description }}
19+ {{- if not $desc }}{{ $desc = site.Params.description }}{{ end }}
20+ {{- if not $desc }}{{ with site.Params.profile.bio }}{{ $desc = . | markdownify | plainify }}{{ end }}{{ end }}
21+ {{ with $desc -}}
22+ <meta name="description" content="{{ . }}">
23+ {{- end }}
24+
25+ <!-- Fonts are self-hosted, so there is no third-party DNS + TLS before first paint. -->
26+ <link rel="preload" href="/fonts/Newsreader-latin.woff2" as="font" type="font/woff2" crossorigin>
27+ <link rel="preload" href="/fonts/JetBrainsMono-Regular.woff2" as="font" type="font/woff2" crossorigin>
28+ {{ with resources.Get "css/style.scss" }}
29+ {{ $opts := dict "transpiler" "dartsass" "outputStyle" "compressed" }}
30+ {{ with . | toCSS $opts }}
31+ <link rel="stylesheet" type="text/css" href="{{ .RelPermalink }}">
32+ {{ end }}
33+ {{ end }}
34+
35+ <link rel="alternate" type="application/rss+xml" title="{{ site.Title }}" href="{{ "index.xml" | absURL }}">
36+ {{ block "head" . }}{{ end }}
37+ </head>
38+ <body>
39+ <a class="skip-link" href="#main">Skip to content</a>
40+ <div class="wrap">
41+ {{ partial "masthead.html" . }}
42+ <main id="main">{{ block "main" . }}{{ end }}</main>
43+ {{ partial "site-foot.html" . }}
44+ </div>
45+ </body>
46+</html>
added blog/layouts/_default/list.html +27 -0
new file mode 100644
@@ -0,0 +1,27 @@
1+{{ define "main" }}
2+{{ $years := .Pages.GroupByDate "2006" }}
3+<div class="page-index column">
4+ <header class="post-header rise">
5+ <h1 class="post-title">{{ .Title }}</h1>
6+ <hr class="post-rule">
7+ </header>
8+ <div class="index-body">
9+ {{ range $i, $year := $years }}
10+ <section class="year rise" style="--i: {{ add $i 1 }}">
11+ <h2 class="year__label">
12+ {{ $year.Key }}
13+ <span class="count">{{ len $year.Pages }} posts</span>
14+ </h2>
15+ <ul class="year__list">
16+ {{ range $year.Pages }}
17+ <li class="entry">
18+ <span class="date">{{ .Date.Format "Jan 02" }}</span>
19+ <a href="{{ .RelPermalink }}">{{ .Title }}</a>
20+ </li>
21+ {{ end }}
22+ </ul>
23+ </section>
24+ {{ end }}
25+ </div>
26+</div>
27+{{ end }}
new file mode 100644
@@ -0,0 +1,27 @@
1+{{ define "main" }}
2+{{ $years := .Pages.GroupByDate "2006" }}
3+<div class="page-index column">
4+ <header class="post-header rise">
5+ <h1 class="post-title">{{ .Title }}</h1>
6+ <hr class="post-rule">
7+ </header>
8+ <div class="index-body">
9+ {{ range $i, $year := $years }}
10+ <section class="year rise" style="--i: {{ add $i 1 }}">
11+ <h2 class="year__label">
12+ {{ $year.Key }}
13+ <span class="count">{{ len $year.Pages }} posts</span>
14+ </h2>
15+ <ul class="year__list">
16+ {{ range $year.Pages }}
17+ <li class="entry">
18+ <span class="date">{{ .Date.Format "Jan 02" }}</span>
19+ <a href="{{ .RelPermalink }}">{{ .Title }}</a>
20+ </li>
21+ {{ end }}
22+ </ul>
23+ </section>
24+ {{ end }}
25+ </div>
26+</div>
27+{{ end }}
added blog/layouts/_default/single.html +17 -0
new file mode 100644
@@ -0,0 +1,17 @@
1+{{ define "main" }}
2+<article class="column">
3+ <header class="post-header rise">
4+ <p class="post-kicker">
5+ {{ with .Date }}
6+ <time datetime="{{ .Format "2006-01-02T15:04:05Z07:00" }}">{{ .Format "2006-01-02" }}</time>
7+ <span class="sep" aria-hidden="true">&middot;</span>
8+ {{ end }}
9+ <span>{{ .ReadingTime }} min read</span>
10+ </p>
11+ <h1 class="post-title">{{ .Title }}</h1>
12+ <hr class="post-rule">
13+ </header>
14+
15+ <div class="post-body prose">{{ .Content }}</div>
16+</article>
17+{{ end }}
new file mode 100644
@@ -0,0 +1,17 @@
1+{{ define "main" }}
2+<article class="column">
3+ <header class="post-header rise">
4+ <p class="post-kicker">
5+ {{ with .Date }}
6+ <time datetime="{{ .Format "2006-01-02T15:04:05Z07:00" }}">{{ .Format "2006-01-02" }}</time>
7+ <span class="sep" aria-hidden="true">&middot;</span>
8+ {{ end }}
9+ <span>{{ .ReadingTime }} min read</span>
10+ </p>
11+ <h1 class="post-title">{{ .Title }}</h1>
12+ <hr class="post-rule">
13+ </header>
14+
15+ <div class="post-body prose">{{ .Content }}</div>
16+</article>
17+{{ end }}
added blog/layouts/index.html +38 -0
new file mode 100644
@@ -0,0 +1,38 @@
1+{{ define "main" }}
2+{{ $posts := (where site.RegularPages "Section" "posts").ByDate.Reverse }}
3+{{ $lead := index $posts 0 }}
4+{{ $rest := after 1 $posts }}
5+{{ $years := $rest.GroupByDate "2006" }}
6+
7+<div class="page-index column">
8+ {{ partial "colophon.html" . }}
9+
10+ <div class="index-body">
11+ {{ with $lead }}
12+ <div class="index-lead rise" style="--i: 1">
13+ <span class="label">Latest &middot; {{ .Date.Format "January 2, 2006" }}</span>
14+ <a class="title" href="{{ .RelPermalink }}">{{ .Title }}</a>
15+ <span class="excerpt">{{ .Summary }}</span>
16+ <span class="more" aria-hidden="true">Read &rarr;</span>
17+ </div>
18+ {{ end }}
19+
20+ {{ range $i, $year := $years }}
21+ <section class="year rise" style="--i: {{ add $i 2 }}">
22+ <h2 class="year__label">
23+ {{ $year.Key }}
24+ <span class="count">{{ len $year.Pages }} posts</span>
25+ </h2>
26+ <ul class="year__list">
27+ {{ range $year.Pages }}
28+ <li class="entry">
29+ <span class="date">{{ .Date.Format "Jan 02" }}</span>
30+ <a href="{{ .RelPermalink }}">{{ .Title }}</a>
31+ </li>
32+ {{ end }}
33+ </ul>
34+ </section>
35+ {{ end }}
36+ </div>
37+</div>
38+{{ end }}
new file mode 100644
@@ -0,0 +1,38 @@
1+{{ define "main" }}
2+{{ $posts := (where site.RegularPages "Section" "posts").ByDate.Reverse }}
3+{{ $lead := index $posts 0 }}
4+{{ $rest := after 1 $posts }}
5+{{ $years := $rest.GroupByDate "2006" }}
6+
7+<div class="page-index column">
8+ {{ partial "colophon.html" . }}
9+
10+ <div class="index-body">
11+ {{ with $lead }}
12+ <div class="index-lead rise" style="--i: 1">
13+ <span class="label">Latest &middot; {{ .Date.Format "January 2, 2006" }}</span>
14+ <a class="title" href="{{ .RelPermalink }}">{{ .Title }}</a>
15+ <span class="excerpt">{{ .Summary }}</span>
16+ <span class="more" aria-hidden="true">Read &rarr;</span>
17+ </div>
18+ {{ end }}
19+
20+ {{ range $i, $year := $years }}
21+ <section class="year rise" style="--i: {{ add $i 2 }}">
22+ <h2 class="year__label">
23+ {{ $year.Key }}
24+ <span class="count">{{ len $year.Pages }} posts</span>
25+ </h2>
26+ <ul class="year__list">
27+ {{ range $year.Pages }}
28+ <li class="entry">
29+ <span class="date">{{ .Date.Format "Jan 02" }}</span>
30+ <a href="{{ .RelPermalink }}">{{ .Title }}</a>
31+ </li>
32+ {{ end }}
33+ </ul>
34+ </section>
35+ {{ end }}
36+ </div>
37+</div>
38+{{ end }}
added blog/layouts/partials/colophon.html +39 -0
new file mode 100644
@@ -0,0 +1,39 @@
1+{{- /*
2+ Bio card for the homepage. Ported from fzakaria.com's _includes/colophon.html.
3+ Reads entirely from site.Params.profile, so it renders nothing until that's
4+ configured in hugo.toml, and the avatar itself no-ops if the file is missing.
5+*/ -}}
6+{{- with site.Params.profile -}}
7+ {{- $profile := . -}}
8+<aside class="colophon" id="about">
9+ {{- with .avatar }}
10+ {{- with resources.Get . }}
11+ <img
12+ class="colophon__avatar"
13+ src="{{ .RelPermalink }}"
14+ width="{{ .Width }}"
15+ height="{{ .Height }}"
16+ alt="{{ $profile.avatarAlt | default site.Title }}"
17+ >
18+ {{- end }}
19+ {{- end }}
20+ <div>
21+ {{- if or .firstName .lastName }}
22+ <p class="colophon__name">{{ with .firstName }}{{ . }} {{ end }}<span class="colophon__name-last">{{ .lastName }}</span></p>
23+ {{- end }}
24+ {{- with .bio | default site.Params.description }}
25+ <div class="colophon__bio">{{ . | markdownify }}</div>
26+ {{- end }}
27+ {{- with .links }}
28+ <ul class="colophon__links">
29+ {{- range . }}
30+ <li>
31+ <span class="key">{{ .key }}</span>
32+ <span class="val"><a href="{{ .url }}">{{ .value }}</a></span>
33+ </li>
34+ {{- end }}
35+ </ul>
36+ {{- end }}
37+ </div>
38+</aside>
39+{{- end -}}
new file mode 100644
@@ -0,0 +1,39 @@
1+{{- /*
2+ Bio card for the homepage. Ported from fzakaria.com's _includes/colophon.html.
3+ Reads entirely from site.Params.profile, so it renders nothing until that's
4+ configured in hugo.toml, and the avatar itself no-ops if the file is missing.
5+*/ -}}
6+{{- with site.Params.profile -}}
7+ {{- $profile := . -}}
8+<aside class="colophon" id="about">
9+ {{- with .avatar }}
10+ {{- with resources.Get . }}
11+ <img
12+ class="colophon__avatar"
13+ src="{{ .RelPermalink }}"
14+ width="{{ .Width }}"
15+ height="{{ .Height }}"
16+ alt="{{ $profile.avatarAlt | default site.Title }}"
17+ >
18+ {{- end }}
19+ {{- end }}
20+ <div>
21+ {{- if or .firstName .lastName }}
22+ <p class="colophon__name">{{ with .firstName }}{{ . }} {{ end }}<span class="colophon__name-last">{{ .lastName }}</span></p>
23+ {{- end }}
24+ {{- with .bio | default site.Params.description }}
25+ <div class="colophon__bio">{{ . | markdownify }}</div>
26+ {{- end }}
27+ {{- with .links }}
28+ <ul class="colophon__links">
29+ {{- range . }}
30+ <li>
31+ <span class="key">{{ .key }}</span>
32+ <span class="val"><a href="{{ .url }}">{{ .value }}</a></span>
33+ </li>
34+ {{- end }}
35+ </ul>
36+ {{- end }}
37+ </div>
38+</aside>
39+{{- end -}}
added blog/layouts/partials/masthead.html +23 -0
new file mode 100644
@@ -0,0 +1,23 @@
1+{{- $signal := partial "signal.html" .RelPermalink -}}
2+<header class="masthead column rise">
3+ <p class="masthead__title"><a href="{{ "/" | relURL }}">{{ site.Title }}</a></p>
4+ {{- if and .IsHome site.Params.profile }}
5+ <a class="masthead__about" href="#about">About</a>
6+ {{- end }}
7+ <div
8+ class="masthead__signal"
9+ aria-hidden="true"
10+ title="sha256 of this page's URL, drawn — {{ $signal.hash }}"
11+ >
12+ <svg viewBox="0 0 1000 100" preserveAspectRatio="none" focusable="false">
13+ <defs>
14+ <linearGradient id="signal-ramp" x1="0" y1="0" x2="1" y2="0">
15+ <stop class="signal-ramp__lead" offset="0" />
16+ <stop class="signal-ramp__body" offset="0.24" />
17+ <stop class="signal-ramp__tail" offset="1" />
18+ </linearGradient>
19+ </defs>
20+ <path d="{{ $signal.path }}" pathLength="100" />
21+ </svg>
22+ </div>
23+</header>
new file mode 100644
@@ -0,0 +1,23 @@
1+{{- $signal := partial "signal.html" .RelPermalink -}}
2+<header class="masthead column rise">
3+ <p class="masthead__title"><a href="{{ "/" | relURL }}">{{ site.Title }}</a></p>
4+ {{- if and .IsHome site.Params.profile }}
5+ <a class="masthead__about" href="#about">About</a>
6+ {{- end }}
7+ <div
8+ class="masthead__signal"
9+ aria-hidden="true"
10+ title="sha256 of this page's URL, drawn — {{ $signal.hash }}"
11+ >
12+ <svg viewBox="0 0 1000 100" preserveAspectRatio="none" focusable="false">
13+ <defs>
14+ <linearGradient id="signal-ramp" x1="0" y1="0" x2="1" y2="0">
15+ <stop class="signal-ramp__lead" offset="0" />
16+ <stop class="signal-ramp__body" offset="0.24" />
17+ <stop class="signal-ramp__tail" offset="1" />
18+ </linearGradient>
19+ </defs>
20+ <path d="{{ $signal.path }}" pathLength="100" />
21+ </svg>
22+ </div>
23+</header>
added blog/layouts/partials/signal-num.html +7 -0
new file mode 100644
@@ -0,0 +1,7 @@
1+{{- $f := . -}}
2+{{- $s := printf "%.2f" $f -}}
3+{{- $s = strings.TrimRight "0" $s -}}
4+{{- $s = strings.TrimRight "." $s -}}
5+{{- if eq $s "" }}{{ $s = "0" }}{{ end -}}
6+{{- if eq $s "-0" }}{{ $s = "0" }}{{ end -}}
7+{{- return $s -}}
new file mode 100644
@@ -0,0 +1,7 @@
1+{{- $f := . -}}
2+{{- $s := printf "%.2f" $f -}}
3+{{- $s = strings.TrimRight "0" $s -}}
4+{{- $s = strings.TrimRight "." $s -}}
5+{{- if eq $s "" }}{{ $s = "0" }}{{ end -}}
6+{{- if eq $s "-0" }}{{ $s = "0" }}{{ end -}}
7+{{- return $s -}}
added blog/layouts/partials/signal.html +57 -0
new file mode 100644
@@ -0,0 +1,57 @@
1+{{- /*
2+ Plots a Catmull-Rom curve through the 32 bytes of SHA-256(url), one sample
3+ per byte, emitted as an SVG path. Port of _plugins/signal.rb.
4+*/ -}}
5+{{- $url := . -}}
6+{{- $hash := crypto.SHA256 $url -}}
7+{{- $hexVal := dict "0" 0 "1" 1 "2" 2 "3" 3 "4" 4 "5" 5 "6" 6 "7" 7 "8" 8 "9" 9 "a" 10 "b" 11 "c" 12 "d" 13 "e" 14 "f" 15 -}}
8+
9+{{- $width := 1000.0 -}}
10+{{- $height := 100.0 -}}
11+{{- $pad := 18.0 -}}
12+{{- $tension := 0.72 -}}
13+{{- $n := 32 -}}
14+{{- $last := sub $n 1 -}}
15+{{- $span := sub $height (mul 2 $pad) -}}
16+{{- $step := div $width (sub $n 1.0) -}}
17+
18+{{- $bytes := slice -}}
19+{{- range $i := seq 0 $last -}}
20+ {{- $hi := index $hexVal (substr $hash (mul $i 2) 1) -}}
21+ {{- $lo := index $hexVal (substr $hash (add (mul $i 2) 1) 1) -}}
22+ {{- $bytes = $bytes | append (add (mul $hi 16) $lo) -}}
23+{{- end -}}
24+
25+{{- $pts := slice -}}
26+{{- range $i, $b := $bytes -}}
27+ {{- $x := mul (float $i) $step -}}
28+ {{- $y := add $pad (mul (sub 1.0 (div (float $b) 255.0)) $span) -}}
29+ {{- $pts = $pts | append (dict "x" $x "y" $y) -}}
30+{{- end -}}
31+
32+{{- $p0 := index $pts 0 -}}
33+{{- $d := printf "M%s,%s" (partial "signal-num.html" $p0.x) (partial "signal-num.html" $p0.y) -}}
34+
35+{{- range $i := seq 0 (sub $last 1) -}}
36+ {{- $p0idx := 0 -}}
37+ {{- if ge $i 1 }}{{ $p0idx = sub $i 1 }}{{ end -}}
38+ {{- $p3idx := add $i 2 -}}
39+ {{- if gt $p3idx $last }}{{ $p3idx = $last }}{{ end -}}
40+
41+ {{- $pp0 := index $pts $p0idx -}}
42+ {{- $pp1 := index $pts $i -}}
43+ {{- $pp2 := index $pts (add $i 1) -}}
44+ {{- $pp3 := index $pts $p3idx -}}
45+
46+ {{- $c1x := add $pp1.x (mul (div (sub $pp2.x $pp0.x) 6.0) $tension) -}}
47+ {{- $c1y := add $pp1.y (mul (div (sub $pp2.y $pp0.y) 6.0) $tension) -}}
48+ {{- $c2x := sub $pp2.x (mul (div (sub $pp3.x $pp1.x) 6.0) $tension) -}}
49+ {{- $c2y := sub $pp2.y (mul (div (sub $pp3.y $pp1.y) 6.0) $tension) -}}
50+
51+ {{- $d = printf "%s C%s,%s %s,%s %s,%s" $d
52+ (partial "signal-num.html" $c1x) (partial "signal-num.html" $c1y)
53+ (partial "signal-num.html" $c2x) (partial "signal-num.html" $c2y)
54+ (partial "signal-num.html" $pp2.x) (partial "signal-num.html" $pp2.y) -}}
55+{{- end -}}
56+
57+{{- return (dict "hash" $hash "path" $d) -}}
new file mode 100644
@@ -0,0 +1,57 @@
1+{{- /*
2+ Plots a Catmull-Rom curve through the 32 bytes of SHA-256(url), one sample
3+ per byte, emitted as an SVG path. Port of _plugins/signal.rb.
4+*/ -}}
5+{{- $url := . -}}
6+{{- $hash := crypto.SHA256 $url -}}
7+{{- $hexVal := dict "0" 0 "1" 1 "2" 2 "3" 3 "4" 4 "5" 5 "6" 6 "7" 7 "8" 8 "9" 9 "a" 10 "b" 11 "c" 12 "d" 13 "e" 14 "f" 15 -}}
8+
9+{{- $width := 1000.0 -}}
10+{{- $height := 100.0 -}}
11+{{- $pad := 18.0 -}}
12+{{- $tension := 0.72 -}}
13+{{- $n := 32 -}}
14+{{- $last := sub $n 1 -}}
15+{{- $span := sub $height (mul 2 $pad) -}}
16+{{- $step := div $width (sub $n 1.0) -}}
17+
18+{{- $bytes := slice -}}
19+{{- range $i := seq 0 $last -}}
20+ {{- $hi := index $hexVal (substr $hash (mul $i 2) 1) -}}
21+ {{- $lo := index $hexVal (substr $hash (add (mul $i 2) 1) 1) -}}
22+ {{- $bytes = $bytes | append (add (mul $hi 16) $lo) -}}
23+{{- end -}}
24+
25+{{- $pts := slice -}}
26+{{- range $i, $b := $bytes -}}
27+ {{- $x := mul (float $i) $step -}}
28+ {{- $y := add $pad (mul (sub 1.0 (div (float $b) 255.0)) $span) -}}
29+ {{- $pts = $pts | append (dict "x" $x "y" $y) -}}
30+{{- end -}}
31+
32+{{- $p0 := index $pts 0 -}}
33+{{- $d := printf "M%s,%s" (partial "signal-num.html" $p0.x) (partial "signal-num.html" $p0.y) -}}
34+
35+{{- range $i := seq 0 (sub $last 1) -}}
36+ {{- $p0idx := 0 -}}
37+ {{- if ge $i 1 }}{{ $p0idx = sub $i 1 }}{{ end -}}
38+ {{- $p3idx := add $i 2 -}}
39+ {{- if gt $p3idx $last }}{{ $p3idx = $last }}{{ end -}}
40+
41+ {{- $pp0 := index $pts $p0idx -}}
42+ {{- $pp1 := index $pts $i -}}
43+ {{- $pp2 := index $pts (add $i 1) -}}
44+ {{- $pp3 := index $pts $p3idx -}}
45+
46+ {{- $c1x := add $pp1.x (mul (div (sub $pp2.x $pp0.x) 6.0) $tension) -}}
47+ {{- $c1y := add $pp1.y (mul (div (sub $pp2.y $pp0.y) 6.0) $tension) -}}
48+ {{- $c2x := sub $pp2.x (mul (div (sub $pp3.x $pp1.x) 6.0) $tension) -}}
49+ {{- $c2y := sub $pp2.y (mul (div (sub $pp3.y $pp1.y) 6.0) $tension) -}}
50+
51+ {{- $d = printf "%s C%s,%s %s,%s %s,%s" $d
52+ (partial "signal-num.html" $c1x) (partial "signal-num.html" $c1y)
53+ (partial "signal-num.html" $c2x) (partial "signal-num.html" $c2y)
54+ (partial "signal-num.html" $pp2.x) (partial "signal-num.html" $pp2.y) -}}
55+{{- end -}}
56+
57+{{- return (dict "hash" $hash "path" $d) -}}
added blog/layouts/partials/site-foot.html +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+<footer class="site-foot column">
2+ <span>Text is <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA</a></span>
3+ <span class="sep" aria-hidden="true">&middot;</span>
4+ <span>
5+ <a class="rss" href="{{ "index.xml" | relURL }}" title="Subscribe via RSS">
6+ <svg viewBox="0 0 24 24" aria-hidden="true">
7+ <path d="M4 11a9 9 0 0 1 9 9h-2.5A6.5 6.5 0 0 0 4 13.5V11z" />
8+ <path d="M4 4a16 16 0 0 1 16 16h-2.5A13.5 13.5 0 0 0 4 6.5V4z" />
9+ <circle cx="5.5" cy="18.5" r="2" />
10+ </svg>
11+ RSS
12+ </a>
13+ </span>
14+ {{ with site.Params.nixstorepath }}
15+ <span class="store-path-row">
16+ <code class="store-path" title="This site was built by Nix">{{ . }}</code>
17+ </span>
18+ {{ end }}
19+</footer>
new file mode 100644
@@ -0,0 +1,19 @@
1+<footer class="site-foot column">
2+ <span>Text is <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA</a></span>
3+ <span class="sep" aria-hidden="true">&middot;</span>
4+ <span>
5+ <a class="rss" href="{{ "index.xml" | relURL }}" title="Subscribe via RSS">
6+ <svg viewBox="0 0 24 24" aria-hidden="true">
7+ <path d="M4 11a9 9 0 0 1 9 9h-2.5A6.5 6.5 0 0 0 4 13.5V11z" />
8+ <path d="M4 4a16 16 0 0 1 16 16h-2.5A13.5 13.5 0 0 0 4 6.5V4z" />
9+ <circle cx="5.5" cy="18.5" r="2" />
10+ </svg>
11+ RSS
12+ </a>
13+ </span>
14+ {{ with site.Params.nixstorepath }}
15+ <span class="store-path-row">
16+ <code class="store-path" title="This site was built by Nix">{{ . }}</code>
17+ </span>
18+ {{ end }}
19+</footer>
added blog/layouts/shortcodes/alert.html +3 -0
new file mode 100644
@@ -0,0 +1,3 @@
1+<div class="alert alert-{{ .Get 0 | default "note" }}">
2+{{ .Inner }}
3+</div>
new file mode 100644
@@ -0,0 +1,3 @@
1+<div class="alert alert-{{ .Get 0 | default "note" }}">
2+{{ .Inner }}
3+</div>
added blog/static/fonts/JetBrainsMono-Regular.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/blog/static/fonts/JetBrainsMono-Regular.woff2 differ
new file mode 100644
Binary files /dev/null and b/blog/static/fonts/JetBrainsMono-Regular.woff2 differBinary files /dev/null and b/blog/static/fonts/JetBrainsMono-Regular.woff2 differ
added blog/static/fonts/Newsreader-italic-latin-ext.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/blog/static/fonts/Newsreader-italic-latin-ext.woff2 differ
new file mode 100644
Binary files /dev/null and b/blog/static/fonts/Newsreader-italic-latin-ext.woff2 differBinary files /dev/null and b/blog/static/fonts/Newsreader-italic-latin-ext.woff2 differ
added blog/static/fonts/Newsreader-italic-latin.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/blog/static/fonts/Newsreader-italic-latin.woff2 differ
new file mode 100644
Binary files /dev/null and b/blog/static/fonts/Newsreader-italic-latin.woff2 differBinary files /dev/null and b/blog/static/fonts/Newsreader-italic-latin.woff2 differ
added blog/static/fonts/Newsreader-latin-ext.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/blog/static/fonts/Newsreader-latin-ext.woff2 differ
new file mode 100644
Binary files /dev/null and b/blog/static/fonts/Newsreader-latin-ext.woff2 differBinary files /dev/null and b/blog/static/fonts/Newsreader-latin-ext.woff2 differ
added blog/static/fonts/Newsreader-latin.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/blog/static/fonts/Newsreader-latin.woff2 differ
new file mode 100644
Binary files /dev/null and b/blog/static/fonts/Newsreader-latin.woff2 differBinary files /dev/null and b/blog/static/fonts/Newsreader-latin.woff2 differ
modified flake.nix +15 -8
@@ -6,28 +6,35 @@
66 outputs = { self, multiverse }:
77 let
88 system = "x86_64-linux";
9- mv = multiverse.multiverse.${system};
10- hugoVersion = "0.166.0";
11- hugo = mv.version "hugo" hugoVersion;
9+ pin = multiverse.multiverse.${system};
10+ pkgs = pin.tip;
11+ hugo = pin.version "hugo" "0.166.0";
12+ dart-sass = pin.version "dart-sass" "1.104.0";
1213 in
1314 {
14- packages.${system}.default = mv.tip.stdenvNoCC.mkDerivation {
15+ packages.${system}.default = pkgs.stdenvNoCC.mkDerivation {
1516 name = "mykiwi-blog";
1617 src = ./blog;
17- nativeBuildInputs = [ hugo ];
18- buildPhase = "hugo --minify --destination $out";
18+ nativeBuildInputs = [ hugo dart-sass ];
19+ buildPhase = ''
20+ HUGO_PARAMS_NIXSTOREPATH="$out" hugo --minify --destination "$out"
21+ '';
1922 dontInstall = true;
2023 };
2124
22- devShells.${system}.default = mv.tip.mkShell {
25+ devShells.${system}.default = pkgs.mkShell {
2326 name = "mykiwi-blog";
2427
2528 packages = [
2629 hugo
30+ dart-sass
2731 ];
2832
2933 shellHook = ''
30- echo "Hugo $(hugo version)"
34+ dim=$'\033[2m'
35+ reset=$'\033[0m'
36+ printf '%-11s%-9s%s%s%s\n' "Hugo" "$(hugo version | sed -n 's/.*v\([0-9.]*\)+.*/\1/p')" "$dim" "https://github.com/gohugoio/hugo/releases" "$reset"
37+ printf '%-11s%-9s%s%s%s\n' "Dart Sass" "$(sass --version)" "$dim" "https://github.com/sass/dart-sass/releases" "$reset"
3138 '';
3239 };
3340 };
@@ -6,28 +6,35 @@
6 outputs = { self, multiverse }:6 outputs = { self, multiverse }:
7 let7 let
8 system = "x86_64-linux";8 system = "x86_64-linux";
9- mv = multiverse.multiverse.${system};9+ pin = multiverse.multiverse.${system};
10- hugoVersion = "0.166.0";10+ pkgs = pin.tip;
11- hugo = mv.version "hugo" hugoVersion;11+ hugo = pin.version "hugo" "0.166.0";
12+ dart-sass = pin.version "dart-sass" "1.104.0";
12 in13 in
13 {14 {
14- packages.${system}.default = mv.tip.stdenvNoCC.mkDerivation {15+ packages.${system}.default = pkgs.stdenvNoCC.mkDerivation {
15 name = "mykiwi-blog";16 name = "mykiwi-blog";
16 src = ./blog;17 src = ./blog;
17- nativeBuildInputs = [ hugo ];18+ nativeBuildInputs = [ hugo dart-sass ];
18- buildPhase = "hugo --minify --destination $out";19+ buildPhase = ''
20+ HUGO_PARAMS_NIXSTOREPATH="$out" hugo --minify --destination "$out"
21+ '';
19 dontInstall = true;22 dontInstall = true;
20 };23 };
21 24
22- devShells.${system}.default = mv.tip.mkShell {25+ devShells.${system}.default = pkgs.mkShell {
23 name = "mykiwi-blog";26 name = "mykiwi-blog";
24 27
25 packages = [28 packages = [
26 hugo29 hugo
30+ dart-sass
27 ];31 ];
28 32
29 shellHook = ''33 shellHook = ''
30- echo "Hugo $(hugo version)"34+ dim=$'\033[2m'
35+ reset=$'\033[0m'
36+ printf '%-11s%-9s%s%s%s\n' "Hugo" "$(hugo version | sed -n 's/.*v\([0-9.]*\)+.*/\1/p')" "$dim" "https://github.com/gohugoio/hugo/releases" "$reset"
37+ printf '%-11s%-9s%s%s%s\n' "Dart Sass" "$(sass --version)" "$dim" "https://github.com/sass/dart-sass/releases" "$reset"
31 '';38 '';
32 };39 };
33 };40 };