@refrakt-md/lumina 0.4.0 → 0.5.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 (71) hide show
  1. package/base.css +16 -0
  2. package/contracts/structures.json +1317 -3
  3. package/dist/config.d.ts +2 -3
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +4 -229
  6. package/dist/config.js.map +1 -1
  7. package/dist/transform.d.ts +2 -0
  8. package/dist/transform.d.ts.map +1 -1
  9. package/dist/transform.js +2 -0
  10. package/dist/transform.js.map +1 -1
  11. package/index.css +11 -0
  12. package/package.json +18 -11
  13. package/styles/elements/blockquote.css +8 -4
  14. package/styles/global.css +0 -7
  15. package/styles/layouts/blog.css +255 -0
  16. package/styles/layouts/default.css +11 -3
  17. package/styles/layouts/docs.css +67 -13
  18. package/styles/layouts/mobile.css +84 -0
  19. package/styles/runes/bento.css +2 -0
  20. package/styles/runes/codegroup.css +7 -2
  21. package/styles/runes/design-context.css +25 -0
  22. package/styles/runes/feature.css +20 -14
  23. package/styles/runes/form.css +1 -2
  24. package/styles/runes/grid.css +25 -7
  25. package/styles/runes/hero.css +15 -0
  26. package/styles/runes/map.css +113 -0
  27. package/styles/runes/palette.css +86 -0
  28. package/styles/runes/preview.css +187 -0
  29. package/styles/runes/sandbox.css +23 -0
  30. package/styles/runes/spacing.css +105 -0
  31. package/styles/runes/steps.css +7 -1
  32. package/styles/runes/swatch.css +28 -0
  33. package/styles/runes/symbol.css +164 -0
  34. package/styles/runes/tabs.css +6 -0
  35. package/styles/runes/testimonial.css +2 -3
  36. package/styles/runes/timeline.css +43 -24
  37. package/styles/runes/typography.css +91 -0
  38. package/svelte/elements.ts +1 -0
  39. package/{sveltekit → svelte}/index.ts +0 -8
  40. package/svelte/layouts/BlogLayout.svelte +173 -0
  41. package/svelte/layouts/DefaultLayout.svelte +67 -0
  42. package/svelte/layouts/DocsLayout.svelte +155 -0
  43. package/{sveltekit → svelte}/manifest.json +1 -1
  44. package/svelte/registry.ts +2 -0
  45. package/svelte/tokens.css +6 -0
  46. package/sveltekit/components/Accordion.svelte +0 -26
  47. package/sveltekit/components/Bento.svelte +0 -50
  48. package/sveltekit/components/Chart.svelte +0 -121
  49. package/sveltekit/components/CodeGroup.svelte +0 -88
  50. package/sveltekit/components/Comparison.svelte +0 -209
  51. package/sveltekit/components/DataTable.svelte +0 -154
  52. package/sveltekit/components/Details.svelte +0 -23
  53. package/sveltekit/components/Diagram.svelte +0 -45
  54. package/sveltekit/components/Embed.svelte +0 -36
  55. package/sveltekit/components/Form.svelte +0 -194
  56. package/sveltekit/components/Grid.svelte +0 -42
  57. package/sveltekit/components/Nav.svelte +0 -62
  58. package/sveltekit/components/Pricing.svelte +0 -20
  59. package/sveltekit/components/Reveal.svelte +0 -62
  60. package/sveltekit/components/Storyboard.svelte +0 -41
  61. package/sveltekit/components/Tabs.svelte +0 -75
  62. package/sveltekit/components/Testimonial.svelte +0 -26
  63. package/sveltekit/elements/Blockquote.svelte +0 -37
  64. package/sveltekit/elements/Pre.svelte +0 -77
  65. package/sveltekit/elements/Table.svelte +0 -40
  66. package/sveltekit/elements.ts +0 -11
  67. package/sveltekit/layouts/BlogLayout.svelte +0 -382
  68. package/sveltekit/layouts/DefaultLayout.svelte +0 -70
  69. package/sveltekit/layouts/DocsLayout.svelte +0 -133
  70. package/sveltekit/registry.ts +0 -59
  71. package/sveltekit/tokens.css +0 -71
@@ -1,75 +0,0 @@
1
- <script lang="ts">
2
- import type { SerializedTag, RendererNode } from '@refrakt-md/svelte';
3
- import { Renderer } from '@refrakt-md/svelte';
4
- import type { Snippet } from 'svelte';
5
-
6
- let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
7
-
8
- const typeName = $derived(tag.attributes.typeof);
9
-
10
- function isTag(n: RendererNode): n is SerializedTag {
11
- return n !== null && typeof n === 'object' && !Array.isArray(n) && (n as any).$$mdtype === 'Tag';
12
- }
13
-
14
- function getTextContent(node: RendererNode): string {
15
- if (typeof node === 'string') return node;
16
- if (typeof node === 'number') return String(node);
17
- if (isTag(node)) return node.children.map(getTextContent).join('');
18
- if (Array.isArray(node)) return node.map(getTextContent).join('');
19
- return '';
20
- }
21
-
22
- // For TabGroup: parse the ul/li structure from the transform
23
- const { tabs, panels } = $derived.by(() => {
24
- const tabs: { name: string }[] = [];
25
- const panels: { children: RendererNode[] }[] = [];
26
- if (typeName === 'TabGroup') {
27
- for (const child of tag.children) {
28
- if (!isTag(child) || child.name !== 'ul') continue;
29
- for (const item of child.children) {
30
- if (!isTag(item)) continue;
31
- if (item.attributes?.typeof === 'Tab') {
32
- const nameNode = item.children.find(
33
- (c): c is SerializedTag => isTag(c) && c.attributes?.property === 'name'
34
- );
35
- const name = nameNode ? getTextContent(nameNode) : getTextContent(item);
36
- tabs.push({ name: name.trim() });
37
- } else if (item.attributes?.typeof === 'TabPanel') {
38
- panels.push({ children: item.children });
39
- }
40
- }
41
- }
42
- }
43
- return { tabs, panels };
44
- });
45
-
46
- let activeIndex = $state(0);
47
- </script>
48
-
49
- {#if typeName === 'TabGroup' && tabs.length > 0}
50
- <div class="rf-tabs">
51
- <div class="rf-tabs__bar" role="tablist">
52
- {#each tabs as tab, i}
53
- <button
54
- class="rf-tabs__button {i === activeIndex ? 'rf-tabs__button--active' : ''}"
55
- role="tab"
56
- aria-selected={i === activeIndex}
57
- onclick={() => activeIndex = i}
58
- >
59
- {tab.name}
60
- </button>
61
- {/each}
62
- </div>
63
- <div class="rf-tabs__panels">
64
- {#each panels as panel, i}
65
- {#if i === activeIndex}
66
- <div class="rf-tabs__panel" role="tabpanel">
67
- <Renderer node={panel.children} />
68
- </div>
69
- {/if}
70
- {/each}
71
- </div>
72
- </div>
73
- {:else}
74
- {@render children()}
75
- {/if}
@@ -1,26 +0,0 @@
1
- <script lang="ts">
2
- import type { SerializedTag } from '@refrakt-md/svelte';
3
- import type { Snippet } from 'svelte';
4
-
5
- let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
6
-
7
- const rating = $derived(tag.children
8
- .find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'rating')
9
- ?.attributes?.content);
10
-
11
- const hasRating = $derived(rating !== undefined && rating !== null);
12
- const stars = $derived(hasRating ? Math.min(5, Math.max(0, Number(rating))) : 0);
13
- </script>
14
-
15
- <article class="rf-testimonial">
16
- {#if hasRating}
17
- <div class="rf-testimonial__rating" aria-label="{stars} out of 5 stars">
18
- {#each Array(5) as _, i}
19
- <span class="rf-testimonial__star {i < stars ? 'rf-testimonial__star--filled' : ''}">&#9733;</span>
20
- {/each}
21
- </div>
22
- {/if}
23
- <div class="rf-testimonial__content">
24
- {@render children()}
25
- </div>
26
- </article>
@@ -1,37 +0,0 @@
1
- <script lang="ts">
2
- import type { SerializedTag } from '@refrakt-md/svelte';
3
- import type { Snippet } from 'svelte';
4
-
5
- let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
6
- </script>
7
-
8
- <blockquote class="themed-blockquote">
9
- <svg class="quote-mark" width="24" height="24" viewBox="0 0 24 24" fill="none">
10
- <path d="M10 8C10 5.79 8.21 4 6 4C3.79 4 2 5.79 2 8C2 10.21 3.79 12 6 12C6.67 12 7.31 11.83 7.88 11.54C7.63 14.08 5.55 16.09 3 16.34V18.36C6.68 18.09 9.64 15.13 9.93 11.45C9.98 10.99 10 10.5 10 10V8ZM22 8C22 5.79 20.21 4 18 4C15.79 4 14 5.79 14 8C14 10.21 15.79 12 18 12C18.67 12 19.31 11.83 19.88 11.54C19.63 14.08 17.55 16.09 15 16.34V18.36C18.68 18.09 21.64 15.13 21.93 11.45C21.98 10.99 22 10.5 22 10V8Z" fill="currentColor"/>
11
- </svg>
12
- {@render children()}
13
- </blockquote>
14
-
15
- <style>
16
- .themed-blockquote {
17
- position: relative;
18
- border-left: 3px solid var(--color-primary);
19
- margin: 1.5rem 0;
20
- padding: 1rem 1.25rem 1rem 2.75rem;
21
- color: var(--color-muted);
22
- background: var(--color-surface);
23
- border-radius: 0 var(--radius-md) var(--radius-md) 0;
24
- }
25
- .quote-mark {
26
- position: absolute;
27
- left: 0.75rem;
28
- top: 1rem;
29
- width: 18px;
30
- height: 18px;
31
- color: var(--color-primary);
32
- opacity: 0.4;
33
- }
34
- .themed-blockquote :global(p:last-child) {
35
- margin-bottom: 0;
36
- }
37
- </style>
@@ -1,77 +0,0 @@
1
- <script lang="ts">
2
- import type { SerializedTag } from '@refrakt-md/svelte';
3
- import type { Snippet } from 'svelte';
4
-
5
- let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
6
-
7
- const isCodeBlock = 'data-language' in (tag.attributes || {});
8
-
9
- let preEl: HTMLPreElement;
10
- let copied = $state(false);
11
- let timer: ReturnType<typeof setTimeout>;
12
-
13
- function copy() {
14
- const text = preEl.textContent ?? '';
15
- navigator.clipboard.writeText(text);
16
- copied = true;
17
- clearTimeout(timer);
18
- timer = setTimeout(() => copied = false, 2000);
19
- }
20
- </script>
21
-
22
- {#if isCodeBlock}
23
- <div class="rf-codeblock">
24
- <pre bind:this={preEl} {...tag.attributes}>{@render children()}</pre>
25
- <button class="rf-codeblock__copy" class:rf-codeblock__copy--copied={copied} onclick={copy} aria-label="Copy code">
26
- {#if copied}
27
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
28
- <polyline points="20 6 9 17 4 12" />
29
- </svg>
30
- {:else}
31
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
32
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
33
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
34
- </svg>
35
- {/if}
36
- </button>
37
- </div>
38
- {:else}
39
- <pre {...tag.attributes}>{@render children()}</pre>
40
- {/if}
41
-
42
- <style>
43
- .rf-codeblock {
44
- position: relative;
45
- }
46
- .rf-codeblock :global(pre) {
47
- margin: 0;
48
- }
49
- .rf-codeblock__copy {
50
- position: absolute;
51
- top: 0.5rem;
52
- right: 0.5rem;
53
- display: flex;
54
- align-items: center;
55
- justify-content: center;
56
- width: 2rem;
57
- height: 2rem;
58
- border: none;
59
- border-radius: var(--rf-radius-sm);
60
- background: transparent;
61
- color: var(--rf-color-code-text, #e2e8f0);
62
- cursor: pointer;
63
- opacity: 0;
64
- transition: opacity 150ms ease, background-color 150ms ease;
65
- }
66
- .rf-codeblock:hover .rf-codeblock__copy {
67
- opacity: 0.6;
68
- }
69
- .rf-codeblock__copy:hover {
70
- opacity: 1 !important;
71
- background: rgba(255, 255, 255, 0.1);
72
- }
73
- .rf-codeblock__copy--copied {
74
- opacity: 1 !important;
75
- color: var(--rf-color-success, #4ade80);
76
- }
77
- </style>
@@ -1,40 +0,0 @@
1
- <script lang="ts">
2
- import type { SerializedTag } from '@refrakt-md/svelte';
3
- import type { Snippet } from 'svelte';
4
-
5
- let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
6
- </script>
7
-
8
- <div class="table-wrapper">
9
- <table {...tag.attributes}>
10
- {@render children()}
11
- </table>
12
- </div>
13
-
14
- <style>
15
- .table-wrapper {
16
- overflow-x: auto;
17
- -webkit-overflow-scrolling: touch;
18
- margin: 1.5rem 0;
19
- border: 1px solid var(--color-border);
20
- border-radius: var(--radius-md);
21
- }
22
- .table-wrapper :global(table) {
23
- margin: 0;
24
- border-collapse: collapse;
25
- width: 100%;
26
- min-width: 100%;
27
- }
28
- .table-wrapper :global(th) {
29
- background: var(--color-surface);
30
- }
31
- .table-wrapper :global(th:first-child) {
32
- border-top-left-radius: var(--radius-md);
33
- }
34
- .table-wrapper :global(th:last-child) {
35
- border-top-right-radius: var(--radius-md);
36
- }
37
- .table-wrapper :global(tr:last-child td) {
38
- border-bottom: none;
39
- }
40
- </style>
@@ -1,11 +0,0 @@
1
- import type { ElementOverrides } from '@refrakt-md/svelte';
2
- import Table from './elements/Table.svelte';
3
- import Blockquote from './elements/Blockquote.svelte';
4
- import Pre from './elements/Pre.svelte';
5
-
6
- /** Maps HTML element names to Lumina theme override components */
7
- export const elements: ElementOverrides = {
8
- 'table': Table,
9
- 'blockquote': Blockquote,
10
- 'pre': Pre,
11
- };
@@ -1,382 +0,0 @@
1
- <script lang="ts">
2
- import { Renderer } from '@refrakt-md/svelte';
3
-
4
- let { title, frontmatter, regions, renderable, pages }: {
5
- title: string;
6
- description: string;
7
- frontmatter?: Record<string, unknown>;
8
- regions: Record<string, { name: string; mode: string; content: any[] }>;
9
- renderable: any;
10
- pages: Array<{
11
- url: string;
12
- title: string;
13
- draft: boolean;
14
- description?: string;
15
- date?: string;
16
- author?: string;
17
- tags?: string[];
18
- image?: string;
19
- }>;
20
- } = $props();
21
-
22
- const date = $derived(frontmatter?.date as string | undefined);
23
- const author = $derived(frontmatter?.author as string | undefined);
24
- const tags = $derived(frontmatter?.tags as string[] | undefined);
25
-
26
- // Index page has no date; individual posts always have one
27
- const isIndex = $derived(!date);
28
-
29
- const posts = $derived(
30
- isIndex
31
- ? pages
32
- .filter(p => p.url.startsWith('/blog/') && p.url !== '/blog' && !p.draft && p.date)
33
- .sort((a, b) => (b.date ?? '').localeCompare(a.date ?? ''))
34
- : []
35
- );
36
-
37
- const hasSidebar = $derived(!!regions.sidebar);
38
-
39
- function formatDate(iso: string): string {
40
- const d = new Date(iso + 'T00:00:00');
41
- return d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
42
- }
43
- </script>
44
-
45
- {#if regions.header}
46
- <header class="site-header">
47
- <div class="site-header-inner">
48
- <Renderer node={regions.header.content} />
49
- </div>
50
- </header>
51
- {/if}
52
-
53
- <div class="blog-layout" class:has-sidebar={hasSidebar}>
54
- {#if isIndex}
55
- <div class="blog-index">
56
- <h1 class="blog-index-title">{title}</h1>
57
-
58
- <div class="blog-index-body">
59
- <Renderer node={renderable} />
60
- </div>
61
-
62
- <div class="blog-index-posts">
63
- {#each posts as post}
64
- <a href={post.url} class="blog-post-card">
65
- <h2 class="blog-post-card-title">{post.title}</h2>
66
- <div class="blog-post-card-meta">
67
- {#if post.date}
68
- <time datetime={post.date}>{formatDate(post.date)}</time>
69
- {/if}
70
- {#if post.author}
71
- <span class="blog-post-card-author">{post.author}</span>
72
- {/if}
73
- </div>
74
- {#if post.description}
75
- <p class="blog-post-card-desc">{post.description}</p>
76
- {/if}
77
- {#if post.tags && post.tags.length > 0}
78
- <div class="blog-post-card-tags">
79
- {#each post.tags as tag}
80
- <span class="blog-article-tag">{tag}</span>
81
- {/each}
82
- </div>
83
- {/if}
84
- <span class="blog-post-card-link">Read more &rarr;</span>
85
- </a>
86
- {/each}
87
- </div>
88
- </div>
89
- {:else}
90
- <article class="blog-article">
91
- <header class="blog-article-header">
92
- <h1 class="blog-article-title">{title}</h1>
93
- {#if date || author}
94
- <div class="blog-article-meta">
95
- {#if date}
96
- <time datetime={date}>{formatDate(date)}</time>
97
- {/if}
98
- {#if author}
99
- <span class="blog-article-author">{author}</span>
100
- {/if}
101
- </div>
102
- {/if}
103
- {#if tags && tags.length > 0}
104
- <div class="blog-article-tags">
105
- {#each tags as tag}
106
- <span class="blog-article-tag">{tag}</span>
107
- {/each}
108
- </div>
109
- {/if}
110
- </header>
111
-
112
- <div class="blog-article-body">
113
- <Renderer node={renderable} />
114
- </div>
115
- </article>
116
-
117
- {#if regions.sidebar}
118
- <aside class="blog-sidebar">
119
- <Renderer node={regions.sidebar.content} />
120
- </aside>
121
- {/if}
122
- {/if}
123
- </div>
124
-
125
- {#if regions.footer}
126
- <footer class="site-footer">
127
- <Renderer node={regions.footer.content} />
128
- </footer>
129
- {/if}
130
-
131
- <style>
132
- /* ---- Site header (shared with other layouts) ---- */
133
- .site-header {
134
- border-bottom: 1px solid var(--color-border);
135
- }
136
- .site-header-inner {
137
- display: flex;
138
- align-items: center;
139
- justify-content: space-between;
140
- padding: 0.875rem 1.5rem;
141
- }
142
- .site-header :global(p) {
143
- margin: 0;
144
- line-height: 1;
145
- }
146
- .site-header :global(a) {
147
- display: inline-block;
148
- color: inherit;
149
- text-decoration: none;
150
- }
151
- .site-header :global(a:hover) {
152
- text-decoration: none;
153
- }
154
- .site-header :global(img) {
155
- display: block;
156
- height: 1.5rem;
157
- width: auto;
158
- }
159
- .site-header-inner :global(p:last-child:not(:first-child)) {
160
- font-size: 0.85rem;
161
- }
162
- .site-header-inner :global(p:last-child:not(:first-child) a) {
163
- margin-left: 1.5rem;
164
- color: var(--color-muted);
165
- }
166
- .site-header-inner :global(p:last-child:not(:first-child) a:hover) {
167
- color: var(--color-text);
168
- }
169
-
170
- /* ---- Blog layout ---- */
171
- .blog-layout {
172
- max-width: 72rem;
173
- margin: 0 auto;
174
- padding: 2.5rem 1.5rem 4rem;
175
- }
176
- .blog-layout.has-sidebar {
177
- display: grid;
178
- grid-template-columns: 1fr 16rem;
179
- gap: 3rem;
180
- align-items: start;
181
- }
182
-
183
- /* ---- Blog index ---- */
184
- .blog-index {
185
- max-width: 42rem;
186
- }
187
- .blog-index-title {
188
- font-size: 2.25rem;
189
- font-weight: 800;
190
- line-height: 1.15;
191
- letter-spacing: -0.02em;
192
- margin: 0 0 1rem;
193
- color: var(--color-text);
194
- }
195
- .blog-index-body {
196
- margin-bottom: 2rem;
197
- line-height: 1.8;
198
- color: var(--color-muted);
199
- }
200
- .blog-index-body:empty {
201
- display: none;
202
- }
203
- .blog-index-posts {
204
- display: flex;
205
- flex-direction: column;
206
- gap: 1.5rem;
207
- }
208
-
209
- /* ---- Post card ---- */
210
- .blog-post-card {
211
- display: block;
212
- padding: 1.5rem;
213
- border: 1px solid var(--color-border);
214
- border-radius: var(--radius-md, 0.5rem);
215
- text-decoration: none;
216
- color: inherit;
217
- transition: border-color 0.15s, box-shadow 0.15s;
218
- }
219
- .blog-post-card:hover {
220
- border-color: var(--color-text);
221
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
222
- text-decoration: none;
223
- }
224
- .blog-post-card-title {
225
- font-size: 1.35rem;
226
- font-weight: 700;
227
- margin: 0 0 0.5rem;
228
- color: var(--color-text);
229
- }
230
- .blog-post-card-meta {
231
- display: flex;
232
- align-items: center;
233
- gap: 0.75rem;
234
- font-size: 0.85rem;
235
- color: var(--color-muted);
236
- margin-bottom: 0.5rem;
237
- }
238
- .blog-post-card-author::before {
239
- content: '\00b7';
240
- margin-right: 0.75rem;
241
- }
242
- .blog-post-card-desc {
243
- margin: 0 0 0.75rem;
244
- font-size: 0.95rem;
245
- line-height: 1.6;
246
- color: var(--color-muted);
247
- }
248
- .blog-post-card-tags {
249
- display: flex;
250
- flex-wrap: wrap;
251
- gap: 0.5rem;
252
- margin-bottom: 0.75rem;
253
- }
254
- .blog-post-card-link {
255
- font-size: 0.85rem;
256
- font-weight: 600;
257
- color: var(--color-primary, var(--color-text));
258
- }
259
-
260
- /* ---- Article ---- */
261
- .blog-article {
262
- max-width: 42rem;
263
- }
264
- .blog-article-header {
265
- margin-bottom: 2.5rem;
266
- padding-bottom: 1.5rem;
267
- border-bottom: 1px solid var(--color-border);
268
- }
269
- .blog-article-title {
270
- font-size: 2.25rem;
271
- font-weight: 800;
272
- line-height: 1.15;
273
- letter-spacing: -0.02em;
274
- margin: 0 0 1rem;
275
- color: var(--color-text);
276
- }
277
- .blog-article-meta {
278
- display: flex;
279
- align-items: center;
280
- gap: 0.75rem;
281
- font-size: 0.9rem;
282
- color: var(--color-muted);
283
- }
284
- .blog-article-meta time {
285
- font-weight: 500;
286
- }
287
- .blog-article-author::before {
288
- content: '\00b7';
289
- margin-right: 0.75rem;
290
- }
291
- .blog-article-tags {
292
- display: flex;
293
- flex-wrap: wrap;
294
- gap: 0.5rem;
295
- margin-top: 0.75rem;
296
- }
297
- .blog-article-tag {
298
- font-size: 0.75rem;
299
- font-weight: 500;
300
- padding: 0.2rem 0.6rem;
301
- border-radius: 9999px;
302
- background: var(--color-surface);
303
- color: var(--color-muted);
304
- border: 1px solid var(--color-border);
305
- }
306
-
307
- /* ---- Article body typography ---- */
308
- .blog-article-body {
309
- line-height: 1.8;
310
- }
311
- .blog-article-body :global(h2) {
312
- margin-top: 2.5rem;
313
- }
314
- .blog-article-body :global(h3) {
315
- margin-top: 2rem;
316
- }
317
- .blog-article-body :global(p) {
318
- margin-bottom: 1.25rem;
319
- }
320
- .blog-article-body :global(img) {
321
- border-radius: var(--radius-md);
322
- margin: 1.5rem 0;
323
- }
324
-
325
- /* ---- Sidebar ---- */
326
- .blog-sidebar {
327
- position: sticky;
328
- top: 2.5rem;
329
- font-size: 0.85rem;
330
- color: var(--color-muted);
331
- }
332
- .blog-sidebar :global(h2),
333
- .blog-sidebar :global(h3),
334
- .blog-sidebar :global(h4) {
335
- font-size: 0.75rem;
336
- font-weight: 600;
337
- text-transform: uppercase;
338
- letter-spacing: 0.05em;
339
- color: var(--color-muted);
340
- margin-top: 0;
341
- margin-bottom: 0.5rem;
342
- }
343
- .blog-sidebar :global(ul) {
344
- list-style: none;
345
- padding: 0;
346
- margin: 0;
347
- }
348
- .blog-sidebar :global(li) {
349
- padding: 0.25rem 0;
350
- }
351
- .blog-sidebar :global(a) {
352
- color: var(--color-muted);
353
- text-decoration: none;
354
- }
355
- .blog-sidebar :global(a:hover) {
356
- color: var(--color-text);
357
- }
358
-
359
- /* ---- Footer ---- */
360
- .site-footer {
361
- border-top: 1px solid var(--color-border);
362
- padding: 2rem 1.5rem;
363
- text-align: center;
364
- font-size: 0.85rem;
365
- color: var(--color-muted);
366
- }
367
-
368
- /* ---- Mobile ---- */
369
- @media (max-width: 768px) {
370
- .blog-layout.has-sidebar {
371
- grid-template-columns: 1fr;
372
- }
373
- .blog-article-title {
374
- font-size: 1.75rem;
375
- }
376
- .blog-sidebar {
377
- position: static;
378
- border-top: 1px solid var(--color-border);
379
- padding-top: 1.5rem;
380
- }
381
- }
382
- </style>