@opentf/web-docs 0.1.0 → 0.2.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.
Files changed (43) hide show
  1. package/build/blog-posts-plugin.js +127 -0
  2. package/build/docs-nav-plugin.js +17 -10
  3. package/build/feed.js +78 -0
  4. package/build/index.js +4 -0
  5. package/build/last-updated-plugin.js +98 -0
  6. package/build/last-updated.js +45 -0
  7. package/build/pagefind.js +58 -0
  8. package/build/reading-time.js +16 -0
  9. package/components/BlogLayout.jsx +77 -0
  10. package/components/Breadcrumbs.jsx +30 -27
  11. package/components/Callout.jsx +7 -7
  12. package/components/Card.jsx +21 -0
  13. package/components/Cards.jsx +11 -0
  14. package/components/CodeBlock.jsx +39 -0
  15. package/components/DocsLayout.jsx +66 -6
  16. package/components/LastUpdated.jsx +22 -0
  17. package/components/NavIcon.jsx +34 -0
  18. package/components/Navbar.jsx +34 -22
  19. package/components/NavbarLink.jsx +33 -0
  20. package/components/Pagination.jsx +37 -33
  21. package/components/PostBanner.jsx +25 -0
  22. package/components/PostCard.jsx +19 -0
  23. package/components/PostList.jsx +17 -0
  24. package/components/PostMeta.jsx +28 -0
  25. package/components/ReadingTime.jsx +6 -0
  26. package/components/Search.jsx +211 -0
  27. package/components/SearchTrigger.jsx +14 -3
  28. package/components/Sidebar.jsx +111 -9
  29. package/components/SidebarNode.jsx +1 -0
  30. package/components/SidebarToggle.jsx +47 -0
  31. package/components/Steps.jsx +14 -0
  32. package/components/Tabs.jsx +4 -2
  33. package/components/ThemeToggle.jsx +150 -25
  34. package/components/Tooltip.jsx +19 -0
  35. package/components/format.js +11 -0
  36. package/config.js +25 -1
  37. package/index.js +29 -2
  38. package/nav-virtual.js +6 -4
  39. package/package.json +15 -1
  40. package/posts-virtual.js +17 -0
  41. package/theme/index.css +1034 -58
  42. package/updated-virtual.js +9 -0
  43. package/components/CodeGroup.jsx +0 -28
package/theme/index.css CHANGED
@@ -65,22 +65,33 @@ body {
65
65
  /* Custom elements are inline by default. Structural hosts become block-level grid
66
66
  * items; pure wrapper hosts (a link, a list node) become layout-transparent
67
67
  * (`display: contents`) so their inner semantic element participates directly in the
68
- * surrounding flex/grid/list. */
69
- web-docs-layout,
70
- web-navbar,
71
- web-sidebar,
72
- web-toc,
73
- web-footer,
74
- web-breadcrumbs,
75
- web-pagination,
76
- web-callout,
77
- web-tabs,
78
- web-code-group,
79
- web-table {
68
+ * surrounding flex/grid/list. Hosts are addressed by the stable `web-<kebab>` class
69
+ * the compiler stamps on every component (the element tag itself is module-hashed
70
+ * to stay collision-free, so it is not a reliable selector). */
71
+ .web-docs-layout,
72
+ .web-toc,
73
+ .web-footer,
74
+ .web-breadcrumbs,
75
+ .web-pagination,
76
+ .web-callout,
77
+ .web-tabs,
78
+ .web-code-group,
79
+ .web-table {
80
80
  display: block;
81
81
  }
82
- web-link,
83
- web-sidebar-node {
82
+ /* The navbar host must be transparent so its sticky <header> sticks to the page
83
+ * scroll container, not to its own wrapper. Link hosts are transparent so the inner
84
+ * <a> participates directly in the navbar/list flex layout. The sidebar host (and its
85
+ * shell wrapper) are transparent so the inner <aside> is the docs-grid column on
86
+ * desktop and a fixed off-canvas drawer on mobile; the burger host so the button sits
87
+ * directly in the navbar lead flow. */
88
+ .web-navbar,
89
+ .web-navbar-link,
90
+ .web-link,
91
+ .web-sidebar-node,
92
+ .web-sidebar,
93
+ .web-sidebar-toggle,
94
+ .otfw-sidebar-shell {
84
95
  display: contents;
85
96
  }
86
97
 
@@ -109,6 +120,19 @@ web-sidebar-node {
109
120
  max-width: 90rem;
110
121
  margin: 0 auto;
111
122
  padding: 0 1.5rem;
123
+ /* Three zones: brand (left) · search (centered) · nav + actions (right). The auto
124
+ * center column keeps the search trigger centered in the bar regardless of how wide
125
+ * the side zones are. */
126
+ display: grid;
127
+ grid-template-columns: 1fr auto 1fr;
128
+ align-items: center;
129
+ gap: 1.5rem;
130
+ }
131
+ .otfw-navbar-search {
132
+ justify-self: center;
133
+ }
134
+ .otfw-navbar-right {
135
+ justify-self: end;
112
136
  display: flex;
113
137
  align-items: center;
114
138
  gap: 1.5rem;
@@ -126,38 +150,256 @@ web-sidebar-node {
126
150
  width: 1.75rem;
127
151
  height: 1.75rem;
128
152
  }
153
+ .otfw-navbar-version {
154
+ display: inline-flex;
155
+ align-items: center;
156
+ line-height: 1;
157
+ padding: 0.22rem 0.45rem;
158
+ border-radius: 0.4rem;
159
+ border: 1px solid var(--otfw-border);
160
+ background: var(--otfw-bg-surface);
161
+ color: var(--otfw-text-muted);
162
+ font-size: 0.62rem;
163
+ font-weight: 700;
164
+ letter-spacing: 0.03em;
165
+ text-transform: uppercase;
166
+ }
167
+ /* Brand sits at the start of the left grid zone. */
168
+ .otfw-navbar-brand {
169
+ justify-self: start;
170
+ }
129
171
  .otfw-navbar-nav {
130
172
  display: flex;
131
- gap: 1.25rem;
132
- margin-right: auto;
173
+ align-items: center;
174
+ gap: 1.5rem;
133
175
  font-size: 0.9rem;
134
176
  }
135
177
  .otfw-navbar-link {
178
+ position: relative;
179
+ display: inline-flex;
180
+ align-items: center;
181
+ gap: 0.4rem;
136
182
  color: var(--otfw-text-muted);
137
183
  text-decoration: none;
138
184
  font-weight: 500;
139
185
  transition: color 0.15s;
140
186
  }
187
+ /* Subtle accent underline beneath the link; scales in on the active route (and
188
+ * faintly on hover). */
189
+ .otfw-navbar-link::after {
190
+ content: "";
191
+ position: absolute;
192
+ left: 0;
193
+ right: 0;
194
+ bottom: -0.45rem;
195
+ height: 2px;
196
+ border-radius: 2px;
197
+ background: var(--otfw-accent);
198
+ transform: scaleX(0);
199
+ transition: transform 0.18s ease, opacity 0.18s ease;
200
+ }
141
201
  .otfw-navbar-link:hover {
142
202
  color: var(--otfw-text);
143
203
  }
204
+ .otfw-navbar-link:hover::after {
205
+ transform: scaleX(1);
206
+ opacity: 0.35;
207
+ }
208
+ .otfw-navbar-link.otfw-active {
209
+ color: var(--otfw-text);
210
+ font-weight: 600;
211
+ }
212
+ .otfw-navbar-link.otfw-active::after {
213
+ transform: scaleX(1);
214
+ opacity: 1;
215
+ }
216
+ .otfw-nav-icon {
217
+ display: inline-flex;
218
+ }
219
+ .otfw-nav-icon svg {
220
+ display: block;
221
+ }
144
222
  .otfw-navbar-actions {
145
223
  display: flex;
146
224
  align-items: center;
147
225
  gap: 0.5rem;
148
226
  }
227
+ /* GitHub link and theme toggle are uniform ghost icon buttons (no border/fill, subtle
228
+ * hover background), so the whole right cluster (links + icons) reads as one aligned
229
+ * row. */
149
230
  .otfw-navbar-icon {
150
231
  display: inline-flex;
151
- padding: 0.4rem;
232
+ align-items: center;
233
+ justify-content: center;
234
+ width: 2rem;
235
+ height: 2rem;
236
+ padding: 0;
237
+ border: none;
238
+ background: none;
152
239
  border-radius: 0.5rem;
153
240
  color: var(--otfw-text-muted);
241
+ cursor: pointer;
242
+ font-family: inherit;
243
+ /* Icons compile to inline custom-element hosts with the inherited 1.5 line-height,
244
+ * which baseline-aligns the SVG and pushes it a few px above center. Zero the line
245
+ * box so the flex centering is exact. */
246
+ line-height: 0;
247
+ }
248
+ .otfw-navbar-icon > * {
249
+ display: inline-flex;
250
+ align-items: center;
154
251
  }
155
252
  .otfw-navbar-icon:hover {
156
253
  color: var(--otfw-text);
157
254
  background: var(--otfw-bg-surface);
158
255
  }
159
256
 
160
- .otfw-theme-toggle,
257
+ /* Left zone of the navbar: an optional mobile drawer burger followed by the brand. */
258
+ .otfw-navbar-lead {
259
+ justify-self: start;
260
+ display: flex;
261
+ align-items: center;
262
+ gap: 0.5rem;
263
+ min-width: 0;
264
+ }
265
+ /* The burger reuses `.otfw-navbar-icon` styling; this rule (after it, so it wins the
266
+ * display cascade) hides it except on small screens that actually have a sidebar
267
+ * mounted — the <Sidebar> sets `data-otfw-has-sidebar` on the root while present. */
268
+ .otfw-navbar-burger {
269
+ display: none;
270
+ }
271
+ @media (max-width: 767px) {
272
+ :root[data-otfw-has-sidebar] .otfw-navbar-burger {
273
+ display: inline-flex;
274
+ }
275
+ }
276
+
277
+ /* ── Navbar: mobile layout ─────────────────────────────────────────────────── */
278
+ /* Below the sidebar breakpoint the centered three-column grid gets cramped, so the bar
279
+ * becomes a simple flex row: brand (with burger) at the start, a compact action cluster
280
+ * (search · GitHub · theme) at the end. On pages that have a sidebar the top-level links
281
+ * move into the drawer (see `.otfw-drawer-links`), so they're dropped from the bar here. */
282
+ @media (max-width: 767px) {
283
+ .otfw-navbar-inner {
284
+ display: flex;
285
+ align-items: center;
286
+ gap: 0.5rem;
287
+ padding: 0 1rem;
288
+ }
289
+ .otfw-navbar-lead {
290
+ margin-right: auto; /* push the action cluster to the far end */
291
+ }
292
+ .otfw-navbar-search {
293
+ justify-self: auto;
294
+ }
295
+ .otfw-navbar-right {
296
+ gap: 0.5rem;
297
+ }
298
+ .otfw-navbar-version {
299
+ display: none; /* reclaim brand width on small screens */
300
+ }
301
+ :root[data-otfw-has-sidebar] .otfw-navbar-nav {
302
+ display: none; /* surfaced in the drawer instead */
303
+ }
304
+ }
305
+
306
+ /* Theme switcher: trigger reuses `.otfw-navbar-icon` so it matches the GitHub button;
307
+ * it just grows a little to fit the System-mode pair of icons, and anchors the menu. */
308
+ .otfw-theme {
309
+ position: relative;
310
+ display: inline-flex;
311
+ }
312
+ .otfw-theme-trigger {
313
+ width: auto;
314
+ min-width: 2rem;
315
+ padding: 0 0.3rem;
316
+ }
317
+ .otfw-theme-trigger-system {
318
+ display: inline-flex;
319
+ align-items: center;
320
+ gap: 0.3rem;
321
+ }
322
+ .otfw-theme-trigger-badge {
323
+ display: inline-flex;
324
+ align-items: center;
325
+ padding-left: 0.3rem;
326
+ border-left: 1px solid var(--otfw-border);
327
+ opacity: 0.85;
328
+ }
329
+ .otfw-theme-menu {
330
+ position: absolute;
331
+ top: calc(100% + 0.4rem);
332
+ right: 0;
333
+ z-index: 60;
334
+ min-width: 9rem;
335
+ padding: 0.3rem;
336
+ display: flex;
337
+ flex-direction: column;
338
+ gap: 0.1rem;
339
+ background: var(--otfw-bg-elevated);
340
+ border: 1px solid var(--otfw-border);
341
+ border-radius: 0.6rem;
342
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.18);
343
+ }
344
+ .otfw-theme-item {
345
+ display: flex;
346
+ width: 100%;
347
+ align-items: center;
348
+ gap: 0.55rem;
349
+ padding: 0.45rem 0.6rem;
350
+ border: none;
351
+ background: none;
352
+ border-radius: 0.4rem;
353
+ color: var(--otfw-text-muted);
354
+ font: inherit;
355
+ font-size: 0.85rem;
356
+ text-align: left;
357
+ cursor: pointer;
358
+ }
359
+ .otfw-theme-item:hover {
360
+ background: var(--otfw-bg-surface);
361
+ color: var(--otfw-text);
362
+ }
363
+ .otfw-theme-item.is-active {
364
+ color: var(--otfw-accent);
365
+ font-weight: 600;
366
+ }
367
+
368
+ /* Tooltip (hover/focus) — a small contrasting bubble. */
369
+ .otfw-tooltip {
370
+ position: relative;
371
+ display: inline-flex;
372
+ }
373
+ .otfw-tooltip-bubble {
374
+ position: absolute;
375
+ left: 50%;
376
+ transform: translateX(-50%) translateY(3px);
377
+ white-space: nowrap;
378
+ padding: 0.28rem 0.5rem;
379
+ border-radius: 0.4rem;
380
+ background: var(--otfw-text);
381
+ color: var(--otfw-bg);
382
+ font-size: 0.72rem;
383
+ font-weight: 500;
384
+ opacity: 0;
385
+ pointer-events: none;
386
+ transition: opacity 0.12s ease, transform 0.12s ease;
387
+ z-index: 70;
388
+ }
389
+ .otfw-tooltip-bottom .otfw-tooltip-bubble {
390
+ top: 100%;
391
+ margin-top: 0.45rem;
392
+ }
393
+ .otfw-tooltip-top .otfw-tooltip-bubble {
394
+ bottom: 100%;
395
+ margin-bottom: 0.45rem;
396
+ }
397
+ .otfw-tooltip:hover .otfw-tooltip-bubble,
398
+ .otfw-tooltip:focus-within .otfw-tooltip-bubble {
399
+ opacity: 1;
400
+ transform: translateX(-50%) translateY(0);
401
+ }
402
+
161
403
  .otfw-search-trigger {
162
404
  display: inline-flex;
163
405
  align-items: center;
@@ -168,25 +410,30 @@ web-sidebar-node {
168
410
  border-radius: 0.5rem;
169
411
  cursor: pointer;
170
412
  font-family: inherit;
413
+ padding: 0.4rem 0.65rem;
414
+ font-size: 0.82rem;
171
415
  }
172
- .otfw-theme-toggle {
173
- padding: 0.4rem;
174
- }
175
- .otfw-theme-toggle:hover,
176
416
  .otfw-search-trigger:hover {
177
417
  color: var(--otfw-accent);
178
418
  border-color: var(--otfw-accent);
179
419
  }
180
- .otfw-search-trigger {
181
- padding: 0.4rem 0.65rem;
182
- font-size: 0.82rem;
183
- }
184
420
  .otfw-search-kbd {
421
+ display: none;
422
+ align-items: center;
423
+ height: 1.25rem;
424
+ padding: 0 0.35rem;
185
425
  border: 1px solid var(--otfw-border);
186
426
  border-radius: 0.3rem;
187
- padding: 0 0.3rem;
188
- font-size: 0.7rem;
427
+ background: var(--otfw-bg);
428
+ color: var(--otfw-text-muted);
189
429
  font-family: var(--otfw-mono);
430
+ font-size: 0.68rem;
431
+ line-height: 1;
432
+ }
433
+ @media (min-width: 768px) {
434
+ .otfw-search-kbd {
435
+ display: inline-flex;
436
+ }
190
437
  }
191
438
  .otfw-search-label {
192
439
  display: none;
@@ -197,6 +444,147 @@ web-sidebar-node {
197
444
  }
198
445
  }
199
446
 
447
+ /* ── Search modal (Pagefind) ───────────────────────────────────────────────── */
448
+ .web-search {
449
+ display: contents;
450
+ }
451
+ .otfw-search-modal {
452
+ position: fixed;
453
+ inset: 0;
454
+ z-index: 100;
455
+ display: none;
456
+ align-items: flex-start;
457
+ justify-content: center;
458
+ padding: 12vh 1rem 1rem;
459
+ background: color-mix(in srgb, #000 45%, transparent);
460
+ backdrop-filter: blur(2px);
461
+ }
462
+ .otfw-search-modal.is-open {
463
+ display: flex;
464
+ }
465
+ .otfw-search-panel {
466
+ width: 100%;
467
+ max-width: 36rem;
468
+ max-height: 70vh;
469
+ display: flex;
470
+ flex-direction: column;
471
+ background: var(--otfw-bg-elevated);
472
+ border: 1px solid var(--otfw-border);
473
+ border-radius: 0.9rem;
474
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
475
+ overflow: hidden;
476
+ }
477
+ .otfw-search-field {
478
+ display: flex;
479
+ align-items: center;
480
+ gap: 0.6rem;
481
+ padding: 0.85rem 1rem;
482
+ border-bottom: 1px solid var(--otfw-border);
483
+ color: var(--otfw-text-muted);
484
+ }
485
+ .otfw-search-input-field {
486
+ flex: 1;
487
+ min-width: 0;
488
+ border: none;
489
+ background: none;
490
+ outline: none;
491
+ font: inherit;
492
+ font-size: 1rem;
493
+ color: var(--otfw-text);
494
+ }
495
+ /* Keyboard hint row beneath the input / results. */
496
+ .otfw-search-hint {
497
+ display: flex;
498
+ flex-wrap: wrap;
499
+ gap: 1rem;
500
+ padding: 0.6rem 1rem;
501
+ border-top: 1px solid var(--otfw-border);
502
+ color: var(--otfw-text-muted);
503
+ font-size: 0.72rem;
504
+ }
505
+ .otfw-search-hint-item {
506
+ display: inline-flex;
507
+ align-items: center;
508
+ gap: 0.3rem;
509
+ }
510
+ .otfw-search-hint kbd {
511
+ display: inline-flex;
512
+ align-items: center;
513
+ justify-content: center;
514
+ min-width: 1.3rem;
515
+ height: 1.3rem;
516
+ padding: 0 0.35rem;
517
+ border: 1px solid var(--otfw-border);
518
+ border-bottom-width: 2px;
519
+ border-radius: 0.3rem;
520
+ background: var(--otfw-bg-surface);
521
+ color: var(--otfw-text);
522
+ font-family: var(--otfw-mono);
523
+ font-size: 0.7rem;
524
+ line-height: 1;
525
+ }
526
+ .otfw-search-results {
527
+ list-style: none;
528
+ margin: 0;
529
+ padding: 0.4rem;
530
+ overflow-y: auto;
531
+ }
532
+ .otfw-search-result {
533
+ display: block;
534
+ padding: 0.6rem 0.75rem;
535
+ border-radius: 0.5rem;
536
+ text-decoration: none;
537
+ color: var(--otfw-text);
538
+ }
539
+ .otfw-search-result.is-active {
540
+ background: var(--otfw-accent-soft);
541
+ }
542
+ .otfw-search-result-crumb {
543
+ display: block;
544
+ font-size: 0.72rem;
545
+ color: var(--otfw-text-muted);
546
+ letter-spacing: 0.01em;
547
+ margin-bottom: 0.1rem;
548
+ }
549
+ .otfw-search-result-title {
550
+ display: block;
551
+ font-weight: 600;
552
+ font-size: 0.9rem;
553
+ }
554
+ /* The matched section heading, under the page/post title. */
555
+ .otfw-search-result-section {
556
+ display: flex;
557
+ align-items: center;
558
+ gap: 0.3rem;
559
+ font-size: 0.8rem;
560
+ color: var(--otfw-text-muted);
561
+ margin-top: 0.05rem;
562
+ }
563
+ .otfw-search-result-section::before {
564
+ content: "#";
565
+ color: var(--otfw-accent);
566
+ font-weight: 700;
567
+ }
568
+ .otfw-search-result-excerpt {
569
+ display: block;
570
+ font-size: 0.8rem;
571
+ color: var(--otfw-text-muted);
572
+ margin-top: 0.15rem;
573
+ line-height: 1.5;
574
+ }
575
+ .otfw-search-result-excerpt mark {
576
+ background: var(--otfw-accent-soft);
577
+ color: var(--otfw-accent);
578
+ border-radius: 0.2rem;
579
+ padding: 0 0.1rem;
580
+ }
581
+ .otfw-search-empty {
582
+ padding: 1.5rem 1rem;
583
+ text-align: center;
584
+ color: var(--otfw-text-muted);
585
+ font-size: 0.9rem;
586
+ }
587
+
200
588
  /* ── Docs grid ─────────────────────────────────────────────────────────────── */
201
589
  .otfw-docs {
202
590
  max-width: 90rem;
@@ -218,20 +606,92 @@ web-sidebar-node {
218
606
  }
219
607
 
220
608
  /* ── Sidebar ───────────────────────────────────────────────────────────────── */
609
+ /* Mobile-first: the sidebar is an off-canvas drawer below the navbar, slid in over a
610
+ * backdrop. It sits *under* the navbar (z < navbar's 50) so the burger stays tappable
611
+ * to toggle it. The desktop block below turns it back into the sticky grid column. */
221
612
  .otfw-sidebar {
222
- display: none;
613
+ position: fixed;
614
+ top: var(--otfw-navbar-height);
615
+ bottom: 0;
616
+ left: 0;
617
+ z-index: 40;
618
+ width: min(19rem, 80vw);
619
+ padding: 1.25rem 1.1rem 2rem;
620
+ overflow-y: auto;
621
+ overscroll-behavior: contain;
622
+ background: var(--otfw-bg);
623
+ border-right: 1px solid var(--otfw-border);
624
+ transform: translateX(-100%);
625
+ visibility: hidden;
626
+ transition: transform 0.25s ease, visibility 0.25s ease;
627
+ }
628
+ .otfw-sidebar.is-open {
629
+ transform: translateX(0);
630
+ visibility: visible;
631
+ }
632
+ .otfw-sidebar-backdrop {
633
+ position: fixed;
634
+ top: var(--otfw-navbar-height);
635
+ inset-inline: 0;
636
+ bottom: 0;
637
+ z-index: 39;
638
+ background: color-mix(in srgb, var(--otfw-text) 45%, transparent);
639
+ opacity: 0;
640
+ pointer-events: none;
641
+ transition: opacity 0.25s ease;
642
+ }
643
+ .otfw-sidebar-backdrop.is-open {
644
+ opacity: 1;
645
+ pointer-events: auto;
223
646
  }
224
647
  @media (min-width: 768px) {
225
648
  .otfw-sidebar {
226
- display: block;
227
649
  position: sticky;
228
650
  top: var(--otfw-navbar-height);
651
+ bottom: auto;
652
+ left: auto;
653
+ z-index: auto;
654
+ width: auto;
229
655
  align-self: start;
230
656
  max-height: calc(100vh - var(--otfw-navbar-height));
231
657
  overflow-y: auto;
232
658
  padding: 2rem 0.5rem 2rem 0;
659
+ background: none;
660
+ border-right: none;
661
+ transform: none;
662
+ visibility: visible;
663
+ }
664
+ .otfw-sidebar-backdrop {
665
+ display: none;
666
+ }
667
+ }
668
+ @media (prefers-reduced-motion: reduce) {
669
+ .otfw-sidebar,
670
+ .otfw-sidebar-backdrop {
671
+ transition: none;
672
+ }
673
+ }
674
+
675
+ /* Top-level site links shown at the top of the mobile drawer (the navbar hides its own
676
+ * copy there). Hidden on desktop, where those links stay in the navbar and the sidebar
677
+ * shows only the section tree. The links are NavbarLink elements, stacked into a column
678
+ * and separated from the tree by a divider. */
679
+ .otfw-drawer-links {
680
+ display: none;
681
+ }
682
+ @media (max-width: 767px) {
683
+ .otfw-drawer-links {
684
+ display: flex;
685
+ flex-direction: column;
686
+ align-items: flex-start;
687
+ gap: 0.9rem;
688
+ margin-bottom: 1.25rem;
689
+ padding-bottom: 1.25rem;
690
+ border-bottom: 1px solid var(--otfw-border);
691
+ font-size: 0.95rem;
233
692
  }
234
693
  }
694
+
235
695
  .otfw-sidebar-list,
236
696
  .otfw-sidebar-sublist {
237
697
  list-style: none;
@@ -256,23 +716,40 @@ web-sidebar-node {
256
716
  color: var(--otfw-text-muted);
257
717
  }
258
718
  .otfw-sidebar-link {
259
- display: block;
260
- padding: 0.3rem 0.6rem;
719
+ display: flex;
720
+ align-items: center;
721
+ gap: 0.6rem;
722
+ padding: 0.35rem 0.6rem;
261
723
  border-radius: 0.45rem;
262
724
  font-size: 0.875rem;
263
725
  color: var(--otfw-text-muted);
264
726
  text-decoration: none;
265
727
  transition: color 0.12s, background 0.12s;
266
728
  }
729
+ .otfw-sidebar-dot {
730
+ flex: none;
731
+ width: 5px;
732
+ height: 5px;
733
+ border-radius: 99px;
734
+ background: currentColor;
735
+ opacity: 0.35;
736
+ transition: opacity 0.12s;
737
+ }
267
738
  .otfw-sidebar-link:hover {
268
739
  color: var(--otfw-text);
269
740
  background: var(--otfw-bg-surface);
270
741
  }
742
+ .otfw-sidebar-link:hover .otfw-sidebar-dot {
743
+ opacity: 0.6;
744
+ }
271
745
  .otfw-sidebar-link.otfw-active {
272
746
  color: var(--otfw-accent);
273
747
  background: var(--otfw-accent-soft);
274
748
  font-weight: 600;
275
749
  }
750
+ .otfw-sidebar-link.otfw-active .otfw-sidebar-dot {
751
+ opacity: 1;
752
+ }
276
753
 
277
754
  /* ── Content ───────────────────────────────────────────────────────────────── */
278
755
  .otfw-content {
@@ -302,10 +779,55 @@ web-sidebar-node {
302
779
  font-weight: 700;
303
780
  margin: 2rem 0 0.75rem;
304
781
  }
782
+ .otfw-prose h4 {
783
+ font-size: 1.05rem;
784
+ font-weight: 700;
785
+ margin: 1.6rem 0 0.6rem;
786
+ }
787
+ .otfw-prose h5 {
788
+ font-size: 0.95rem;
789
+ font-weight: 700;
790
+ margin: 1.4rem 0 0.5rem;
791
+ }
792
+ .otfw-prose h6 {
793
+ font-size: 0.85rem;
794
+ font-weight: 700;
795
+ text-transform: uppercase;
796
+ letter-spacing: 0.04em;
797
+ color: var(--otfw-text-muted);
798
+ margin: 1.3rem 0 0.5rem;
799
+ }
800
+ .otfw-prose h1,
305
801
  .otfw-prose h2,
306
- .otfw-prose h3 {
802
+ .otfw-prose h3,
803
+ .otfw-prose h4,
804
+ .otfw-prose h5,
805
+ .otfw-prose h6 {
307
806
  scroll-margin-top: calc(var(--otfw-navbar-height) + 1rem);
308
807
  }
808
+
809
+ /* Self-linking heading anchor (the hover `#`). Hidden until the heading is
810
+ hovered or the anchor itself is focused, so it doesn't clutter the page. */
811
+ .otfw-heading-anchor {
812
+ margin-left: 0.4rem;
813
+ color: var(--otfw-text-muted);
814
+ font-weight: 400;
815
+ text-decoration: none;
816
+ opacity: 0;
817
+ transition: opacity 0.12s ease;
818
+ }
819
+ /* The `#` is presentational only — drawn here, not as a DOM text node, so it never
820
+ leaks into heading text read by the TOC or the search index. */
821
+ .otfw-heading-anchor::before {
822
+ content: "#";
823
+ }
824
+ .otfw-prose :is(h1, h2, h3, h4, h5, h6):hover .otfw-heading-anchor,
825
+ .otfw-heading-anchor:focus-visible {
826
+ opacity: 1;
827
+ }
828
+ .otfw-heading-anchor:hover {
829
+ color: var(--otfw-accent);
830
+ }
309
831
  .otfw-prose p,
310
832
  .otfw-prose ul,
311
833
  .otfw-prose ol {
@@ -317,9 +839,83 @@ web-sidebar-node {
317
839
  .otfw-prose ol {
318
840
  padding-left: 1.4rem;
319
841
  }
842
+ /* Restore list markers: the Tailwind preflight reset (@import "tailwindcss") sets
843
+ * `list-style: none` on every ul/ol, which strips bullets and numbers from prose. */
844
+ .otfw-prose ul {
845
+ list-style: disc;
846
+ }
847
+ .otfw-prose ul ul {
848
+ list-style: circle;
849
+ }
850
+ .otfw-prose ol {
851
+ list-style: decimal;
852
+ }
320
853
  .otfw-prose li {
321
854
  margin: 0.35rem 0;
322
855
  }
856
+ /* Loose list items wrap their content in <p>; keep that from adding block spacing. */
857
+ .otfw-prose li > p {
858
+ margin: 0;
859
+ }
860
+ /* GFM task list: drop the bullet and keep the checkbox inline with its label
861
+ * (the label is a block <p>, which would otherwise wrap to the next line). */
862
+ .otfw-prose li:has(> input[type="checkbox"]) {
863
+ list-style: none;
864
+ }
865
+ .otfw-prose li:has(> input[type="checkbox"]) > p {
866
+ display: inline;
867
+ }
868
+ /* Custom checkbox so checked vs. unchecked read at a glance (the native disabled
869
+ * box is nearly identical in both states). Unchecked is an empty bordered box;
870
+ * checked fills with the accent and draws a white tick. */
871
+ .otfw-prose li > input[type="checkbox"] {
872
+ appearance: none;
873
+ -webkit-appearance: none;
874
+ width: 1.05rem;
875
+ height: 1.05rem;
876
+ margin: 0 0.55em 0 0;
877
+ border: 1.5px solid var(--otfw-border);
878
+ border-radius: 0.3rem;
879
+ background: var(--otfw-bg-surface);
880
+ vertical-align: -0.18em;
881
+ position: relative;
882
+ opacity: 1; /* override the browser's disabled-input dimming */
883
+ cursor: default;
884
+ }
885
+ .otfw-prose li > input[type="checkbox"]:checked {
886
+ background: var(--otfw-accent);
887
+ border-color: var(--otfw-accent);
888
+ }
889
+ .otfw-prose li > input[type="checkbox"]:checked::after {
890
+ content: "";
891
+ position: absolute;
892
+ left: 0.34rem;
893
+ top: 0.13rem;
894
+ width: 0.26rem;
895
+ height: 0.52rem;
896
+ border: solid #fff;
897
+ border-width: 0 2px 2px 0;
898
+ transform: rotate(45deg);
899
+ }
900
+ /* Completed items: mute the label (the filled check already marks it done). */
901
+ .otfw-prose li:has(> input[type="checkbox"]:checked) > p {
902
+ color: var(--otfw-text-muted);
903
+ }
904
+ /* An unclassed <button> in prose (e.g. an inline interactive demo). Component
905
+ * buttons carry their own class, so this only catches bare demo buttons. */
906
+ .otfw-prose button:not([class]) {
907
+ font: inherit;
908
+ cursor: pointer;
909
+ padding: 0.4rem 0.9rem;
910
+ border: 1px solid var(--otfw-border);
911
+ border-radius: 0.5rem;
912
+ background: var(--otfw-bg-surface);
913
+ color: inherit;
914
+ transition: border-color 0.15s ease, background 0.15s ease;
915
+ }
916
+ .otfw-prose button:not([class]):hover {
917
+ border-color: var(--otfw-accent);
918
+ }
323
919
  .otfw-prose a {
324
920
  color: var(--otfw-accent);
325
921
  text-decoration: none;
@@ -356,6 +952,80 @@ web-sidebar-node {
356
952
  padding: 0;
357
953
  font-size: inherit;
358
954
  }
955
+ /* Titled code block (built by the MDX front-end): a header bar — language label,
956
+ * optional filename, and a copy button on the right — above the highlighted <pre>.
957
+ * The wrapper owns the radius and clips the corners of the dark header and code. */
958
+ .otfw-code {
959
+ margin: 0 0 1.4rem;
960
+ border: 1px solid var(--otfw-border);
961
+ border-radius: var(--otfw-radius);
962
+ overflow: hidden;
963
+ background: #2b303b;
964
+ }
965
+ .otfw-code-head {
966
+ display: flex;
967
+ align-items: center;
968
+ gap: 0.55rem;
969
+ padding: 0.4rem 0.5rem 0.4rem 0.95rem;
970
+ background: #23272f;
971
+ border-bottom: 1px solid rgba(255, 255, 255, 0.07);
972
+ }
973
+ .otfw-code-lang {
974
+ text-transform: uppercase;
975
+ letter-spacing: 0.05em;
976
+ font-size: 0.66rem;
977
+ font-weight: 700;
978
+ color: rgba(255, 255, 255, 0.45);
979
+ }
980
+ .otfw-code-name {
981
+ font-family: var(--otfw-mono);
982
+ font-size: 0.78rem;
983
+ color: rgba(255, 255, 255, 0.82);
984
+ }
985
+ .otfw-code pre {
986
+ margin: 0;
987
+ border: none;
988
+ border-radius: 0;
989
+ /* Readable default for blocks without build-time highlighting (e.g. string content
990
+ * rendered by Tabs via CodeBlock). Fenced blocks carry inline syntect
991
+ * colors, which override this. */
992
+ color: #e2e8f0;
993
+ }
994
+ .otfw-copy {
995
+ margin-left: auto;
996
+ display: inline-flex;
997
+ align-items: center;
998
+ gap: 0.35rem;
999
+ padding: 0.28rem 0.6rem;
1000
+ border: 1px solid rgba(255, 255, 255, 0.16);
1001
+ border-radius: 999px;
1002
+ background: rgba(255, 255, 255, 0.06);
1003
+ color: rgba(255, 255, 255, 0.78);
1004
+ font-family: var(--otfw-font);
1005
+ font-size: 0.72rem;
1006
+ font-weight: 600;
1007
+ line-height: 1;
1008
+ cursor: pointer;
1009
+ transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
1010
+ }
1011
+ .otfw-copy:hover {
1012
+ background: rgba(255, 255, 255, 0.14);
1013
+ color: #fff;
1014
+ }
1015
+ .otfw-copy.is-copied {
1016
+ background: color-mix(in srgb, #10b981 22%, transparent);
1017
+ border-color: color-mix(in srgb, #10b981 55%, transparent);
1018
+ color: #34d399;
1019
+ }
1020
+ .otfw-copy .otfw-check-icon {
1021
+ display: none;
1022
+ }
1023
+ .otfw-copy.is-copied .otfw-copy-icon {
1024
+ display: none;
1025
+ }
1026
+ .otfw-copy.is-copied .otfw-check-icon {
1027
+ display: inline;
1028
+ }
359
1029
  .otfw-prose blockquote {
360
1030
  margin: 0 0 1.2rem;
361
1031
  padding: 0.25rem 1.1rem;
@@ -467,6 +1137,43 @@ web-sidebar-node {
467
1137
  color: var(--otfw-accent);
468
1138
  }
469
1139
 
1140
+ /* ── Page meta (last updated · edit link) ──────────────────────────────────── */
1141
+ .otfw-page-meta {
1142
+ display: flex;
1143
+ align-items: center;
1144
+ justify-content: space-between;
1145
+ gap: 1rem;
1146
+ flex-wrap: wrap;
1147
+ margin-top: 2.5rem;
1148
+ }
1149
+ /* Empty when a page has neither bit of meta — collapse the top margin then. */
1150
+ .otfw-page-meta:empty {
1151
+ margin-top: 0;
1152
+ }
1153
+ .otfw-last-updated {
1154
+ font-size: 0.8rem;
1155
+ color: var(--otfw-text-muted);
1156
+ }
1157
+ .otfw-last-updated-time {
1158
+ font-weight: 600;
1159
+ color: var(--otfw-text);
1160
+ }
1161
+ .otfw-edit-page {
1162
+ display: inline-flex;
1163
+ align-items: center;
1164
+ gap: 0.35rem;
1165
+ font-size: 0.8rem;
1166
+ font-weight: 500;
1167
+ color: var(--otfw-text-muted);
1168
+ text-decoration: none;
1169
+ }
1170
+ .otfw-edit-page:hover {
1171
+ color: var(--otfw-accent);
1172
+ }
1173
+ .otfw-page-meta + .otfw-pagination {
1174
+ margin-top: 1.5rem;
1175
+ }
1176
+
470
1177
  /* ── Pagination ────────────────────────────────────────────────────────────── */
471
1178
  .otfw-pagination {
472
1179
  display: flex;
@@ -536,58 +1243,79 @@ web-sidebar-node {
536
1243
  }
537
1244
 
538
1245
  /* ── Callout ───────────────────────────────────────────────────────────────── */
1246
+ /* Per-type accent color `--c` drives a tinted fill, border, title color and a
1247
+ * filled icon chip — all derived from the one color via color-mix, so the same
1248
+ * rule adapts to light and dark automatically. */
539
1249
  .otfw-callout {
1250
+ --c: var(--otfw-text-muted);
1251
+ display: grid;
1252
+ grid-template-columns: auto 1fr;
1253
+ gap: 0.85rem;
540
1254
  margin: 1.5rem 0;
541
1255
  padding: 1rem 1.1rem;
542
- border: 1px solid var(--otfw-border);
543
- border-left-width: 3px;
544
- border-radius: var(--otfw-radius);
545
- background: var(--otfw-bg-surface);
1256
+ border: 1px solid color-mix(in srgb, var(--c) 28%, var(--otfw-border));
1257
+ border-left: 3px solid var(--c);
1258
+ border-radius: 0.75rem;
1259
+ background: color-mix(in srgb, var(--c) 7%, var(--otfw-bg));
1260
+ }
1261
+ /* Plain icon (design default): the glyph is tinted with the accent, no filled chip. */
1262
+ .otfw-callout-icon {
1263
+ display: grid;
1264
+ place-items: center;
1265
+ flex: none;
1266
+ width: 1.65rem;
1267
+ height: 1.65rem;
1268
+ color: var(--c);
1269
+ font-size: 1.15rem;
1270
+ line-height: 1;
546
1271
  }
547
1272
  .otfw-callout-title {
548
- display: flex;
549
- align-items: center;
550
- gap: 0.5rem;
551
1273
  font-weight: 700;
552
- margin-bottom: 0.4rem;
1274
+ font-size: 0.9rem;
1275
+ color: color-mix(in srgb, var(--c) 60%, var(--otfw-text));
1276
+ margin-bottom: 0.25rem;
1277
+ }
1278
+ .otfw-callout-content {
1279
+ min-width: 0;
1280
+ }
1281
+ .otfw-callout-body {
1282
+ font-size: 0.92rem;
1283
+ line-height: 1.65;
553
1284
  }
554
1285
  .otfw-callout-body > :last-child {
555
1286
  margin-bottom: 0;
556
1287
  }
557
1288
  .otfw-callout-note {
558
- border-left-color: var(--otfw-text-muted);
1289
+ --c: var(--otfw-text-muted);
559
1290
  }
560
1291
  .otfw-callout-tip {
561
- border-left-color: #10b981;
1292
+ --c: #10b981;
562
1293
  }
563
1294
  .otfw-callout-info {
564
- border-left-color: #3b82f6;
1295
+ --c: #3b82f6;
565
1296
  }
566
1297
  .otfw-callout-warning {
567
- border-left-color: #f59e0b;
1298
+ --c: #f59e0b;
568
1299
  }
569
1300
  .otfw-callout-danger {
570
- border-left-color: #ef4444;
1301
+ --c: #ef4444;
571
1302
  }
572
1303
 
573
- /* ── Tabs / CodeGroup ──────────────────────────────────────────────────────── */
574
- .otfw-tabs,
575
- .otfw-codegroup {
1304
+ /* ── Tabs ──────────────────────────────────────────────────────────────────── */
1305
+ .otfw-tabs {
576
1306
  margin: 1.5rem 0;
577
1307
  border: 1px solid var(--otfw-border);
578
1308
  border-radius: var(--otfw-radius);
579
1309
  overflow: hidden;
580
1310
  }
581
- .otfw-tabs-list,
582
- .otfw-codegroup-tabs {
1311
+ .otfw-tabs-list {
583
1312
  display: flex;
584
1313
  gap: 0.25rem;
585
1314
  padding: 0.4rem;
586
1315
  background: var(--otfw-bg-surface);
587
1316
  border-bottom: 1px solid var(--otfw-border);
588
1317
  }
589
- .otfw-tab,
590
- .otfw-codegroup-tab {
1318
+ .otfw-tab {
591
1319
  padding: 0.35rem 0.85rem;
592
1320
  border: none;
593
1321
  border-radius: 0.4rem;
@@ -598,25 +1326,69 @@ web-sidebar-node {
598
1326
  font-weight: 600;
599
1327
  cursor: pointer;
600
1328
  }
601
- .otfw-tab.otfw-active,
602
- .otfw-codegroup-tab.otfw-active {
1329
+ .otfw-tab.otfw-active {
603
1330
  background: var(--otfw-bg);
604
1331
  color: var(--otfw-accent);
605
1332
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
606
1333
  }
607
- .otfw-tabs-panels,
608
- .otfw-codegroup-panel {
1334
+ .otfw-tabs-panels {
609
1335
  padding: 1rem 1.1rem;
610
1336
  }
611
- .otfw-codegroup-panel pre,
612
1337
  .otfw-tabs-panels pre {
613
1338
  margin: 0;
614
1339
  border: none;
615
1340
  }
1341
+ /* A CodeBlock filling a panel shouldn't add its own trailing margin on top of the
1342
+ * panel's padding. */
1343
+ .otfw-tab-panel > .otfw-code {
1344
+ margin: 0;
1345
+ }
616
1346
  .otfw-hidden {
617
1347
  display: none;
618
1348
  }
619
1349
 
1350
+ /* ── Steps ─────────────────────────────────────────────────────────────────── */
1351
+ .otfw-steps {
1352
+ counter-reset: otfw-step;
1353
+ margin: 1.5rem 0;
1354
+ padding-left: 1.75rem;
1355
+ border-left: 1px solid var(--otfw-border);
1356
+ }
1357
+ .otfw-steps > h3,
1358
+ .otfw-steps > h4 {
1359
+ position: relative;
1360
+ counter-increment: otfw-step;
1361
+ margin-top: 1.6rem;
1362
+ font-size: 1.05rem;
1363
+ }
1364
+ .otfw-steps > h3:first-child,
1365
+ .otfw-steps > h4:first-child {
1366
+ margin-top: 0;
1367
+ }
1368
+ .otfw-steps > h3::before,
1369
+ .otfw-steps > h4::before {
1370
+ content: counter(otfw-step);
1371
+ position: absolute;
1372
+ left: calc(-1.75rem - 1px);
1373
+ top: 50%;
1374
+ transform: translate(-50%, -50%);
1375
+ display: grid;
1376
+ place-items: center;
1377
+ width: 1.7rem;
1378
+ height: 1.7rem;
1379
+ border-radius: 999px;
1380
+ background: var(--otfw-bg);
1381
+ border: 1px solid var(--otfw-border);
1382
+ color: var(--otfw-text-muted);
1383
+ font-size: 0.8rem;
1384
+ font-weight: 700;
1385
+ }
1386
+ .otfw-steps > h3:hover::before,
1387
+ .otfw-steps > h4:hover::before {
1388
+ border-color: var(--otfw-accent);
1389
+ color: var(--otfw-accent);
1390
+ }
1391
+
620
1392
  /* ── Table wrapper ─────────────────────────────────────────────────────────── */
621
1393
  .otfw-table-wrap {
622
1394
  overflow-x: auto;
@@ -637,3 +1409,207 @@ web-sidebar-node {
637
1409
  background: var(--otfw-bg-surface);
638
1410
  font-weight: 600;
639
1411
  }
1412
+
1413
+ /* ── Cards (index grids, e.g. the API overview) ────────────────────────────── */
1414
+ .otfw-cards {
1415
+ display: grid;
1416
+ grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
1417
+ gap: 1rem;
1418
+ margin: 1.5rem 0;
1419
+ }
1420
+ .otfw-card {
1421
+ display: flex;
1422
+ flex-direction: column;
1423
+ gap: 0.35rem;
1424
+ padding: 1.1rem 1.25rem;
1425
+ border: 1px solid var(--otfw-border);
1426
+ border-radius: 0.75rem;
1427
+ background: var(--otfw-bg-surface);
1428
+ text-decoration: none;
1429
+ transition: border-color 0.15s, transform 0.15s;
1430
+ }
1431
+ .otfw-card:hover {
1432
+ border-color: var(--otfw-accent);
1433
+ transform: translateY(-2px);
1434
+ }
1435
+ .otfw-card-title {
1436
+ font-weight: 700;
1437
+ color: var(--otfw-text);
1438
+ }
1439
+ .otfw-card-desc {
1440
+ font-size: 0.85rem;
1441
+ line-height: 1.5;
1442
+ color: var(--otfw-text-muted);
1443
+ }
1444
+
1445
+ /* ── Blog ──────────────────────────────────────────────────────────────────── */
1446
+ .otfw-blog {
1447
+ max-width: 90rem;
1448
+ margin: 0 auto;
1449
+ padding: 0 1.5rem;
1450
+ }
1451
+ .otfw-blog-main {
1452
+ width: 100%;
1453
+ max-width: var(--otfw-content-max);
1454
+ margin: 0 auto;
1455
+ padding: 3rem 0;
1456
+ }
1457
+ /* A post gets content + a sticky "On this page" TOC on wide screens. */
1458
+ .otfw-blog-post {
1459
+ display: grid;
1460
+ grid-template-columns: 1fr;
1461
+ gap: 2.5rem;
1462
+ }
1463
+ @media (min-width: 1280px) {
1464
+ .otfw-blog-post {
1465
+ grid-template-columns: minmax(0, 1fr) var(--otfw-toc-width);
1466
+ /* No `align-items: start` — the TOC column is a <web-toc> custom element that
1467
+ must stretch to the row height so its inner `position: sticky` nav has travel
1468
+ room (mirrors `.otfw-docs`). With `start` the host shrinks to content and the
1469
+ TOC scrolls away with the page. */
1470
+ }
1471
+ .otfw-blog-post .otfw-blog-main {
1472
+ margin: 0;
1473
+ justify-self: end;
1474
+ }
1475
+ }
1476
+ .otfw-blog-back {
1477
+ display: inline-block;
1478
+ margin-bottom: 1.5rem;
1479
+ font-size: 0.85rem;
1480
+ color: var(--otfw-text-muted);
1481
+ text-decoration: none;
1482
+ }
1483
+ .otfw-blog-back:hover {
1484
+ color: var(--otfw-text);
1485
+ }
1486
+
1487
+ /* Post banner (title + description + meta). */
1488
+ .otfw-post-banner {
1489
+ margin-bottom: 2.5rem;
1490
+ padding-bottom: 1.75rem;
1491
+ border-bottom: 1px solid var(--otfw-border);
1492
+ }
1493
+ .otfw-post-title {
1494
+ font-size: clamp(2rem, 5vw, 2.75rem);
1495
+ font-weight: 800;
1496
+ letter-spacing: -0.02em;
1497
+ line-height: 1.1;
1498
+ color: var(--otfw-text);
1499
+ }
1500
+ .otfw-post-desc {
1501
+ margin-top: 0.75rem;
1502
+ font-size: 1.15rem;
1503
+ color: var(--otfw-text-muted);
1504
+ line-height: 1.5;
1505
+ }
1506
+
1507
+ /* Meta row: date · author · reading time. */
1508
+ .otfw-post-meta {
1509
+ display: flex;
1510
+ flex-wrap: wrap;
1511
+ align-items: center;
1512
+ gap: 0.75rem;
1513
+ margin-top: 1rem;
1514
+ font-size: 0.85rem;
1515
+ color: var(--otfw-text-muted);
1516
+ }
1517
+ .otfw-post-meta > * + *::before {
1518
+ content: "·";
1519
+ margin-right: 0.75rem;
1520
+ color: var(--otfw-border);
1521
+ }
1522
+ .otfw-reading-time,
1523
+ .otfw-post-author,
1524
+ .otfw-post-date {
1525
+ white-space: nowrap;
1526
+ }
1527
+
1528
+ /* Tags. */
1529
+ .otfw-post-tags {
1530
+ display: flex;
1531
+ flex-wrap: wrap;
1532
+ gap: 0.5rem;
1533
+ margin-top: 1rem;
1534
+ }
1535
+ .otfw-post-tag {
1536
+ padding: 0.15rem 0.6rem;
1537
+ border-radius: 9999px;
1538
+ background: var(--otfw-bg-surface);
1539
+ border: 1px solid var(--otfw-border);
1540
+ font-size: 0.72rem;
1541
+ font-weight: 600;
1542
+ color: var(--otfw-text-muted);
1543
+ }
1544
+
1545
+ /* Index listing. */
1546
+ .otfw-post-list {
1547
+ display: flex;
1548
+ flex-direction: column;
1549
+ }
1550
+ .otfw-post-card {
1551
+ display: block;
1552
+ padding: 1.6rem 0;
1553
+ border-top: 1px solid var(--otfw-border);
1554
+ text-decoration: none;
1555
+ transition: opacity 0.15s;
1556
+ }
1557
+ .otfw-post-card:hover .otfw-post-card-title {
1558
+ color: var(--otfw-accent);
1559
+ }
1560
+ .otfw-post-card .otfw-post-meta {
1561
+ margin-top: 0;
1562
+ }
1563
+ .otfw-post-card-title {
1564
+ margin-top: 0.4rem;
1565
+ font-size: 1.35rem;
1566
+ font-weight: 700;
1567
+ color: var(--otfw-text);
1568
+ transition: color 0.15s;
1569
+ }
1570
+ .otfw-post-card-desc {
1571
+ margin-top: 0.4rem;
1572
+ color: var(--otfw-text-muted);
1573
+ line-height: 1.5;
1574
+ }
1575
+ .otfw-post-empty {
1576
+ padding: 2rem 0;
1577
+ color: var(--otfw-text-muted);
1578
+ }
1579
+
1580
+ /* ── Blog: cover images & author ───────────────────────────────────────────── */
1581
+ .otfw-post-cover {
1582
+ display: block;
1583
+ width: 100%;
1584
+ aspect-ratio: 16 / 7;
1585
+ object-fit: cover;
1586
+ border-radius: 0.85rem;
1587
+ border: 1px solid var(--otfw-border);
1588
+ margin-bottom: 1.75rem;
1589
+ }
1590
+ .otfw-post-author {
1591
+ display: inline-flex;
1592
+ align-items: center;
1593
+ gap: 0.4rem;
1594
+ }
1595
+ .otfw-post-avatar {
1596
+ width: 1.4rem;
1597
+ height: 1.4rem;
1598
+ border-radius: 9999px;
1599
+ object-fit: cover;
1600
+ border: 1px solid var(--otfw-border);
1601
+ }
1602
+ .otfw-post-author-role {
1603
+ color: var(--otfw-text-muted);
1604
+ opacity: 0.8;
1605
+ }
1606
+ /* Cards with a cover use a media + body two-row layout. */
1607
+ .otfw-post-card-cover {
1608
+ display: block;
1609
+ width: 100%;
1610
+ aspect-ratio: 16 / 8;
1611
+ object-fit: cover;
1612
+ border-radius: 0.6rem;
1613
+ border: 1px solid var(--otfw-border);
1614
+ margin-bottom: 1rem;
1615
+ }