1<script lang="ts">
  2	import { remark } from 'remark';
  3	import remarkBreaks from 'remark-breaks';
  4	import remarkGfm from 'remark-gfm';
  5	import remarkMath from 'remark-math';
  6	import rehypeHighlight from 'rehype-highlight';
  7	import remarkRehype from 'remark-rehype';
  8	import rehypeKatex from 'rehype-katex';
  9	import rehypeStringify from 'rehype-stringify';
 10	import type { Root as HastRoot, RootContent as HastRootContent } from 'hast';
 11	import type { Root as MdastRoot } from 'mdast';
 12	import { browser } from '$app/environment';
 13	import { onDestroy, tick } from 'svelte';
 14	import { rehypeRestoreTableHtml } from '$lib/markdown/table-html-restorer';
 15	import { rehypeEnhanceLinks } from '$lib/markdown/enhance-links';
 16	import { rehypeEnhanceCodeBlocks } from '$lib/markdown/enhance-code-blocks';
 17	import { remarkLiteralHtml } from '$lib/markdown/literal-html';
 18	import { copyCodeToClipboard, preprocessLaTeX } from '$lib/utils';
 19	import '$styles/katex-custom.scss';
 20	import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
 21	import githubLightCss from 'highlight.js/styles/github.css?inline';
 22	import { mode } from 'mode-watcher';
 23	import CodePreviewDialog from './CodePreviewDialog.svelte';
 24
 25	interface Props {
 26		content: string;
 27		class?: string;
 28	}
 29
 30	interface MarkdownBlock {
 31		id: string;
 32		html: string;
 33	}
 34
 35	let { content, class: className = '' }: Props = $props();
 36
 37	let containerRef = $state<HTMLDivElement>();
 38	let renderedBlocks = $state<MarkdownBlock[]>([]);
 39	let unstableBlockHtml = $state('');
 40	let previewDialogOpen = $state(false);
 41	let previewCode = $state('');
 42	let previewLanguage = $state('text');
 43
 44	let pendingMarkdown: string | null = null;
 45	let isProcessing = false;
 46
 47	const themeStyleId = `highlight-theme-${(window.idxThemeStyle = (window.idxThemeStyle ?? 0) + 1)}`;
 48
 49	let processor = $derived(() => {
 50		return remark()
 51			.use(remarkGfm) // GitHub Flavored Markdown
 52			.use(remarkMath) // Parse $inline$ and $$block$$ math
 53			.use(remarkBreaks) // Convert line breaks to <br>
 54			.use(remarkLiteralHtml) // Treat raw HTML as literal text with preserved indentation
 55			.use(remarkRehype) // Convert Markdown AST to rehype
 56			.use(rehypeKatex) // Render math using KaTeX
 57			.use(rehypeHighlight) // Add syntax highlighting
 58			.use(rehypeRestoreTableHtml) // Restore limited HTML (e.g., <br>, <ul>) inside Markdown tables
 59			.use(rehypeEnhanceLinks) // Add target="_blank" to links
 60			.use(rehypeEnhanceCodeBlocks) // Wrap code blocks with header and actions
 61			.use(rehypeStringify, { allowDangerousHtml: true }); // Convert to HTML string
 62	});
 63
 64	/**
 65	 * Removes click event listeners from copy and preview buttons.
 66	 * Called on component destroy.
 67	 */
 68	function cleanupEventListeners() {
 69		if (!containerRef) return;
 70
 71		const copyButtons = containerRef.querySelectorAll<HTMLButtonElement>('.copy-code-btn');
 72		const previewButtons = containerRef.querySelectorAll<HTMLButtonElement>('.preview-code-btn');
 73
 74		for (const button of copyButtons) {
 75			button.removeEventListener('click', handleCopyClick);
 76		}
 77
 78		for (const button of previewButtons) {
 79			button.removeEventListener('click', handlePreviewClick);
 80		}
 81	}
 82
 83	/**
 84	 * Removes this component's highlight.js theme style from the document head.
 85	 * Called on component destroy to clean up injected styles.
 86	 */
 87	function cleanupHighlightTheme() {
 88		if (!browser) return;
 89
 90		const existingTheme = document.getElementById(themeStyleId);
 91		existingTheme?.remove();
 92	}
 93
 94	/**
 95	 * Loads the appropriate highlight.js theme based on dark/light mode.
 96	 * Injects a scoped style element into the document head.
 97	 * @param isDark - Whether to load the dark theme (true) or light theme (false)
 98	 */
 99	function loadHighlightTheme(isDark: boolean) {
100		if (!browser) return;
101
102		const existingTheme = document.getElementById(themeStyleId);
103		existingTheme?.remove();
104
105		const style = document.createElement('style');
106		style.id = themeStyleId;
107		style.textContent = isDark ? githubDarkCss : githubLightCss;
108
109		document.head.appendChild(style);
110	}
111
112	/**
113	 * Extracts code information from a button click target within a code block.
114	 * @param target - The clicked button element
115	 * @returns Object with rawCode and language, or null if extraction fails
116	 */
117	function getCodeInfoFromTarget(target: HTMLElement) {
118		const wrapper = target.closest('.code-block-wrapper');
119
120		if (!wrapper) {
121			console.error('No wrapper found');
122			return null;
123		}
124
125		const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
126
127		if (!codeElement) {
128			console.error('No code element found in wrapper');
129			return null;
130		}
131
132		const rawCode = codeElement.textContent ?? '';
133
134		const languageLabel = wrapper.querySelector<HTMLElement>('.code-language');
135		const language = languageLabel?.textContent?.trim() || 'text';
136
137		return { rawCode, language };
138	}
139
140	/**
141	 * Generates a unique identifier for a HAST node based on its position.
142	 * Used for stable block identification during incremental rendering.
143	 * @param node - The HAST root content node
144	 * @param indexFallback - Fallback index if position is unavailable
145	 * @returns Unique string identifier for the node
146	 */
147	function getHastNodeId(node: HastRootContent, indexFallback: number): string {
148		const position = node.position;
149
150		if (position?.start?.offset != null && position?.end?.offset != null) {
151			return `hast-${position.start.offset}-${position.end.offset}`;
152		}
153
154		return `${node.type}-${indexFallback}`;
155	}
156
157	/**
158	 * Handles click events on copy buttons within code blocks.
159	 * Copies the raw code content to the clipboard.
160	 * @param event - The click event from the copy button
161	 */
162	async function handleCopyClick(event: Event) {
163		event.preventDefault();
164		event.stopPropagation();
165
166		const target = event.currentTarget as HTMLButtonElement | null;
167
168		if (!target) {
169			return;
170		}
171
172		const info = getCodeInfoFromTarget(target);
173
174		if (!info) {
175			return;
176		}
177
178		try {
179			await copyCodeToClipboard(info.rawCode);
180		} catch (error) {
181			console.error('Failed to copy code:', error);
182		}
183	}
184
185	/**
186	 * Handles preview dialog open state changes.
187	 * Clears preview content when dialog is closed.
188	 * @param open - Whether the dialog is being opened or closed
189	 */
190	function handlePreviewDialogOpenChange(open: boolean) {
191		previewDialogOpen = open;
192
193		if (!open) {
194			previewCode = '';
195			previewLanguage = 'text';
196		}
197	}
198
199	/**
200	 * Handles click events on preview buttons within HTML code blocks.
201	 * Opens a preview dialog with the rendered HTML content.
202	 * @param event - The click event from the preview button
203	 */
204	function handlePreviewClick(event: Event) {
205		event.preventDefault();
206		event.stopPropagation();
207
208		const target = event.currentTarget as HTMLButtonElement | null;
209
210		if (!target) {
211			return;
212		}
213
214		const info = getCodeInfoFromTarget(target);
215
216		if (!info) {
217			return;
218		}
219
220		previewCode = info.rawCode;
221		previewLanguage = info.language;
222		previewDialogOpen = true;
223	}
224
225	/**
226	 * Processes markdown content into stable and unstable HTML blocks.
227	 * Uses incremental rendering: stable blocks are cached, unstable block is re-rendered.
228	 * @param markdown - The raw markdown string to process
229	 */
230	async function processMarkdown(markdown: string) {
231		if (!markdown) {
232			renderedBlocks = [];
233			unstableBlockHtml = '';
234			return;
235		}
236
237		const normalized = preprocessLaTeX(markdown);
238		const processorInstance = processor();
239		const ast = processorInstance.parse(normalized) as MdastRoot;
240		const processedRoot = (await processorInstance.run(ast)) as HastRoot;
241		const processedChildren = processedRoot.children ?? [];
242		const stableCount = Math.max(processedChildren.length - 1, 0);
243		const nextBlocks: MarkdownBlock[] = [];
244
245		for (let index = 0; index < stableCount; index++) {
246			const hastChild = processedChildren[index];
247			const id = getHastNodeId(hastChild, index);
248			const existing = renderedBlocks[index];
249
250			if (existing && existing.id === id) {
251				nextBlocks.push(existing);
252				continue;
253			}
254
255			const html = stringifyProcessedNode(
256				processorInstance,
257				processedRoot,
258				processedChildren[index]
259			);
260
261			nextBlocks.push({ id, html });
262		}
263
264		let unstableHtml = '';
265
266		if (processedChildren.length > stableCount) {
267			const unstableChild = processedChildren[stableCount];
268			unstableHtml = stringifyProcessedNode(processorInstance, processedRoot, unstableChild);
269		}
270
271		renderedBlocks = nextBlocks;
272		await tick(); // Force DOM sync before updating unstable HTML block
273		unstableBlockHtml = unstableHtml;
274	}
275
276	/**
277	 * Attaches click event listeners to copy and preview buttons in code blocks.
278	 * Uses data-listener-bound attribute to prevent duplicate bindings.
279	 */
280	function setupCodeBlockActions() {
281		if (!containerRef) return;
282
283		const wrappers = containerRef.querySelectorAll<HTMLElement>('.code-block-wrapper');
284
285		for (const wrapper of wrappers) {
286			const copyButton = wrapper.querySelector<HTMLButtonElement>('.copy-code-btn');
287			const previewButton = wrapper.querySelector<HTMLButtonElement>('.preview-code-btn');
288
289			if (copyButton && copyButton.dataset.listenerBound !== 'true') {
290				copyButton.dataset.listenerBound = 'true';
291				copyButton.addEventListener('click', handleCopyClick);
292			}
293
294			if (previewButton && previewButton.dataset.listenerBound !== 'true') {
295				previewButton.dataset.listenerBound = 'true';
296				previewButton.addEventListener('click', handlePreviewClick);
297			}
298		}
299	}
300
301	/**
302	 * Converts a single HAST node to an enhanced HTML string.
303	 * Applies link and code block enhancements to the output.
304	 * @param processorInstance - The remark/rehype processor instance
305	 * @param processedRoot - The full processed HAST root (for context)
306	 * @param child - The specific HAST child node to stringify
307	 * @returns Enhanced HTML string representation of the node
308	 */
309	function stringifyProcessedNode(
310		processorInstance: ReturnType<typeof processor>,
311		processedRoot: HastRoot,
312		child: unknown
313	) {
314		const root: HastRoot = {
315			...(processedRoot as HastRoot),
316			children: [child as never]
317		};
318
319		return processorInstance.stringify(root);
320	}
321
322	/**
323	 * Queues markdown for processing with coalescing support.
324	 * Only processes the latest markdown when multiple updates arrive quickly.
325	 * @param markdown - The markdown content to render
326	 */
327	async function updateRenderedBlocks(markdown: string) {
328		pendingMarkdown = markdown;
329
330		if (isProcessing) {
331			return;
332		}
333
334		isProcessing = true;
335
336		try {
337			while (pendingMarkdown !== null) {
338				const nextMarkdown = pendingMarkdown;
339				pendingMarkdown = null;
340
341				await processMarkdown(nextMarkdown);
342			}
343		} catch (error) {
344			console.error('Failed to process markdown:', error);
345			renderedBlocks = [];
346			unstableBlockHtml = markdown.replace(/\n/g, '<br>');
347		} finally {
348			isProcessing = false;
349		}
350	}
351
352	$effect(() => {
353		const currentMode = mode.current;
354		const isDark = currentMode === 'dark';
355
356		loadHighlightTheme(isDark);
357	});
358
359	$effect(() => {
360		updateRenderedBlocks(content);
361	});
362
363	$effect(() => {
364		const hasRenderedBlocks = renderedBlocks.length > 0;
365		const hasUnstableBlock = Boolean(unstableBlockHtml);
366
367		if ((hasRenderedBlocks || hasUnstableBlock) && containerRef) {
368			setupCodeBlockActions();
369		}
370	});
371
372	onDestroy(() => {
373		cleanupEventListeners();
374		cleanupHighlightTheme();
375	});
376</script>
377
378<div bind:this={containerRef} class={className}>
379	{#each renderedBlocks as block (block.id)}
380		<div class="markdown-block" data-block-id={block.id}>
381			<!-- eslint-disable-next-line no-at-html-tags -->
382			{@html block.html}
383		</div>
384	{/each}
385
386	{#if unstableBlockHtml}
387		<div class="markdown-block markdown-block--unstable" data-block-id="unstable">
388			<!-- eslint-disable-next-line no-at-html-tags -->
389			{@html unstableBlockHtml}
390		</div>
391	{/if}
392</div>
393
394<CodePreviewDialog
395	open={previewDialogOpen}
396	code={previewCode}
397	language={previewLanguage}
398	onOpenChange={handlePreviewDialogOpenChange}
399/>
400
401<style>
402	.markdown-block,
403	.markdown-block--unstable {
404		display: contents;
405	}
406
407	/* Base typography styles */
408	div :global(p:not(:last-child)) {
409		margin-bottom: 1rem;
410		line-height: 1.75;
411	}
412
413	div :global(:is(h1, h2, h3, h4, h5, h6):first-child) {
414		margin-top: 0;
415	}
416
417	/* Headers with consistent spacing */
418	div :global(h1) {
419		font-size: 1.875rem;
420		font-weight: 700;
421		line-height: 1.2;
422		margin: 1.5rem 0 0.75rem 0;
423	}
424
425	div :global(h2) {
426		font-size: 1.5rem;
427		font-weight: 600;
428		line-height: 1.3;
429		margin: 1.25rem 0 0.5rem 0;
430	}
431
432	div :global(h3) {
433		font-size: 1.25rem;
434		font-weight: 600;
435		margin: 1.5rem 0 0.5rem 0;
436		line-height: 1.4;
437	}
438
439	div :global(h4) {
440		font-size: 1.125rem;
441		font-weight: 600;
442		margin: 0.75rem 0 0.25rem 0;
443	}
444
445	div :global(h5) {
446		font-size: 1rem;
447		font-weight: 600;
448		margin: 0.5rem 0 0.25rem 0;
449	}
450
451	div :global(h6) {
452		font-size: 0.875rem;
453		font-weight: 600;
454		margin: 0.5rem 0 0.25rem 0;
455	}
456
457	/* Text formatting */
458	div :global(strong) {
459		font-weight: 600;
460	}
461
462	div :global(em) {
463		font-style: italic;
464	}
465
466	div :global(del) {
467		text-decoration: line-through;
468		opacity: 0.7;
469	}
470
471	/* Inline code */
472	div :global(code:not(pre code)) {
473		background: var(--muted);
474		color: var(--muted-foreground);
475		padding: 0.125rem 0.375rem;
476		border-radius: 0.375rem;
477		font-size: 0.875rem;
478		font-family:
479			ui-monospace, SFMono-Regular, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas,
480			'Liberation Mono', Menlo, monospace;
481	}
482
483	/* Links */
484	div :global(a) {
485		color: var(--primary);
486		text-decoration: underline;
487		text-underline-offset: 2px;
488		transition: color 0.2s ease;
489	}
490
491	div :global(a:hover) {
492		color: var(--primary);
493	}
494
495	/* Lists */
496	div :global(ul) {
497		list-style-type: disc;
498		margin-left: 1.5rem;
499		margin-bottom: 1rem;
500	}
501
502	div :global(ol) {
503		list-style-type: decimal;
504		margin-left: 1.5rem;
505		margin-bottom: 1rem;
506	}
507
508	div :global(li) {
509		margin-bottom: 0.25rem;
510		padding-left: 0.5rem;
511	}
512
513	div :global(li::marker) {
514		color: var(--muted-foreground);
515	}
516
517	/* Nested lists */
518	div :global(ul ul) {
519		list-style-type: circle;
520		margin-top: 0.25rem;
521		margin-bottom: 0.25rem;
522	}
523
524	div :global(ol ol) {
525		list-style-type: lower-alpha;
526		margin-top: 0.25rem;
527		margin-bottom: 0.25rem;
528	}
529
530	/* Task lists */
531	div :global(.task-list-item) {
532		list-style: none;
533		margin-left: 0;
534		padding-left: 0;
535	}
536
537	div :global(.task-list-item-checkbox) {
538		margin-right: 0.5rem;
539		margin-top: 0.125rem;
540	}
541
542	/* Blockquotes */
543	div :global(blockquote) {
544		border-left: 4px solid var(--border);
545		padding: 0.5rem 1rem;
546		margin: 1.5rem 0;
547		font-style: italic;
548		color: var(--muted-foreground);
549		background: var(--muted);
550		border-radius: 0 0.375rem 0.375rem 0;
551	}
552
553	/* Tables */
554	div :global(table) {
555		width: 100%;
556		margin: 1.5rem 0;
557		border-collapse: collapse;
558		border: 1px solid var(--border);
559		border-radius: 0.375rem;
560		overflow: hidden;
561	}
562
563	div :global(th) {
564		background: hsl(var(--muted) / 0.3);
565		border: 1px solid var(--border);
566		padding: 0.5rem 0.75rem;
567		text-align: left;
568		font-weight: 600;
569	}
570
571	div :global(td) {
572		border: 1px solid var(--border);
573		padding: 0.5rem 0.75rem;
574	}
575
576	div :global(tr:nth-child(even)) {
577		background: hsl(var(--muted) / 0.1);
578	}
579
580	/* User message markdown should keep table borders visible on light primary backgrounds */
581	div.markdown-user-content :global(table),
582	div.markdown-user-content :global(th),
583	div.markdown-user-content :global(td),
584	div.markdown-user-content :global(.table-wrapper) {
585		border-color: currentColor;
586	}
587
588	/* Horizontal rules */
589	div :global(hr) {
590		border: none;
591		border-top: 1px solid var(--border);
592		margin: 1.5rem 0;
593	}
594
595	/* Images */
596	div :global(img) {
597		border-radius: 0.5rem;
598		box-shadow:
599			0 1px 3px 0 rgb(0 0 0 / 0.1),
600			0 1px 2px -1px rgb(0 0 0 / 0.1);
601		margin: 1.5rem 0;
602		max-width: 100%;
603		height: auto;
604	}
605
606	/* Code blocks */
607
608	div :global(.code-block-wrapper) {
609		margin: 1.5rem 0;
610		border-radius: 0.75rem;
611		overflow: hidden;
612		border: 1px solid var(--border);
613		background: var(--code-background);
614	}
615
616	div :global(.code-block-header) {
617		display: flex;
618		justify-content: space-between;
619		align-items: center;
620		padding: 0.5rem 1rem;
621		background: hsl(var(--muted) / 0.5);
622		border-bottom: 1px solid var(--border);
623		font-size: 0.875rem;
624	}
625
626	div :global(.code-language) {
627		color: var(--code-foreground);
628		font-weight: 500;
629		font-family:
630			ui-monospace, SFMono-Regular, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas,
631			'Liberation Mono', Menlo, monospace;
632		text-transform: uppercase;
633		font-size: 0.75rem;
634		letter-spacing: 0.05em;
635	}
636
637	div :global(.code-block-actions) {
638		display: flex;
639		align-items: center;
640		gap: 0.5rem;
641	}
642
643	div :global(.copy-code-btn),
644	div :global(.preview-code-btn) {
645		display: flex;
646		align-items: center;
647		justify-content: center;
648		padding: 0;
649		background: transparent;
650		color: var(--code-foreground);
651		cursor: pointer;
652		transition: all 0.2s ease;
653	}
654
655	div :global(.copy-code-btn:hover),
656	div :global(.preview-code-btn:hover) {
657		transform: scale(1.05);
658	}
659
660	div :global(.copy-code-btn:active),
661	div :global(.preview-code-btn:active) {
662		transform: scale(0.95);
663	}
664
665	div :global(.code-block-wrapper pre) {
666		background: transparent;
667		padding: 1rem;
668		margin: 0;
669		overflow-x: auto;
670		border-radius: 0;
671		border: none;
672		font-size: 0.875rem;
673		line-height: 1.5;
674	}
675
676	div :global(pre) {
677		background: var(--muted);
678		margin: 1.5rem 0;
679		overflow-x: auto;
680		border-radius: 1rem;
681		border: none;
682	}
683
684	div :global(code) {
685		background: transparent;
686		color: var(--code-foreground);
687	}
688
689	/* Mentions and hashtags */
690	div :global(.mention) {
691		color: hsl(var(--primary));
692		font-weight: 500;
693		text-decoration: none;
694	}
695
696	div :global(.mention:hover) {
697		text-decoration: underline;
698	}
699
700	div :global(.hashtag) {
701		color: hsl(var(--primary));
702		font-weight: 500;
703		text-decoration: none;
704	}
705
706	div :global(.hashtag:hover) {
707		text-decoration: underline;
708	}
709
710	/* Advanced table enhancements */
711	div :global(table) {
712		transition: all 0.2s ease;
713	}
714
715	div :global(table:hover) {
716		box-shadow:
717			0 4px 6px -1px rgb(0 0 0 / 0.1),
718			0 2px 4px -2px rgb(0 0 0 / 0.1);
719	}
720
721	div :global(th:hover),
722	div :global(td:hover) {
723		background: var(--muted);
724	}
725
726	/* Disable hover effects when rendering user messages */
727	.markdown-user-content :global(a),
728	.markdown-user-content :global(a:hover) {
729		color: var(--primary-foreground);
730	}
731
732	.markdown-user-content :global(table:hover) {
733		box-shadow: none;
734	}
735
736	.markdown-user-content :global(th:hover),
737	.markdown-user-content :global(td:hover) {
738		background: inherit;
739	}
740
741	/* Enhanced blockquotes */
742	div :global(blockquote) {
743		transition: all 0.2s ease;
744		position: relative;
745	}
746
747	div :global(blockquote:hover) {
748		border-left-width: 6px;
749		background: var(--muted);
750		transform: translateX(2px);
751	}
752
753	div :global(blockquote::before) {
754		content: '"';
755		position: absolute;
756		top: -0.5rem;
757		left: 0.5rem;
758		font-size: 3rem;
759		color: var(--muted-foreground);
760		font-family: serif;
761		line-height: 1;
762	}
763
764	/* Enhanced images */
765	div :global(img) {
766		transition: all 0.3s ease;
767		cursor: pointer;
768	}
769
770	div :global(img:hover) {
771		transform: scale(1.02);
772		box-shadow:
773			0 10px 15px -3px rgb(0 0 0 / 0.1),
774			0 4px 6px -4px rgb(0 0 0 / 0.1);
775	}
776
777	/* Image zoom overlay */
778	div :global(.image-zoom-overlay) {
779		position: fixed;
780		top: 0;
781		left: 0;
782		right: 0;
783		bottom: 0;
784		background: rgba(0, 0, 0, 0.8);
785		display: flex;
786		align-items: center;
787		justify-content: center;
788		z-index: 1000;
789		cursor: pointer;
790	}
791
792	div :global(.image-zoom-overlay img) {
793		max-width: 90vw;
794		max-height: 90vh;
795		border-radius: 0.5rem;
796		box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
797	}
798
799	/* Enhanced horizontal rules */
800	div :global(hr) {
801		border: none;
802		height: 2px;
803		background: linear-gradient(to right, transparent, var(--border), transparent);
804		margin: 2rem 0;
805		position: relative;
806	}
807
808	div :global(hr::after) {
809		content: '';
810		position: absolute;
811		top: 50%;
812		left: 50%;
813		transform: translate(-50%, -50%);
814		width: 1rem;
815		height: 1rem;
816		background: var(--border);
817		border-radius: 50%;
818	}
819
820	/* Scrollable tables */
821	div :global(.table-wrapper) {
822		overflow-x: auto;
823		margin: 1.5rem 0;
824		border-radius: 0.5rem;
825		border: 1px solid var(--border);
826	}
827
828	div :global(.table-wrapper table) {
829		margin: 0;
830		border: none;
831	}
832
833	/* Responsive adjustments */
834	@media (max-width: 640px) {
835		div :global(h1) {
836			font-size: 1.5rem;
837		}
838
839		div :global(h2) {
840			font-size: 1.25rem;
841		}
842
843		div :global(h3) {
844			font-size: 1.125rem;
845		}
846
847		div :global(table) {
848			font-size: 0.875rem;
849		}
850
851		div :global(th),
852		div :global(td) {
853			padding: 0.375rem 0.5rem;
854		}
855
856		div :global(.table-wrapper) {
857			margin: 0.5rem -1rem;
858			border-radius: 0;
859			border-left: none;
860			border-right: none;
861		}
862	}
863
864	/* Dark mode adjustments */
865	@media (prefers-color-scheme: dark) {
866		div :global(blockquote:hover) {
867			background: var(--muted);
868		}
869	}
870</style>