···6262// which is what applyTheme() toggles. This correctly reflects explicit
6363// user overrides, not just the OS-level prefers-color-scheme.
6464const useSystemTheme = () => {
6565- const [theme, setTheme] = useState<'light' | 'dark'>('light')
6565+ // Read the DOM directly so the initial render uses the correct theme.
6666+ // The inline script in root.tsx applies the `dark` class synchronously
6767+ // before React mounts, so this lazy initializer always sees the right value.
6868+ const [theme, setTheme] = useState<'light' | 'dark'>(() =>
6969+ document.documentElement.classList.contains('dark') ? 'dark' : 'light'
7070+ )
66716772 useEffect(() => {
6873 const isDark = () => document.documentElement.classList.contains('dark')
6969- setTheme(isDark() ? 'dark' : 'light')
70747175 const observer = new MutationObserver(() => {
7276 setTheme(isDark() ? 'dark' : 'light')
···9195 const [imageError, setImageError] = useState<string | null>(null)
9296 const imageErrorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
9397 const systemTheme = useSystemTheme()
9898+ const processGenRef = useRef(0)
9499 const editorParentRef = useRef<HTMLDivElement | null>(null)
95100 const previewRef = useRef<HTMLDivElement | null>(null)
96101 const viewRef = useRef<EditorView | null>(null)
···131136 imageErrorTimerRef.current = setTimeout(() => setImageError(null), 4000)
132137 }
133138134134- // Process markdown to HTML using shared processor
139139+ // Process markdown to HTML using shared processor.
140140+ // processGenRef is a generation counter that guards against stale async results.
141141+ // Two calls can be in-flight simultaneously — most commonly on page load when
142142+ // useSystemTheme's initial render uses the correct theme but a theme-change
143143+ // effect can still fire and kick off a second call. Shiki loads each theme's
144144+ // highlighter lazily and concurrently, so the earlier call can resolve last
145145+ // and overwrite the correct HTML. Stamping each call with the current counter
146146+ // and discarding any result whose stamp no longer matches ensures only the
147147+ // most recently dispatched call wins.
135148 const processMarkdownContent = useCallback(async (markdownText: string) => {
149149+ const gen = ++processGenRef.current
136150 setIsProcessing(true)
137151 try {
138152 const htmlString = await processMarkdown(markdownText, systemTheme)
153153+ if (gen !== processGenRef.current) return // a newer call is already in flight; discard
139154 setHtml(htmlString)
140155 } catch (error) {
156156+ if (gen !== processGenRef.current) return
141157 logger.error({ error }, 'Error processing Markdown.')
142158 // Use safe HTML for error message - no user content included
143159 setHtml('<div style="color: red; padding: 1rem; border: 1px solid red; border-radius: 4px; background: rgba(255,0,0,0.1);">⚠️ Error processing markdown. Please check your syntax.</div>')
144160 } finally {
145145- setIsProcessing(false)
161161+ if (gen === processGenRef.current) setIsProcessing(false)
146162 }
147163 }, [systemTheme])
148164