shoreditch 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,991 @@
1
+ /*
2
+ * Shoreditch — a two-column theme for technical blogging.
3
+ *
4
+ * Shipped as a static file through the gem's source manifest, so consuming
5
+ * sites need no esbuild wiring. Everything themeable is a custom property on
6
+ * :root; a site can override any of them from its own stylesheet, which loads
7
+ * after this one.
8
+ *
9
+ * Adapted from Hyde by Mark Otto. The two-column shape is the inheritance;
10
+ * the implementation is not.
11
+ */
12
+
13
+ :root {
14
+ /* Overridden per-site by the accent option in config/initializers.rb. */
15
+ --sd-accent: #4c7a9c;
16
+
17
+ --sd-sidebar-width: 18rem;
18
+ --sd-measure: 46rem;
19
+ --sd-gutter: clamp(1.25rem, 4vw, 3rem);
20
+ --sd-radius: 0.375rem;
21
+
22
+ --sd-font-body: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
23
+ "Helvetica Neue", Arial, sans-serif;
24
+ --sd-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
25
+ "Liberation Mono", monospace;
26
+
27
+ /* Light is the default; the dark block below flips these. */
28
+ --sd-bg: #ffffff;
29
+ --sd-fg: #1c1f23;
30
+ --sd-fg-muted: #5b6470;
31
+ --sd-rule: #e3e6ea;
32
+ /* A hairline separator can afford to be almost invisible; a border drawn
33
+ around something, so that it reads as one object, cannot. Both weights are
34
+ tuned to land at roughly the same contrast in either scheme. */
35
+ --sd-rule-strong: #c9ced6;
36
+ --sd-code-bg: #f5f6f8;
37
+ --sd-sidebar-bg: #1c1f23;
38
+ --sd-sidebar-fg: #eceff3;
39
+ --sd-sidebar-fg-muted: #99a2ad;
40
+
41
+ /* Syntax highlighting. The theme owns the code background, so it has to own
42
+ the foreground too: a site shipping a light-only Rouge palette (which is
43
+ what `bridgetown new` gives you) renders keywords black on near-black the
44
+ moment dark mode flips the background. */
45
+ --sd-code-comment: #6a737d;
46
+ --sd-code-keyword: #d73a49;
47
+ --sd-code-string: #032f62;
48
+ --sd-code-number: #005cc5;
49
+ --sd-code-fn: #6f42c1;
50
+ --sd-code-tag: #22863a;
51
+ --sd-code-punct: #57606a;
52
+
53
+ color-scheme: light dark;
54
+ }
55
+
56
+ @media (prefers-color-scheme: dark) {
57
+ :root {
58
+ --sd-bg: #16181c;
59
+ --sd-fg: #e6e9ee;
60
+ --sd-fg-muted: #98a1ad;
61
+ --sd-rule: #2a2e35;
62
+ --sd-rule-strong: #363c45;
63
+ --sd-code-bg: #1e2126;
64
+ --sd-sidebar-bg: #101216;
65
+ --sd-sidebar-fg: #e6e9ee;
66
+ --sd-sidebar-fg-muted: #8d96a2;
67
+
68
+ --sd-code-comment: #8b949e;
69
+ --sd-code-keyword: #ff7b72;
70
+ --sd-code-string: #a5d6ff;
71
+ --sd-code-number: #79c0ff;
72
+ --sd-code-fn: #d2a8ff;
73
+ --sd-code-tag: #7ee787;
74
+ --sd-code-punct: #a8b3c2;
75
+ }
76
+ }
77
+
78
+ /* The viewer's explicit choice wins over the OS preference, both ways. */
79
+ :root[data-theme="light"] {
80
+ --sd-bg: #ffffff;
81
+ --sd-fg: #1c1f23;
82
+ --sd-fg-muted: #5b6470;
83
+ --sd-rule: #e3e6ea;
84
+ --sd-rule-strong: #c9ced6;
85
+ --sd-code-bg: #f5f6f8;
86
+ --sd-sidebar-bg: #1c1f23;
87
+ --sd-sidebar-fg: #eceff3;
88
+ --sd-sidebar-fg-muted: #99a2ad;
89
+
90
+ --sd-code-comment: #6a737d;
91
+ --sd-code-keyword: #d73a49;
92
+ --sd-code-string: #032f62;
93
+ --sd-code-number: #005cc5;
94
+ --sd-code-fn: #6f42c1;
95
+ --sd-code-tag: #22863a;
96
+ --sd-code-punct: #57606a;
97
+ }
98
+
99
+ :root[data-theme="dark"] {
100
+ --sd-bg: #16181c;
101
+ --sd-fg: #e6e9ee;
102
+ --sd-fg-muted: #98a1ad;
103
+ --sd-rule: #2a2e35;
104
+ --sd-rule-strong: #363c45;
105
+ --sd-code-bg: #1e2126;
106
+ --sd-sidebar-bg: #101216;
107
+ --sd-sidebar-fg: #e6e9ee;
108
+ --sd-sidebar-fg-muted: #8d96a2;
109
+
110
+ --sd-code-comment: #8b949e;
111
+ --sd-code-keyword: #ff7b72;
112
+ --sd-code-string: #a5d6ff;
113
+ --sd-code-number: #79c0ff;
114
+ --sd-code-fn: #d2a8ff;
115
+ --sd-code-tag: #7ee787;
116
+ --sd-code-punct: #a8b3c2;
117
+ }
118
+
119
+ *,
120
+ *::before,
121
+ *::after {
122
+ box-sizing: border-box;
123
+ }
124
+
125
+ body {
126
+ margin: 0;
127
+ background: var(--sd-bg);
128
+ color: var(--sd-fg);
129
+ font-family: var(--sd-font-body);
130
+ font-size: 1.0625rem;
131
+ line-height: 1.65;
132
+ -webkit-text-size-adjust: 100%;
133
+ }
134
+
135
+ a {
136
+ color: var(--sd-accent);
137
+ text-decoration-thickness: 1px;
138
+ text-underline-offset: 0.15em;
139
+ }
140
+
141
+ a:hover {
142
+ text-decoration-thickness: 2px;
143
+ }
144
+
145
+ :focus-visible {
146
+ outline: 2px solid var(--sd-accent);
147
+ outline-offset: 2px;
148
+ }
149
+
150
+ img {
151
+ max-width: 100%;
152
+ height: auto;
153
+ }
154
+
155
+ /* ---------------------------------------------------------------- layout */
156
+
157
+ .sd-shell {
158
+ display: grid;
159
+ grid-template-columns: 1fr;
160
+ min-height: 100vh;
161
+ }
162
+
163
+ @media (min-width: 60rem) {
164
+ .sd-shell {
165
+ grid-template-columns: var(--sd-sidebar-width) minmax(0, 1fr);
166
+ }
167
+
168
+ /* sidebar_side: "right" swaps the columns without touching the markup. */
169
+ .sd-shell[data-sidebar="right"] {
170
+ grid-template-columns: minmax(0, 1fr) var(--sd-sidebar-width);
171
+ }
172
+
173
+ .sd-shell[data-sidebar="right"] > .sd-sidebar {
174
+ order: 2;
175
+ }
176
+ }
177
+
178
+ /* --------------------------------------------------------------- sidebar */
179
+
180
+ .sd-sidebar {
181
+ display: flex;
182
+ flex-direction: column;
183
+ gap: 1.5rem;
184
+ padding: var(--sd-gutter);
185
+ background: var(--sd-sidebar-bg);
186
+ color: var(--sd-sidebar-fg);
187
+ }
188
+
189
+ @media (min-width: 60rem) {
190
+ .sd-sidebar {
191
+ position: sticky;
192
+ top: 0;
193
+ height: 100vh;
194
+ overflow-y: auto;
195
+ }
196
+ }
197
+
198
+ /* The navigation and the footer sit at the foot of the column, leaving the
199
+ logo and site title at the top. Grouped so a single auto margin moves both:
200
+ an auto margin on each would share the free space between them and leave the
201
+ nav floating in the middle. Below the breakpoint the sidebar is a band above
202
+ the content with no spare height, so there is nothing to push against and
203
+ the group simply follows the identity. */
204
+ .sd-sidebar-bottom {
205
+ display: flex;
206
+ flex-direction: column;
207
+ gap: 1.5rem;
208
+ }
209
+
210
+ @media (min-width: 60rem) {
211
+ .sd-sidebar-bottom {
212
+ margin-top: auto;
213
+ }
214
+ }
215
+
216
+ .sd-sidebar a {
217
+ color: inherit;
218
+ }
219
+
220
+ .sd-logo-area {
221
+ display: flex;
222
+ flex-direction: column;
223
+ align-items: center;
224
+ gap: 0.75rem;
225
+ text-align: center;
226
+ }
227
+
228
+ .sd-logo {
229
+ width: 8rem;
230
+ height: 8rem;
231
+ object-fit: cover;
232
+ border: 3px solid var(--sd-accent);
233
+ }
234
+
235
+ .sd-logo[data-shape="round"] {
236
+ border-radius: 50%;
237
+ }
238
+
239
+ .sd-logo[data-shape="square"] {
240
+ border-radius: var(--sd-radius);
241
+ }
242
+
243
+ /* `flashy_logo: true` in front matter. The Jekyll version stacked four red
244
+ drop-shadows and pulsed them; this keeps the pulse but glows in the site's
245
+ own accent, so it reads as deliberate rather than as an alert. */
246
+ .sd-logo[data-flashy="true"] {
247
+ animation: sd-logo-pulse 2.4s ease-in-out infinite;
248
+ }
249
+
250
+ @keyframes sd-logo-pulse {
251
+ 0%,
252
+ 100% {
253
+ filter: drop-shadow(0 0 3px var(--sd-accent));
254
+ }
255
+ 50% {
256
+ filter: drop-shadow(0 0 8px var(--sd-accent))
257
+ drop-shadow(0 0 18px var(--sd-accent));
258
+ }
259
+ }
260
+
261
+ /* The "AVAILABLE FOR HIRE" badge. */
262
+ .sd-logo-legend {
263
+ padding: 0.2rem 0.6rem;
264
+ border-radius: var(--sd-radius);
265
+ background: var(--sd-accent);
266
+ color: #fff;
267
+ font-size: 0.7rem;
268
+ font-weight: 700;
269
+ letter-spacing: 0.08em;
270
+ text-transform: uppercase;
271
+ }
272
+
273
+ /* Shaped independently of the logo, as `logo_legend_shape` did in 0.9.0. */
274
+ .sd-logo-legend[data-shape="round"] {
275
+ border-radius: 999px;
276
+ }
277
+
278
+ .sd-logo-legend[data-shape="straight"] {
279
+ border-radius: 0;
280
+ }
281
+
282
+ .sd-site-title {
283
+ margin: 0;
284
+ font-size: 1.5rem;
285
+ line-height: 1.2;
286
+ }
287
+
288
+ .sd-site-tagline {
289
+ margin: 0;
290
+ color: var(--sd-sidebar-fg-muted);
291
+ font-size: 0.9rem;
292
+ }
293
+
294
+ .sd-nav {
295
+ display: flex;
296
+ flex-wrap: wrap;
297
+ gap: 0.25rem 1rem;
298
+ }
299
+
300
+ @media (min-width: 60rem) {
301
+ .sd-nav {
302
+ flex-direction: column;
303
+ gap: 0.25rem;
304
+ }
305
+ }
306
+
307
+ .sd-nav a {
308
+ padding: 0.15rem 0;
309
+ text-decoration: none;
310
+ }
311
+
312
+ .sd-nav a:hover,
313
+ .sd-nav a[aria-current="page"] {
314
+ color: var(--sd-accent);
315
+ text-decoration: underline;
316
+ }
317
+
318
+ /* ------------------------------------------------- contact panel (details) */
319
+
320
+ .sd-details {
321
+ margin: 0;
322
+ padding: 0;
323
+ list-style: none;
324
+ display: flex;
325
+ flex-direction: column;
326
+ gap: 0.35rem;
327
+ font-size: 0.9rem;
328
+ overflow-wrap: anywhere;
329
+ }
330
+
331
+ .sd-details a {
332
+ display: flex;
333
+ align-items: center;
334
+ gap: 0.55rem;
335
+ color: var(--sd-sidebar-fg-muted);
336
+ text-decoration: none;
337
+ }
338
+
339
+ .sd-details a:hover {
340
+ color: var(--sd-sidebar-fg);
341
+ text-decoration: underline;
342
+ }
343
+
344
+ .sd-icon {
345
+ width: 1.05rem;
346
+ height: 1.05rem;
347
+ flex: none;
348
+ fill: currentColor;
349
+ }
350
+
351
+ /* Each network glows in its own colour on hover — the one flourish the Jekyll
352
+ theme kept in the sidebar. The glow sits on the icon only, never the text,
353
+ so contrast on the label is untouched. */
354
+ .sd-details a:hover .sd-icon {
355
+ color: var(--sd-brand, currentColor);
356
+ filter: drop-shadow(0 0 4px var(--sd-brand, transparent));
357
+ }
358
+
359
+ .sd-details li[data-network="github"] { --sd-brand: #8b6bc4; }
360
+ .sd-details li[data-network="linkedin"] { --sd-brand: #0a86c8; }
361
+ .sd-details li[data-network="mastodon"] { --sd-brand: #6364ff; }
362
+ .sd-details li[data-network="x"] { --sd-brand: #d0d6dd; }
363
+ .sd-details li[data-network="instagram"] { --sd-brand: #6b7ef0; }
364
+ .sd-details li[data-network="reddit"] { --sd-brand: #ff5722; }
365
+ .sd-details li[data-network="youtube"] { --sd-brand: #ff3d3d; }
366
+ .sd-details li[data-network="tiktok"] { --sd-brand: #ff3b73; }
367
+ .sd-details li[data-network="lastfm"] { --sd-brand: #e8362c; }
368
+ .sd-details li[data-network="deviantart"] { --sd-brand: #23d67e; }
369
+ .sd-details li[data-network="artstation"] { --sd-brand: #35b5f0; }
370
+ .sd-details li[data-network="phone"],
371
+ .sd-details li[data-network="email"],
372
+ .sd-details li[data-network="file"] { --sd-brand: var(--sd-accent); }
373
+
374
+ @media (prefers-reduced-motion: reduce) {
375
+ .sd-logo[data-flashy="true"] {
376
+ animation: none;
377
+ filter: drop-shadow(0 0 6px var(--sd-accent));
378
+ }
379
+ }
380
+
381
+ .sd-sidebar-footer {
382
+ color: var(--sd-sidebar-fg-muted);
383
+ font-size: 0.8rem;
384
+ }
385
+
386
+ .sd-theme-toggle {
387
+ padding: 0.35rem 0.7rem;
388
+ border: 1px solid currentColor;
389
+ border-radius: var(--sd-radius);
390
+ background: none;
391
+ color: inherit;
392
+ font: inherit;
393
+ font-size: 0.8rem;
394
+ cursor: pointer;
395
+ }
396
+
397
+ /* Below the breakpoint the sidebar is not a column, it is a header band — and
398
+ it had never been designed for that axis. Every part kept the size it takes
399
+ in an 18rem column with 100vh to spend, so on a 390px phone the masthead,
400
+ nav, toggle and copyright took 439px before the article, putting the title
401
+ at 80% of the first screen and the copyright notice above the headline.
402
+ The parts are promoted to items of the shell so identity and navigation can
403
+ form a compact bar above the content while the toggle and copyright drop
404
+ below it, where a footer belongs. Someone arriving on a post from a search
405
+ result should meet the masthead, not the small print. */
406
+ @media (max-width: 59.99rem) {
407
+ .sd-shell {
408
+ display: flex;
409
+ flex-direction: column;
410
+ }
411
+
412
+ .sd-sidebar,
413
+ .sd-sidebar-bottom {
414
+ display: contents;
415
+ }
416
+
417
+ /* Promoting the parts discards the sidebar's own box, so each one carries
418
+ the background and inline padding it used to provide. They butt into one
419
+ another into a single band because the shell has no gap. */
420
+ .sd-logo-area,
421
+ .sd-details,
422
+ .sd-nav,
423
+ .sd-sidebar-footer {
424
+ background: var(--sd-sidebar-bg);
425
+ color: var(--sd-sidebar-fg);
426
+ padding-inline: var(--sd-gutter);
427
+ }
428
+
429
+ .sd-logo-area {
430
+ flex-direction: row;
431
+ flex-wrap: wrap;
432
+ align-items: center;
433
+ gap: 0.4rem 0.9rem;
434
+ padding-top: 0.9rem;
435
+ text-align: start;
436
+ }
437
+
438
+ /* The title and tagline take what is left of the row, which pushes the badge
439
+ to the end of it — where it reads as a status pill rather than a caption
440
+ under the logo. */
441
+ .sd-site-identity {
442
+ flex: 1 1 10rem;
443
+ }
444
+
445
+ .sd-logo-legend {
446
+ order: 1;
447
+ }
448
+
449
+ .sd-logo {
450
+ width: 3rem;
451
+ height: 3rem;
452
+ border-width: 2px;
453
+ }
454
+
455
+ .sd-site-title {
456
+ font-size: 1.15rem;
457
+ }
458
+
459
+ .sd-site-tagline {
460
+ font-size: 0.8rem;
461
+ }
462
+
463
+ .sd-details {
464
+ padding-top: 0.9rem;
465
+ }
466
+
467
+ .sd-nav {
468
+ padding-block: 0.7rem 0.9rem;
469
+ }
470
+
471
+ /* Grows to take the slack, so a short page still leaves the footer at the
472
+ bottom rather than stranded mid-screen. */
473
+ .sd-main {
474
+ flex: 1;
475
+ }
476
+
477
+ /* Everything else keeps the default order of 0, so this alone lands after
478
+ the content. */
479
+ .sd-sidebar-footer {
480
+ order: 1;
481
+ display: flex;
482
+ flex-wrap: wrap;
483
+ align-items: center;
484
+ justify-content: space-between;
485
+ gap: 0.75rem;
486
+ padding-block: 1.1rem;
487
+ }
488
+
489
+ .sd-sidebar-footer p {
490
+ margin: 0;
491
+ }
492
+ }
493
+
494
+ /* --------------------------------------------------------------- content */
495
+
496
+ .sd-main {
497
+ display: flex;
498
+ flex-direction: column;
499
+ min-width: 0;
500
+ }
501
+
502
+ .sd-cover {
503
+ width: 100%;
504
+ max-height: 22rem;
505
+ object-fit: cover;
506
+ }
507
+
508
+ .sd-content {
509
+ width: 100%;
510
+ /* Wider than a typical prose measure on purpose: code samples need to fit
511
+ 80 characters without wrapping. */
512
+ max-width: var(--sd-measure);
513
+ padding: var(--sd-gutter);
514
+ margin-inline: auto;
515
+ }
516
+
517
+ .sd-content h1,
518
+ .sd-content h2,
519
+ .sd-content h3 {
520
+ line-height: 1.25;
521
+ text-wrap: balance;
522
+ }
523
+
524
+ .sd-content h1 {
525
+ font-size: clamp(1.75rem, 4vw, 2.5rem);
526
+ }
527
+
528
+ /* ----------------------------------------------------------------- posts */
529
+
530
+ .sd-post-header {
531
+ margin-bottom: 2rem;
532
+ padding-bottom: 1rem;
533
+ border-bottom: 1px solid var(--sd-rule);
534
+ }
535
+
536
+ .sd-post-title {
537
+ margin: 0 0 0.5rem;
538
+ }
539
+
540
+ .sd-post-dates {
541
+ display: flex;
542
+ flex-wrap: wrap;
543
+ gap: 0.35rem 1rem;
544
+ margin: 0;
545
+ color: var(--sd-fg-muted);
546
+ font-size: 0.85rem;
547
+ }
548
+
549
+ /* An index entry is a framed object, not a row in a feed. The frame earns its
550
+ place: a post with no `thumbnail` started its title at the content edge
551
+ while its neighbours started an image-column in, so the stack had no shared
552
+ left edge and read as ragged. The border supplies one edge every entry
553
+ shares, whether or not it has an image. 0.9.0's listing was bordered for the
554
+ same reason — this restores that, clipping the excerpt at a line boundary
555
+ rather than mid-word as that version did. */
556
+ .sd-post-summary {
557
+ display: grid;
558
+ overflow: hidden;
559
+ border: 1px solid var(--sd-rule-strong);
560
+ border-radius: var(--sd-radius);
561
+ transition: border-color 0.15s ease;
562
+ }
563
+
564
+ .sd-post-summary + .sd-post-summary {
565
+ margin-top: 1.25rem;
566
+ }
567
+
568
+ /* The same border-to-accent move the tags and the pager already make. */
569
+ .sd-post-summary:hover,
570
+ .sd-post-summary:focus-within {
571
+ border-color: var(--sd-accent);
572
+ }
573
+
574
+ /* The plate is an edge of the card, not a picture sitting inside it: flush to
575
+ the border, no radius of its own (the frame's overflow clips it), and
576
+ stretched to whatever height the text sets. */
577
+ .sd-post-thumbnail {
578
+ display: block;
579
+ aspect-ratio: 16 / 7;
580
+ }
581
+
582
+ .sd-post-thumbnail img {
583
+ display: block;
584
+ width: 100%;
585
+ height: 100%;
586
+ object-fit: cover;
587
+ }
588
+
589
+ /* The thumbnail column is reserved only when there is actually a thumbnail to
590
+ put in it. Gating on :has matters: without it a summary for a post with no
591
+ `thumbnail` in its front matter still had its text auto-placed into the
592
+ image column, squeezing every title and excerpt into a narrow ribbon. */
593
+ @media (min-width: 34rem) {
594
+ .sd-post-summary:has(.sd-post-thumbnail) {
595
+ grid-template-columns: minmax(0, 11rem) minmax(0, 1fr);
596
+ }
597
+
598
+ .sd-post-thumbnail {
599
+ height: 100%;
600
+ min-height: 9rem;
601
+ aspect-ratio: auto;
602
+ }
603
+ }
604
+
605
+ .sd-post-detail {
606
+ min-width: 0;
607
+ padding: 1.15rem 1.4rem;
608
+ }
609
+
610
+ /* Promoted above the title and set in the mono face. The date is not a
611
+ footnote on a technical blog — a listing here spans 2012 to 2026, where
612
+ "how stale is this?" is the first thing a reader wants to know — and mono is
613
+ already the theme's signal for data rather than prose. */
614
+ .sd-post-summary .sd-post-dates {
615
+ margin-bottom: 0.3rem;
616
+ font-family: var(--sd-font-mono);
617
+ font-size: 0.7rem;
618
+ letter-spacing: 0.06em;
619
+ text-transform: uppercase;
620
+ }
621
+
622
+ .sd-post-summary h2 {
623
+ margin: 0 0 0.4rem;
624
+ font-size: 1.25rem;
625
+ line-height: 1.25;
626
+ text-wrap: balance;
627
+ }
628
+
629
+ .sd-post-summary h2 a {
630
+ text-decoration: none;
631
+ }
632
+
633
+ .sd-post-summary h2 a:hover {
634
+ text-decoration: underline;
635
+ }
636
+
637
+ .sd-post-excerpt {
638
+ display: -webkit-box;
639
+ -webkit-box-orient: vertical;
640
+ -webkit-line-clamp: 3;
641
+ line-clamp: 3;
642
+ overflow: hidden;
643
+ color: var(--sd-fg-muted);
644
+ font-size: 0.95rem;
645
+ }
646
+
647
+ /* An excerpt is a preview, so it is set as plain text whatever block the post
648
+ happens to open with. Without this a post starting with {:.message} or a
649
+ blockquote put a filled, accent-barred box inside the card while its
650
+ neighbours stayed plain. Scoped through .sd-content to outrank the prose
651
+ rules further down the file rather than depending on source order. */
652
+ .sd-content .sd-post-excerpt :is(p, blockquote, .message, .callout, .alert, .alarm) {
653
+ margin: 0;
654
+ padding: 0;
655
+ border: 0;
656
+ border-radius: 0;
657
+ background: none;
658
+ color: inherit;
659
+ font-size: inherit;
660
+ font-style: inherit;
661
+ }
662
+
663
+ /* Flattening zeroes every margin, so an excerpt taken from two paragraphs by
664
+ an explicit <!--more--> would run them together. The selector repeats the
665
+ :is() list to match the specificity of the rule above — :is() takes the
666
+ weight of its most specific argument, so that rule is (0,3,0), not (0,2,0)
667
+ — and sits after it to win the tie. */
668
+ .sd-content .sd-post-excerpt * + :is(p, blockquote, .message, .callout, .alert, .alarm) {
669
+ margin-top: 0.5rem;
670
+ }
671
+
672
+ /* ------------------------------------------------------------------ tags */
673
+
674
+ .sd-tags {
675
+ display: flex;
676
+ flex-wrap: wrap;
677
+ gap: 0.4rem;
678
+ margin-block: 2rem;
679
+ }
680
+
681
+ .sd-tag {
682
+ padding: 0.15rem 0.6rem;
683
+ border: 1px solid var(--sd-rule);
684
+ border-radius: 999px;
685
+ color: var(--sd-fg-muted);
686
+ font-size: 0.8rem;
687
+ text-decoration: none;
688
+ }
689
+
690
+ .sd-tag:hover {
691
+ border-color: var(--sd-accent);
692
+ color: var(--sd-accent);
693
+ }
694
+
695
+ /* ------------------------------------------------------- related/credits */
696
+
697
+ .sd-related {
698
+ margin-block: 2.5rem;
699
+ padding-top: 1.5rem;
700
+ border-top: 1px solid var(--sd-rule);
701
+ }
702
+
703
+ .sd-related h2 {
704
+ font-size: 1.1rem;
705
+ }
706
+
707
+ .sd-related ul {
708
+ padding: 0;
709
+ margin: 0;
710
+ list-style: none;
711
+ }
712
+
713
+ .sd-related li {
714
+ padding-block: 0.4rem;
715
+ }
716
+
717
+ .sd-related time {
718
+ display: block;
719
+ color: var(--sd-fg-muted);
720
+ font-size: 0.8rem;
721
+ }
722
+
723
+ .sd-credits {
724
+ color: var(--sd-fg-muted);
725
+ font-size: 0.8rem;
726
+ }
727
+
728
+ /* -------------------------------------------------------------- comments */
729
+
730
+ .sd-comments {
731
+ margin-block-start: 3rem;
732
+ padding-block-start: 1.5rem;
733
+ border-top: 1px solid var(--sd-rule);
734
+ }
735
+
736
+ .sd-comments h2 {
737
+ margin-block-start: 0;
738
+ font-size: 1.15rem;
739
+ }
740
+
741
+ /* ------------------------------------------------------------------- cv */
742
+
743
+ /* Opted into with `class: cv` in front matter. A CV is a dense document of
744
+ roles and dates, so headings get tighter and the role/tenure pair below each
745
+ one is set as a unit rather than as two loose paragraphs. */
746
+ .cv .sd-content h1 {
747
+ margin-block: 2.5rem 1rem;
748
+ padding-block-end: 0.35rem;
749
+ border-bottom: 1px solid var(--sd-rule);
750
+ font-size: 1.5rem;
751
+ }
752
+
753
+ .cv .sd-content h1:first-child {
754
+ margin-block-start: 0;
755
+ }
756
+
757
+ .cv .sd-content .position {
758
+ margin-block-end: 0;
759
+ font-weight: 650;
760
+ }
761
+
762
+ .cv .sd-content .tenure {
763
+ margin-block: 0 0.75rem;
764
+ color: var(--sd-fg-muted);
765
+ font-size: 0.85rem;
766
+ font-variant-numeric: tabular-nums;
767
+ }
768
+
769
+ /* --------------------------------------------------------- post archive */
770
+
771
+ .sd-archive {
772
+ margin-block: 0 2rem;
773
+ padding: 0;
774
+ list-style: none;
775
+ }
776
+
777
+ .sd-archive li {
778
+ display: flex;
779
+ flex-wrap: wrap;
780
+ align-items: baseline;
781
+ justify-content: space-between;
782
+ gap: 0.5rem 1rem;
783
+ padding-block: 0.5rem;
784
+ border-bottom: 1px solid var(--sd-rule);
785
+ }
786
+
787
+ .sd-archive time {
788
+ color: var(--sd-fg-muted);
789
+ font-size: 0.85rem;
790
+ font-variant-numeric: tabular-nums;
791
+ }
792
+
793
+ /* ------------------------------------------------------------ pagination */
794
+
795
+ .sd-pagination {
796
+ display: flex;
797
+ justify-content: space-between;
798
+ gap: 1rem;
799
+ margin-block: 2.5rem;
800
+ }
801
+
802
+ .sd-pagination a {
803
+ padding: 0.4rem 0.9rem;
804
+ border: 1px solid var(--sd-rule);
805
+ border-radius: var(--sd-radius);
806
+ text-decoration: none;
807
+ }
808
+
809
+ .sd-pagination a:hover {
810
+ border-color: var(--sd-accent);
811
+ }
812
+
813
+ /* Keeps "Older" hard right when "Newer" is absent on page 1. */
814
+ .sd-pagination a.sd-older:only-child {
815
+ margin-inline-start: auto;
816
+ }
817
+
818
+ /* ------------------------------------------------------ prose and code */
819
+
820
+ .sd-content :is(pre, .highlight) {
821
+ padding: 1rem;
822
+ border-radius: var(--sd-radius);
823
+ background: var(--sd-code-bg);
824
+ overflow-x: auto;
825
+ }
826
+
827
+ /* Rouge nests pre.highlight inside div.highlight and both match the rule
828
+ above, so the inner element padded, rounded and filled a second time. */
829
+ .sd-content .highlight > pre.highlight {
830
+ padding: 0;
831
+ border-radius: 0;
832
+ background: none;
833
+ }
834
+
835
+ /* A `{:.code-title}` paragraph immediately before a fence labels it — the
836
+ filename the snippet belongs to, usually. */
837
+ .sd-content .code-title {
838
+ margin-bottom: 0;
839
+ padding: 0.4rem 0.75rem;
840
+ border-radius: var(--sd-radius) var(--sd-radius) 0 0;
841
+ background: var(--sd-code-bg);
842
+ border-bottom: 1px solid var(--sd-rule);
843
+ font-family: var(--sd-font-mono);
844
+ font-size: 0.8rem;
845
+ color: var(--sd-fg-muted);
846
+ }
847
+
848
+ .sd-content .code-title + :is(pre, .highlight) {
849
+ margin-top: 0;
850
+ border-start-start-radius: 0;
851
+ border-start-end-radius: 0;
852
+ }
853
+
854
+ /* Rouge token colours, mapped onto the theme's tokens so they follow the
855
+ light/dark switch. Grouped by what they mean rather than by Rouge's names. */
856
+ .sd-content .highlight :is(.c, .cm, .c1, .cs, .cp, .cd) {
857
+ color: var(--sd-code-comment);
858
+ font-style: italic;
859
+ }
860
+
861
+ .sd-content .highlight :is(.k, .kc, .kd, .kn, .kp, .kr, .kt, .o, .ow) {
862
+ color: var(--sd-code-keyword);
863
+ }
864
+
865
+ .sd-content .highlight :is(.s, .s1, .s2, .sb, .sc, .sd, .se, .sh, .si, .sx, .sr, .ss) {
866
+ color: var(--sd-code-string);
867
+ }
868
+
869
+ .sd-content .highlight :is(.m, .mf, .mh, .mi, .mo, .il, .no, .nb, .bp) {
870
+ color: var(--sd-code-number);
871
+ }
872
+
873
+ .sd-content .highlight :is(.nf, .nc, .ne, .nd) {
874
+ color: var(--sd-code-fn);
875
+ }
876
+
877
+ .sd-content .highlight :is(.nt, .na, .nn) {
878
+ color: var(--sd-code-tag);
879
+ }
880
+
881
+ .sd-content .highlight :is(.p, .nv, .vc, .vg, .vi, .nl, .ni) {
882
+ color: var(--sd-code-punct);
883
+ }
884
+
885
+ .sd-content .highlight .err {
886
+ color: var(--sd-code-keyword);
887
+ background: none;
888
+ }
889
+
890
+ /* A pull quote, set apart from the prose rather than inline like a blockquote. */
891
+ .sd-content .pullquote {
892
+ margin: 2rem 0;
893
+ padding: 1.25rem 1.5rem;
894
+ border-radius: var(--sd-radius);
895
+ background: var(--sd-code-bg);
896
+ border-inline-start: 3px solid var(--sd-accent);
897
+ }
898
+
899
+ .sd-content .pullquote .quotation {
900
+ font-size: 1.15rem;
901
+ line-height: 1.45;
902
+ text-wrap: pretty;
903
+ }
904
+
905
+ .sd-content .pullquote .attribution {
906
+ margin-top: 0.5rem;
907
+ font-size: 0.85rem;
908
+ color: var(--sd-fg-muted);
909
+ }
910
+
911
+ .sd-content .pullquote .attribution::before {
912
+ content: "— ";
913
+ }
914
+
915
+ .sd-content :not(pre) > code {
916
+ padding: 0.1em 0.35em;
917
+ border-radius: 0.25em;
918
+ background: var(--sd-code-bg);
919
+ font-size: 0.9em;
920
+ }
921
+
922
+ .sd-content code {
923
+ font-family: var(--sd-font-mono);
924
+ }
925
+
926
+ .sd-content blockquote {
927
+ padding-inline-start: 1rem;
928
+ margin-inline: 0;
929
+ border-inline-start: 3px solid var(--sd-accent);
930
+ color: var(--sd-fg-muted);
931
+ }
932
+
933
+ /* Tables can be wider than the measure — scroll them, never the page. */
934
+ .sd-content .sd-table-wrap {
935
+ overflow-x: auto;
936
+ }
937
+
938
+ .sd-content table {
939
+ border-collapse: collapse;
940
+ width: 100%;
941
+ }
942
+
943
+ .sd-content :is(th, td) {
944
+ padding: 0.4rem 0.7rem;
945
+ border: 1px solid var(--sd-rule);
946
+ text-align: start;
947
+ }
948
+
949
+ /* The message/callout/alert/alarm classes the Jekyll theme defined, kept so
950
+ existing posts using {:.message} and friends still render. */
951
+ .sd-content :is(.message, .callout, .alert, .alarm) {
952
+ padding: 0.75rem 1rem;
953
+ border-inline-start: 4px solid var(--sd-fg-muted);
954
+ border-radius: var(--sd-radius);
955
+ background: var(--sd-code-bg);
956
+ }
957
+
958
+ .sd-content .callout {
959
+ border-inline-start-color: var(--sd-accent);
960
+ }
961
+
962
+ .sd-content .alert {
963
+ border-inline-start-color: #d08b1e;
964
+ }
965
+
966
+ .sd-content .alarm {
967
+ border-inline-start-color: #c0392b;
968
+ }
969
+
970
+ .sd-skip-link {
971
+ position: absolute;
972
+ left: -9999px;
973
+ }
974
+
975
+ .sd-skip-link:focus {
976
+ left: 1rem;
977
+ top: 1rem;
978
+ z-index: 10;
979
+ padding: 0.5rem 1rem;
980
+ background: var(--sd-bg);
981
+ color: var(--sd-fg);
982
+ }
983
+
984
+ @media (prefers-reduced-motion: reduce) {
985
+ *,
986
+ *::before,
987
+ *::after {
988
+ animation-duration: 0.01ms !important;
989
+ transition-duration: 0.01ms !important;
990
+ }
991
+ }