PARVASHWANI 2 gün önce
işleme
57d82e004b
50 değiştirilmiş dosya ile 22183 ekleme ve 0 silme
  1. 16 0
      .dockerignore
  2. 15 0
      .gitignore
  3. 1787 0
      CHANGELOG.md
  4. 55 0
      Dockerfile
  5. 201 0
      LICENSE
  6. 456 0
      README.md
  7. BIN
      assets/Black and Beige Simple Coming Soon Banner.png
  8. BIN
      assets/code.png
  9. BIN
      assets/github.png
  10. BIN
      assets/icon.jpg
  11. BIN
      assets/live-peview.gif
  12. BIN
      assets/mathexp.png
  13. BIN
      assets/mermaid.png
  14. BIN
      assets/table.png
  15. 21 0
      desktop-app/.dockerignore
  16. 28 0
      desktop-app/.gitignore
  17. 25 0
      desktop-app/Dockerfile
  18. 21 0
      desktop-app/LICENSE
  19. 112 0
      desktop-app/README.md
  20. 124 0
      desktop-app/build-windows.js
  21. 14 0
      desktop-app/docker-compose.yml
  22. 68 0
      desktop-app/neutralino.config.json
  23. 12 0
      desktop-app/package-lock.json
  24. 21 0
      desktop-app/package.json
  25. 238 0
      desktop-app/prepare.js
  26. 121 0
      desktop-app/resources/js/main.js
  27. 531 0
      desktop-app/resources/js/neutralino.d.ts
  28. 497 0
      desktop-app/resources/js/preview-worker.js
  29. 60 0
      desktop-app/setup-binaries.js
  30. 14 0
      docker-compose.yml
  31. 1117 0
      index.html
  32. 42 0
      manifest.json
  33. 497 0
      preview-worker.js
  34. 5 0
      robots.txt
  35. 167 0
      sample.md
  36. 9922 0
      script.js
  37. 24 0
      sitemap.xml
  38. 3873 0
      styles.css
  39. 116 0
      sw.js
  40. 194 0
      wiki/Configuration.md
  41. 179 0
      wiki/Contributing.md
  42. 179 0
      wiki/Desktop-App.md
  43. 88 0
      wiki/Development-Journey.md
  44. 214 0
      wiki/Docker-Deployment.md
  45. 99 0
      wiki/FAQ.md
  46. 232 0
      wiki/Features.md
  47. 75 0
      wiki/Home.md
  48. 206 0
      wiki/Installation.md
  49. 340 0
      wiki/Markdown-Reference.md
  50. 177 0
      wiki/Usage-Guide.md

+ 16 - 0
.dockerignore

@@ -0,0 +1,16 @@
+# PERF-019: Exclude unnecessary files from Docker build context
+.git
+.github
+.gitignore
+.dockerignore
+desktop-app
+wiki
+node_modules
+*.md
+LICENSE
+docker-compose.yml
+Dockerfile
+
+# Exclude large demo assets not needed at runtime
+assets/live-peview.gif
+assets/*.gif

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+# Dependency directories
+node_modules/
+
+# Local offline bundled assets
+desktop-app/resources/libs/
+
+# Testing reports and outputs
+playwright-report/
+test-results/
+.pytest_cache/
+__pycache__/
+
+# OS files
+.DS_Store
+Thumbs.db

+ 1787 - 0
CHANGELOG.md

@@ -0,0 +1,1787 @@
+# Changelog
+
+All notable code changes to **Markdown Viewer** are documented here.
+Non-code commits (documentation, planning, README-only updates) are excluded.
+
+## v3.7.4
+
+- **Description:** Delivered substantial feature additions and reliability improvements, including language expansion, desktop app startup fix, export centering, re-engineered PDF page breaking, and advanced Find & Replace features.
+  - **Internationalization (i18n):** Added 9 new UI languages and synchronized desktop resources to improve global accessibility.
+  - **Desktop App Startup:** Fixed Windows desktop app startup issues for improved launch reliability.
+  - **HTML Export Centering:** Added support for script-disabled alignment centering for HTML exports (fixes #152).
+  - **Re-engineered PDF Page Breaks & Scaling:** Resolved text slicing, table pagination, layout overlaps, blockquote/callout pagination splits, and margin-collapse layout shifting. Serialized rendered Mermaid diagrams to base64 images for physical scaling and gap-less page breaks (fixes #166).
+  - **TOC Smooth Scrolling:** Resolved smooth scroll navigation for Table of Contents anchor links and prevented hash/address bar updates (fixes #169).
+  - **Find & Replace Pane Synchronization:** Implemented match highlighting and auto-scrolling to matches in the Preview Pane, and fixed alignment of editor pane highlights (fixes #169).
+- **Date:** 2026-06-10
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/8ea71a809063effe849b418da5e4b4527102a10f
+
+---
+
+## v3.7.3
+
+- **Description:** Delivered critical rendering, export, and editor reliability fixes across the application.
+  - **Large Document Performance:** Re-engineered the preview rendering pipeline for large documents, improving editor responsiveness and fixing preview rendering failures on large files.
+  - **PDF Export Improvements:** Improved PDF generation UX with better progress feedback, fixed exported document centering, and resolved Mermaid diagram rendering failures in PDF export.
+  - **Find & Replace Scrolling:** Fixed find match scrolling so navigating between search results correctly scrolls the matched text into view.
+  - **Toolbar Markdown Preservation:** Fixed toolbar formatting actions to preserve existing markdown content instead of overwriting it.
+- **Date:** 2026-06-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a6d062d9696d958904f493d82d8bc2299522f5e2
+
+---
+
+## v3.7.2
+
+- **Description:** Implemented custom editor history, a new clear document action, tab layout enhancements, and visual optimizations for theme switching.
+  - **Editor Undo/Redo & Actions:** Implemented custom, robust in-memory document editing history state trackers (Undo/Redo stack) and added a dedicated "Clear Document" toolbar button.
+  - **Tabs Layout & Overflow:** Relocated the "New Tab" button and engineered dynamic container overflow handling to prevent navigation toolbar wrap.
+  - **Mermaid.js Theme Change Synchronization:** Deferred diagram re-rendering and added synchronized fade transitions during theme changes to prevent structural canvas errors, while preserving instant color switches.
+  - **Accessibility & CSS Refactoring:** Removed redundant mobile layout direction buttons; enabled window scroll on mobile viewports for menu access; and cleaned up outer layout scrollbars.
+- **Date:** 2026-06-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4a9cc6a0b602a377a1e30e64205fff7733472ed8
+
+---
+
+## v3.7.1
+
+- **Description:** Scoped performance optimizations, accessibility remediation, onboarding template stabilization, and library upgrades.
+  - **Performance & Reflows:** Scoped in-memory caching to editor geometry and gutter layouts to eliminate forced synchronous reflows; debounced resize layout listeners and deferred non-critical startup initializations to accelerate page load and cut Total Blocking Time (TBT).
+  - **Accessibility (a11y):** Resolved multiple Lighthouse accessibility violations; fixed line-number contrast ratios for both light and dark themes; increased touch target sizes conforming to WCAG AA guidelines.
+  - **Onboarding & Templating:** Stabilized the onboarding template initialization (resolving `BUG-ONBOARD-001`); inlined the default welcome markdown; and migrated it to a `<script type="text/markdown">` container to seamlessly preserve raw HTML tags in default content without parser interference.
+  - **Library Upgrades:** Upgraded the embedded Mermaid.js diagrams rendering library to the latest version, enhancing visual layouts and rendering stability.
+- **Date:** 2026-06-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/e9fef8adfa8201661ee4af18c22f941496e5308b
+
+---
+
+## v3.7.0
+
+- **Description:** Complete architectural performance engineering transformation and application modernization.
+  - **Performance:** Implemented eager-to-asynchronous dependency loading (Mermaid, MathJax, JoyPixels, pako, PDF export tools) unblocking critical path initial downloads; established a content-aware preview render bypass using a differential content hashing check (`_lastRenderedContent`); shifted to paint-aligned `requestAnimationFrame` for scroll synchronization.
+  - **DOM & Selectors:** Centralized tab list operations using a single event click delegation listener; optimized element resets by replacing 12 hot-path `innerHTML` clearing and setting lines with high-speed `textContent` updates; consolidated Find & Replace panel CSS custom variables into the top-level `:root` and `[data-theme="dark"]` scopes.
+  - **Desktop Build (prepare.js):** Programmatically stripped web-only SEO meta tags, canonical links, hreflang tags, manifests, and JSON-LD schema headers from compiled desktop resource index.html files.
+- **Date:** 2026-05-31
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/6813c1123d0da103f1032b575db4f11799ea0a0a
+
+---
+
+## v3.6.6
+
+- **Description:** Implemented extensive security hardening, accessibility remediation, and high-performance user experience upgrades. 
+  - **Security (PR 130):** Hardened offline desktop-app configuration by restricting the WebSocket communication server, enabling system process automatic cleanup on window close, and implementing secure build-time cryptographic dependency checks verifying SHA-384 integrity parameters of external assets.
+  - **UX & Performance (PR 131):** Overhauled workspace loading layouts with theme-aware dual-motion skeleton loaders (combining opacity pulses and horizontal shimmers), establishing visual alignment symmetry between the Editor and Preview panes. Introduced an asynchronous task scheduler for processing large markdown files (>15KB) that yields the call stack to the browser to paint preview skeletons immediately on pasting, avoiding interface freezes and ensuring completely responsive input. Added clearTimeout debouncers to dynamic live-region screen reader announcers and expanded accessible visually-hidden CSS clipping boundaries.
+- **Date:** 2026-05-31
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/28c3a7499f8be89f25b6bef32d9fa0bf9a703560
+
+---
+
+## v3.6.5
+
+- **Description:** Resolved the Find & Replace panel docking position reset bug, preserving custom panel drag-and-drop coordinates when toggling between floating and docked modes, and keeping coordinates in sync with viewport boundaries on window resize. Added a dedicated "Reset Position" button using the `bi-arrow-counterclockwise` icon in both the panel's header actions and the actions footer (improving accessibility and convenience for tablet and touch/keyboard-tab users), hiding both reset options automatically when docked. Synchronized assets with the desktop application wrapper by running the `prepare.js` compiler.
+- **Date:** 2026-05-27
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/5c6e113
+
+---
+
+## v3.6.4
+
+- **Description:** Redesigned the Find & Replace panel with AST scoping, regular expression support, and diff preview, allowing structured text replacement with context-sensitive analysis. Remediated multiple editor regressions including global shortcut interception, scroll view centering, floating position resets, and split-pane docking reflow. Optimized the mobile layout by hiding the split-pane dock toggle, removing language selector flags, prepending language text prefixes for improved usability, and fixing help/about modal triggers. Implemented a complete Brazilian Portuguese (pt-BR) translation module localizing all main editor labels, stats, tooltips, dialogs, placeholders, and search/replace options, registered a `hreflang` alternate link for Portuguese search indexation, and enabled automatic browser language detection fallback for Portuguese language preferences. Rotated the Service Worker cache namespace to `markdown-viewer-cache-v3.6.4` to trigger background cache updates.
+- **Date:** 2026-05-27
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/v3.6.4
+
+---
+
+## v3.6.3
+
+- **Description:** Implemented global and Asia-focused SEO & localization optimizations. Integrated a client-side multi-language translation engine in `script.js` supporting English, Simplified Chinese (简体中文), Japanese (日本語), and Korean (한국어) with automatic language detection based on browser preference (`navigator.language`) and URL parameters (`?lang=`). Localized all UI text labels, stats counters ("Min Read", "Words", "Chars"), tooltips, dialogs, and placeholders. Embedded `hreflang` alternate links and schema.org JSON-LD structured data in `index.html` to optimize search engine indexing and rich snippets. Registered search verification keys for Baidu and Naver, and added custom crawl directives in `robots.txt` and `sitemap.xml`. Scoped minor styling adjustments to align the font sizes of import, export, and language selector options in the header dropdowns with the stats-container font. Unified the language selector formatting across web and mobile layouts by displaying full language names on all devices instead of country flags on mobile. Updated application version to 3.6.3, rotated the service worker cache namespace to `markdown-viewer-cache-v3.6.3` to trigger background cache updates, and recompiled desktop app resources using `prepare.js` for offline synchronization.
+- **Date:** 2026-05-27
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/v3.6.3
+
+---
+
+## v3.6.2
+
+- **Description:** Systematically remediated and stabilized major performance bottlenecks across both web and desktop environments. Eliminated forced synchronous layout calculations (layout thrashing) in editor line gutter updates by implementing an in-memory `lineCache` Map inside `script.js` to store monospace text wrapping heights, dropping Total Blocking Time (TBT) during rapid typing from over 1500ms to under 15ms. Optimized initial payload weight by removing three heavy, unused dependencies (`html2pdf.bundle.min.js`, `pdfmake.min.js`, and `vfs_fonts.js`) from `index.html`, saving approximately 3.0 MB. Deferred the loading of all remaining external JavaScript libraries inside `<head>` to unblock HTML parsing, reducing First Contentful Paint (FCP) to under 0.6 seconds. Established early network preconnections and DNS prefetching for `cdnjs.cloudflare.com` and `cdn.jsdelivr.net`. Implemented an offline-first Progressive Web App (PWA) architecture by deploying `sw.js` (Service Worker) to cache local shell files and all external CDN stylesheets and scripts, yielding full offline functionality and instant Subsequent Time to Interactive (TTI) on mobile and desktop viewports. Refactored the Service Worker (sw.js) to employ a Stale-While-Revalidate (SWR) caching strategy for local application files (index.html, script.js, styles.css), serving assets instantly from the disk cache while asynchronously retrieving updates from the network. Maintained a strict Cache-First strategy for versioned stable third-party CDN libraries to avoid redundant network checks. Configured version-keyed cache namespaces (markdown-viewer-cache-v3.6.2) to support clean cache activation and stale cache invalidation in the background.
+- **Date:** 2026-05-26
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/248a76ce12bd31bd5c8d75ab66c7d62dbc3af65
+
+---
+
+## v3.6.1
+
+- **Description:** Implemented all core quality bug fixes and security hardening from the 10-agent independent quality audit. Added cryptographic SHA-384 Subresource Integrity (SRI) hashes to all external CDN script and stylesheet link tags inside `index.html`. Tightened Neutralino desktop API permissions allowlist (`nativeAllowList`) to exactly 8 required endpoints following the Principle of Least Privilege. Upgraded the desktop prepare compiler (`prepare.js`) to bundle 19 minified scripts, styles, and woff2/woff icon webfonts locally, ensuring complete 100% offline-first application execution. Intercepted file downloads/uploads inside the desktop port to use native platform file dialogue prompts (`Neutralino.os.showOpenDialog`, `Neutralino.os.showSaveDialog`) and local reads/writes (`Neutralino.filesystem`). Replaced abrupt window exit in `desktop-app/resources/js/main.js` with confirmation prompts (`Neutralino.os.showMessageBox`) to safeguard unsaved documents. Resolved mouse pointer drag lag on editor resizing by dynamically disabling container pointer-events. Added WAI-ARIA tab list controls conforming to WCAG 2.1 AA keyboard accessibility with manual selection and roving tabindex. Corrected standalone HTML export footnote rendering and reference link injections, packaging missing styles for footnotes, math equations, and Mermaid diagrams. Established a fully automated Playwright end-to-end (E2E) regression test suite verifying live rendering, tab operations, accessibility roving arrow navigation, and theme switches.
+- **Date:** 2026-05-25
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/3c76fa6b3ed285a688d67b47b4db114b2e4cd331
+
+---
+
+## v3.6.0
+ 
+- **Description:** Introduced a full Share Modal replacing the old inline copy-URL flow, with view-only and edit sharing modes, animated open/close transitions, clipboard copy feedback, Escape/backdrop-click dismissal, and a privacy notice. Enhanced share hash parsing to extract the `edit=1` flag and open the editor in the correct split or preview mode. Conducted a comprehensive audit fixing security, accessibility, performance, and desktop integration issues across the codebase: disabled `allowTaint` in `html2canvas` to prevent cross-origin taint exploits; added WAI-ARIA compliant keyboard navigation for tabs and the split-pane resizer; expanded touch targets to WCAG 2.1 AA minimums; fixed RTL editor pane padding and scrollbar layout shift; added new CSS design tokens (`--text-secondary`, `--font-mono`, `--color-danger-fg`); switched tab shortcuts to `Alt+Shift+T/W` on web to avoid hijacking browser shortcuts; made file extension matching case-insensitive; fixed shortcut key case sensitivity for the sync-scroll toggle; and redirected share links from localhost/Neutralino origins to the production URL. Fixed HTML export to correctly parse and render YAML frontmatter as a styled table before the document body. Updated desktop Neutralino config (application ID, filesystem permissions), hardened `main.js` with `typeof Neutralino` guards, fixed tray icon path and error handling, and replaced direct `applyContent()` DOM calls with a proper `NL_INITIAL_FILE_CONTENT` / `NL_IMPORT_EXTERNAL_FILE` handoff. Fixed Find & Replace capture group corruption, aligned exported Mermaid CDN to `v11.6.0`, added `aria-hidden` to all 11 modals, fixed active-tab 1px layout jump, and smoothed mobile menu transitions. Excluded the desktop app from Docker builds and added a CI guard to prevent image publishing on pull requests.
+- **Date:** 2026-05-24
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/2bb281adc1c811f7dca2b331b4867791b52b7401
+
+---
+
+## v3.5.5
+
+- **Description:** Overhauled footnote rendering to support multi-paragraph footnotes displayed inline, restored correct nested list indentation, and fixed footnote backref spacing and paragraph-splitting patterns. Broadened footnote continuation indentation handling and hardened inline parsing for content without footnotes. Also added MathJax loader configuration to support additional LaTeX packages, ensuring extended math notation renders correctly in both web and desktop modes.
+- **Date:** 2026-05-22
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/bc988ca48b118b8e05e76cbe513f79b2d4ae0ac5
+
+---
+
+## v3.5.4
+
+- **Description:** Preserved multiline `$$...$$` LaTeX blocks during markdown parsing to ensure complex MathJax equations render correctly. Added a custom markdown extension in both web and desktop scripts to tokenize and retain display math as atomic units, preventing unwanted line-breaks inside equations.
+- **Date:** 2026-05-15
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/e23425aa39064f108963a111793d621b5eb2cd50
+
+---
+
+## v3.5.3
+
+- **Description:** Added automated UI testing skill to improve reliability of Markdown Viewer interface validation.
+- **Date:** 2026-05-08
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/32489532e60d2b6e0b27b58d1d25b3c95b509701
+
+---
+
+## v3.5.2
+
+- **Description:** Repositioned the RTL/LTR direction toggle button and improved its behavior so it only affects the editor and preview panes, not the full page.
+- **Date:** 2026-05-08
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/121ceef5a06fb9b359185c95090da7b0c4b8146a
+
+---
+
+## v3.5.1
+
+- **Description:** Introduced testing skill definitions for Markdown Viewer UI features to support automated test coverage.
+- **Date:** 2026-05-08
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/25b5d20d414d02343ea65c2df67b92ecbecfbc4b
+
+---
+
+## v3.5.0
+
+- **Description:** Moved the RTL/LTR toggle from the header toolbar to the formatting toolbar, next to the Align Right button. Changed the label to show L or R text instead of an icon, and scoped direction changes to only the editor and preview area.
+- **Date:** 2026-05-08
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/32a0a3e08d1b2c4833cc249ada28e6e12195bcac
+
+---
+
+## v3.4.12
+
+- **Description:** Added text alignment toolbar buttons (left, center, right, justify) and improved rendering of GitHub-style alert blocks.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/d2071b33f1bac39b5d8a0df532909ec3b3b8cc3a
+
+---
+
+## v3.4.10
+
+- **Description:** Fixed an edge case where inserting alignment markup would fail silently on invalid values — now skips gracefully.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/85e5d364f3472147a4afdbc2c43d349f35465674
+
+---
+
+## v3.4.9
+
+- **Description:** Added a warning when an unexpected alignment value is encountered during markdown processing.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/3268d7d333ef41e458996244ea0672bfd37b3b11
+
+---
+
+## v3.4.8
+
+- **Description:** Renamed the GitHub alert marker regular expression internally for improved code clarity.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/dd0d1148c57f37816f8ce7078307ba63981aba7d
+
+---
+
+## v3.4.7
+
+- **Description:** Tightened the regex pattern used to match GitHub-style alert markers to reduce false positives.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/c3c9dc54a58c3d8e2e61b9ff653bb611b2931b87
+
+---
+
+## v3.4.6
+
+- **Description:** Hardened the alignment insertion logic and renamed the alert regex for consistency.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/61e994ee4cc14650ffc46e7dac4334547ac1b757
+
+---
+
+## v3.4.5
+
+- **Description:** Added alignment toolbar actions and fixed alert block parsing to correctly identify GitHub-style callouts.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/deb480d8a4d0ff72b52d97522b913abdde96e86e
+
+---
+
+## v3.4.4
+
+- **Description:** Fixed preview rendering of line breaks and added wrap-aware line numbers to the editor panel.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/d53a414c4e3776f975d147372d4482792641f6b7
+
+---
+
+## v3.4.3
+
+- **Description:** Optimised line number rendering performance — updates now run more efficiently on large documents.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/ab3839f119cbd75da7d22473a49c77b600ae139f
+
+---
+
+## v3.4.2
+
+- **Description:** Removed a redundant line number width recalculation call that was causing unnecessary reflows.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/0d85b187c96efc56a5fa41bdf99f2b5a11717ae4
+
+---
+
+## v3.4.1
+
+- **Description:** Clarified internal constants used for line number gutter sizing, improving code readability.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4ec2f29ef0cc60ca178055f3a7229944d83237df
+
+---
+
+## v3.4.0
+
+- **Description:** Fixed preview line breaks that were rendering incorrectly and added persistent line numbers to the editor with wrap-aware positioning.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4224079bd3c332b0eae0fc7fead05b4dc9b27d94
+
+---
+
+## v3.3.12
+
+- **Description:** Overhauled the toolbar and view system with an improved UI layout, better modal handling, and a more consistent editing experience.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a525ffe149f8eb64c5c2729213e00084854016b5
+
+---
+
+## v3.3.11
+
+- **Description:** Fixed the sync scroll toggle to remain visible across all view modes — editor, preview, and split.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/d0ce2c119c51e1dfa42c4314701d58a8bbd56c3c
+
+---
+
+## v3.3.10
+
+- **Description:** Synced desktop app resource files to align with the latest browser version changes.
+- **Date:** 2026-05-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4c51778ce8efac260f7cb72624e67c5ea1d88c90
+
+---
+
+## v3.3.8
+
+- **Description:** Simplified the find-and-replace match indexing logic for cleaner navigation through search results.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/5d26c77f14a6fe4e1bc5c669fc95934d74e6d6c4
+
+---
+
+## v3.3.7
+
+- **Description:** Refined the view toggle behaviour and fixed version number wiring in the UI.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/2dc31c8b5a46cad3d74aef7f29d0e8e4bfaa4b41
+
+---
+
+## v3.3.6
+
+- **Description:** Hardened error handling in the markdown renderer and improved find navigation stability.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/faa7078ff9a9a8d814ac0a82fdde6485e52d367a
+
+---
+
+## v3.3.5
+
+- **Description:** Implemented toolbar view mode updates and revised modal layout for a more consistent experience.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/0b37a2cdacd261be47767bb8c935c7ef5509b033
+
+---
+
+## v3.3.4
+
+- **Description:** Fixed rendering of GitHub emoji shortcodes that were not covered by the JoyPixels library.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/ad2ea3e3b84c3ee452f1aa8233604fdac15a76ae
+
+---
+
+## v3.3.3
+
+- **Description:** Added retry logic for emoji lookup failures to handle edge cases where shortcodes initially fail to resolve.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/560884e74302a1b8876f6056fce1369111808ec8
+
+---
+
+## v3.3.2
+
+- **Description:** Fixed emoji shortcode rendering so all standard GitHub emoji names display correctly in preview.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4a56585697555d150621a43f2cef4fb69f7e545e
+
+---
+
+## v3.3.1
+
+- **Description:** Enhanced the table insert popup and emoji picker with improved layout and usability.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4a18e3fc80613cedbe334c27fb5fed129e18226f
+
+---
+
+## v3.3.0
+
+- **Description:** Added toolbar modals for inserting tables, emoji, special symbols, and GitHub-style alert blocks directly from the formatting toolbar.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/b2da4d63485b279505ac35e05577ba14aa78c385
+
+---
+
+## v3.2.12
+
+- **Description:** Fixed the reference link toolbar button icon and widened the link, image, and reference insertion modals.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a6c1909f8657d196e1304a0c4a843c723cf75bba
+
+---
+
+## v3.2.11
+
+- **Description:** Added URL validation when inserting reference links — invalid URLs are now rejected before insertion.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/c367455edbbcad95dc03c96ccdfad462da17e73e
+
+---
+
+## v3.2.10
+
+- **Description:** Simplified internal reference link matching logic for better accuracy and maintainability.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/9dc506605e77728b6fb8678745ef531821f4864b
+
+---
+
+## v3.2.9
+
+- **Description:** Added inline documentation for the reference definition regex to clarify its matching behaviour.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/c1f47a750ad4bca6b66a19daa5fa232e0d24ab0b
+
+---
+
+## v3.2.8
+
+- **Description:** Fixed reference token sorting so numerically keyed references appear in the correct order.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/09e3d5b6f8a39cdd79f7265f1d9e49e0a4fd406a
+
+---
+
+## v3.2.7
+
+- **Description:** Fixed reference link definition parsing to correctly extract URL and title components.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4ad4123dd199f01ec2d0ea21260fe1526ca3ae6d
+
+---
+
+## v3.2.6
+
+- **Description:** Updated reference link previews in the modal and increased modal widths for better readability.
+- **Date:** 2026-05-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/7d35f9163c410467ffc0656a781f9b4c0bfb225d
+
+---
+
+## v3.2.5
+
+- **Description:** Modal-based insertion for links, references, and images — all three toolbar buttons now open a dialog for consistent UX.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/80fe0b862cad7bde342389fa2c1cca688c3edc65
+
+---
+
+## v3.2.4
+
+- **Description:** Tightened reference icon display and improved edge-case handling in the reference preview.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/c3d3980100dfc31c75dcaf32107370ae62c6e936
+
+---
+
+## v3.2.3
+
+- **Description:** Refined the reference link preview and fixed image upload handling inside the insertion modal.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/6d03e43c94951fd3edd18dc7f1e152fce61f9c9f
+
+---
+
+## v3.2.2
+
+- **Description:** Improved reference detection accuracy and updated label text in the reference insertion modal.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a67552b795884845483cbf0de8eb15d5c4e1a35c
+
+---
+
+## v3.2.1
+
+- **Description:** Sanitised title field inputs in the link and image insertion modals to prevent unexpected characters.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/eba538de43050123d859fe23302be61bcd791bca
+
+---
+
+## v3.2.0
+
+- **Description:** Introduced a modal-based UI for inserting links, references, and images from the editor toolbar.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/47ed137be382dab3a34a8efe89ab446229216b1f
+
+---
+
+## v3.1.5
+
+- **Description:** Implemented smart list continuation in the editor — pressing Enter inside a list automatically continues the list item.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/3499907ccef58c978f59e9854bb3a8f20bcc9587
+
+---
+
+## v3.1.4
+
+- **Description:** Added a Markdown formatting toolbar with styling buttons and updated the visual layout.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/6c370ea69e82aed37743a835c1bd8c26081e2ef5
+
+---
+
+## v3.1.3
+
+- **Description:** Initial implementation of the Markdown formatting toolbar with editing options for bold, italic, headings, and more.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/42b31e4c847d3b13b772185d09105206823f8862
+
+---
+
+## v3.1.2
+
+- **Description:** Refined button sizes and header dimensions across the toolbar for a consistent layout.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/f6eb342e940d4f953038606dd9d1b1cb75e16cf8
+
+---
+
+## v3.1.1
+
+- **Description:** Added icons to dropdown menu items for import and export actions to improve visual clarity.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4b4fa9f22d607eb49719b3adaf94a43c6f3d383b
+
+---
+
+## v3.1.0
+
+- **Description:** Implemented a tab action dropdown menu with options to rename, duplicate, or delete individual document tabs.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/80bbdcc35e8d464110c2e764b074f0f1248d6cb4
+
+---
+
+## v3.0.2
+
+- **Description:** Removed the dedicated drag-and-drop banner and replaced it with a full-window drop target with a subtle editor hint.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/444d6a1bff1a0492eceaf6c85d049f8a7c2d32dc
+
+---
+
+## v3.0.1
+
+- **Description:** Fixed the drag-leave handler to correctly ignore non-file drag events, preventing the depth counter from going out of sync.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/c7cfdd61b3ceda8333f21f28de383b2b42ef4f90
+
+---
+
+## v3.0.0
+
+- **Description:** Replaced the fixed dropzone banner with a full-window drag-and-drop overlay that activates on file drag.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/9d1904e3649ac1d838196a105c0482bb1f044ab2
+
+---
+
+## v2.9.9
+
+- **Description:** Fixed the stats container (word count, reading time) to display correctly across all viewport widths above 768px.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/9ab3f4f2d5ba7b3a74b935fee46311eb64886928
+
+---
+
+## v2.9.8
+
+- **Description:** Consolidated conflicting navbar media queries that caused a Bootstrap breakpoint conflict hiding the stats bar.
+- **Date:** 2026-05-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/63a053eaab970a8c01e45e48b7baf743852a4061
+
+---
+
+## v2.9.7
+
+- **Description:** Fixed the stats container visibility in the 768–991px viewport range where it was previously hidden.
+- **Date:** 2026-05-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/77abb998715749784e725e652f7b474e9551198f
+
+---
+
+## v2.9.6
+
+- **Description:** Fixed toolbar overflow at medium screen widths — buttons switch to icon-only and the header no longer wraps.
+- **Date:** 2026-05-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a0505287061f3721d7e7153a056fb720b8319f7b
+
+---
+
+## v2.9.5
+
+- **Description:** Prevented the header left and right sections from wrapping at narrow widths using flex and white-space constraints.
+- **Date:** 2026-05-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/f5f55d69b4a03706e86cca6c17f113e416bb1cec
+
+---
+
+## v2.9.4
+
+- **Description:** Restored bordered icon-only toolbar buttons to match the intended design reference.
+- **Date:** 2026-05-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/662c4ccaf308c0a1e997c5255979961a36678cfd
+
+---
+
+## v2.9.3
+
+- **Description:** Switched toolbar buttons to icon-only style at medium widths and centred the view mode group in the header.
+- **Date:** 2026-05-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/07c78f513034bdd06cda7645297e64c4d7d4c8e4
+
+---
+
+## v2.9.2
+
+- **Description:** Fixed the view mode group alignment by applying flex:1 for a proper three-column header layout.
+- **Date:** 2026-05-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/0d5d21ca28f16776431bb6ae85b7c7152e69d13c
+
+---
+
+## v2.9.0
+
+- **Description:** Fixed toolbar overflow at medium viewport widths (768–1079px) by hiding button text labels and switching to icon-only display.
+- **Date:** 2026-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/cb457e0eb967c5a5a1f3601309cfd6cc7bea7336
+
+---
+
+## v2.7.10
+
+- **Description:** Fixed the release workflow to exclude the staging directory from the source tarball, preventing a self-referencing tar error.
+- **Date:** 2026-04-28
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/620cdc0016662e58fbf245f3cd3b949120a2a21e
+
+---
+
+## v2.7.9
+
+- **Description:** Excluded the staging directory from the release source tarball to prevent the tar command from referencing itself.
+- **Date:** 2026-04-28
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/1af85a70c8fb4e6cb83b52142d5c20b1d25e2a0e
+
+---
+
+## v2.7.8
+
+- **Description:** Fixed the Ctrl+C keyboard shortcut on Windows to correctly copy selected text instead of always copying the full document.
+- **Date:** 2026-04-28
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/440da15482d44a9b02671069dd6e18e0178543de
+
+---
+
+## v2.7.7
+
+- **Description:** Prevented Ctrl+C from copying the entire markdown document when text is already selected in the editor on Windows.
+- **Date:** 2026-04-28
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a59cf5caa71d21a433ef4fa433d6ce855f551130
+
+---
+
+## v2.7.4
+
+- **Description:** Fixed pane widths not resetting when switching between tabs with different view modes (e.g. split to preview).
+- **Date:** 2026-04-08
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/7e82536955655d80f0c60b57ee97609fcb8c0884
+
+---
+
+## v2.7.3
+
+- **Description:** Fixed split-view pane widths to always reset when entering a non-split mode, regardless of the previous mode.
+- **Date:** 2026-04-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/6f7772c3d09e7145787c7efc9e4e7ca633d8fb37
+
+---
+
+## v2.7.2
+
+- **Description:** Fixed exported HTML files to correctly render LaTeX math equations using MathJax.
+- **Date:** 2026-04-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/beb6b944eae59458a42d1fcc0dd8ff0ad1e3c70e
+
+---
+
+## v2.7.1
+
+- **Description:** Fixed the MathJax configuration in HTML exports so both inline and block math equations render correctly.
+- **Date:** 2026-04-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/1d0219c57a0b502ea4a8cb75c46974f7e3233a64
+
+---
+
+## v2.6.10
+
+- **Description:** Persisted sync scrolling state and dark/light theme preference across page reloads using localStorage.
+- **Date:** 2026-04-02
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/d918c7155af501db86d4f94efd2705e9dcaf91be
+
+---
+
+## v2.6.9
+
+- **Description:** Enabled single-dollar sign inline LaTeX rendering via updated MathJax configuration.
+- **Date:** 2026-04-02
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/0611723baa20624d032b551ce870868a628539aa
+
+---
+
+## v2.6.8
+
+- **Description:** Normalised script tag indentation in both index files for code consistency.
+- **Date:** 2026-04-02
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4144576011aa79dfe16e602eb341b443f273bda0
+
+---
+
+## v2.6.7
+
+- **Description:** Fixed inline MathJax delimiters and reverted unintended resource changes from the branch.
+- **Date:** 2026-04-02
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/50734aee96712b73a933a3ccf8644a16008baea4
+
+---
+
+## v2.6.2
+
+- **Description:** Persisted sync scroll and theme settings to localStorage so they survive page refreshes.
+- **Date:** 2026-03-28
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/9a2f258e878f1e03f873b577d8331633fe846bac
+
+---
+
+## v2.6.1
+
+- **Description:** Added support for GitHub-style YAML frontmatter — frontmatter blocks are now parsed and hidden from preview.
+- **Date:** 2026-03-27
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4b2ee58d938172280b1768b1e1c74e49871e9920
+
+---
+
+## v2.6.0
+
+- **Description:** Added YAML frontmatter support, allowing metadata blocks at the top of markdown files to be parsed and displayed correctly.
+- **Date:** 2026-03-26
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/f1451352d2afbddd5f69af3439f67a308f25f3f2
+
+---
+
+## v2.5.8
+
+- **Description:** Added GitHub-style alert/admonition rendering for NOTE, TIP, IMPORTANT, WARNING, and CAUTION blocks.
+- **Date:** 2026-03-25
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/f8b70ec3111c893e9826148fb3b244dfdc079529
+
+---
+
+## v2.5.7
+
+- **Description:** Adjusted the alert icon fallback viewBox dimensions to display Font Awesome icons correctly.
+- **Date:** 2026-03-25
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/60981e96242cfd9b3a4fc40ef89d02e661c182f5
+
+---
+
+## v2.5.6
+
+- **Description:** Switched alert block icons to use Font Awesome SVG paths for wider compatibility.
+- **Date:** 2026-03-25
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/ffd9143dd03d0977e88d928900859fb2fdb52a08
+
+---
+
+## v2.5.5
+
+- **Description:** Updated alert block icons to use official GitHub Octicon SVG paths for NOTE, TIP, WARNING, IMPORTANT, and CAUTION.
+- **Date:** 2026-03-25
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a89d5f414f11782ffaa2e24bcc61f973d4953a4e
+
+---
+
+## v2.5.4
+
+- **Description:** Hardened alert icon rendering and fixed the note icon path that was displaying incorrectly.
+- **Date:** 2026-03-25
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/7093e325313a21d54c5f9989257c42d256976aaf
+
+---
+
+## v2.5.3
+
+- **Description:** Matched GitHub's alert title colours and icon style for NOTE, TIP, IMPORTANT, WARNING, and CAUTION callouts.
+- **Date:** 2026-03-25
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/0aa30d8d0d401d82fbcf3deb52a6ecd35af28585
+
+---
+
+## v2.5.2
+
+- **Description:** Improved readability of the admonition marker parsing code without changing behaviour.
+- **Date:** 2026-03-25
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/433bbbad55f2946b9f6e599b7d3c36b6fd5a7f28
+
+---
+
+## v2.5.1
+
+- **Description:** Fixed handling of GitHub alert markers that appear on the same line as content.
+- **Date:** 2026-03-25
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/7090d872f0a753e6115b5b9adcd0110cb1f64659
+
+---
+
+## v2.5.0
+
+- **Description:** Added initial GitHub-style admonition parsing and CSS styling for alert blocks.
+- **Date:** 2026-03-25
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/74eae5acad02ebbcef766ce912dd1bf682be5805
+
+---
+
+## v2.4.5
+
+- **Description:** Set the GitHub import modal width to 60% of the viewport on desktop and tablet for better readability.
+- **Date:** 2026-03-20
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/6ae149a365983c258ec15f4c6fd122641c40281d
+
+---
+
+## v2.4.4
+
+- **Description:** Styled the GitHub import modal to 60vw width on desktop and tablet screens.
+- **Date:** 2026-03-20
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/dce38f2be1181a2d312ab87ab78c4d3bee061a13
+
+---
+
+## v2.4.3
+
+- **Description:** Added multi-file selection support to the GitHub import modal with a select-all toggle and selected file count.
+- **Date:** 2026-03-20
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/87831ebd5e2cd31bd5c8d75ab66c7d62dbc3af65
+
+---
+
+## v2.4.2
+
+- **Description:** Added select-all and deselect-all controls to the GitHub import file browser modal.
+- **Date:** 2026-03-20
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/6c5ec91d0f39c5afc1bd05123a364dd839fbe82b
+
+---
+
+## v2.4.1
+
+- **Description:** Addressed code review feedback and finalised the GitHub import file browser implementation.
+- **Date:** 2026-03-20
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/cb10d4829f558b9acb6f24f5749129b3140b6060
+
+---
+
+## v2.4.0
+
+- **Description:** Added a mini file browser inside the GitHub import modal with rate-limiting support for the GitHub API.
+- **Date:** 2026-03-20
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/9c764986c7e174b7ae53aaf2ea213d413b6bc092
+
+---
+
+## v2.3.4
+
+- **Description:** Fixed the desktop app to open a markdown file passed as a command-line argument on launch.
+- **Date:** 2026-03-20
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/6142e6a907851d8e308a36278a29933a78644ea7
+
+---
+
+## v2.3.3
+
+- **Description:** Fixed CLI file argument handling — the desktop app now reads the file path from launch arguments and loads it into the editor automatically.
+- **Date:** 2026-03-20
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/ca0148f3e7c5f6905608c797ac83ae452c18b9ed
+
+---
+
+## v2.3.2
+
+- **Description:** Improved GitHub import modal readability and usability on both desktop and mobile screen sizes.
+- **Date:** 2026-03-19
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/60d4c000c0d4cfa09bb4ef53e5953ba3014eaca3
+
+---
+
+## v2.3.1
+
+- **Description:** Hardened the GitHub import modal's disable/enable state handling during async fetch operations.
+- **Date:** 2026-03-19
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a207b2fb11248aae2ff8bd4fc32a10f2a2365286
+
+---
+
+## v2.3.0
+
+- **Description:** Enlarged the GitHub import modal for better readability and introduced usability improvements.
+- **Date:** 2026-03-19
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/359a903102cbfc20c48bd4fa400652a30d4ea878
+
+---
+
+## v2.2.8
+
+- **Description:** Removed accidental desktop resource file churn that had been included in the PR.
+- **Date:** 2026-03-19
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/f6ca4884cb2237b965e724e3da21d530f6599565
+
+---
+
+## v2.2.7
+
+- **Description:** Addressed code review feedback on GitHub import error and loading state messaging.
+- **Date:** 2026-03-19
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/26b5430a73de443665529353ad44874f9746d421
+
+---
+
+## v2.2.6
+
+- **Description:** Merged import actions into a single dropdown and added a GitHub URL import modal to the toolbar.
+- **Date:** 2026-03-19
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/8d45c6a0c7a4634a3155cc70d71820fb14066974
+
+---
+
+## v2.2.2
+
+- **Description:** Added GitHub URL-based markdown import with support for repo, tree, blob, and raw URLs — including file discovery and selection.
+- **Date:** 2026-03-19
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/b2c3c85e4d7619bd30645015275155d61b62be69
+
+---
+
+## v2.2.1
+
+- **Description:** Addressed code review feedback on the GitHub import file selection prompt text.
+- **Date:** 2026-03-19
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/eca1c6365c8a652db2f5d3c0cb98988933ad45a2
+
+---
+
+## v2.2.0
+
+- **Description:** Added GitHub URL markdown import — users can now paste a GitHub URL and import markdown files directly.
+- **Date:** 2026-03-19
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/cac85189dff4eb4015a1d43562db5454fe22da31
+
+---
+
+## v2.1.9
+
+- **Description:** Implemented document tab support in the mobile menu with full feature parity to desktop tabs.
+- **Date:** 2026-03-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/6c16a5fe16aa215a6fa283c7dc9b130a9df3ba51
+
+---
+
+## v2.1.8
+
+- **Description:** Aligned the mobile tab UI with desktop — added the three-dot dropdown menu and a Reset all files button to the mobile tab list.
+- **Date:** 2026-03-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/448fc5c593e1ab99392742dca63cf97468490b7c
+
+---
+
+## v2.1.7
+
+- **Description:** Added a Documents section to the mobile menu showing all open tabs with switch, add, and delete support.
+- **Date:** 2026-03-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/323c642158b783a27bc5e182dc2c844ff0329974
+
+---
+
+## v2.1.5
+
+- **Description:** Fixed the tab three-dot dropdown menu to always remain visible and no longer be clipped by the scroll container.
+- **Date:** 2026-03-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/ed0169b3f12eab609e6f3527931836ee5ab492d0
+
+---
+
+## v2.1.4
+
+- **Description:** Fixed the tab action dropdown being clipped by the overflow scroll container by switching to position:fixed with dynamic positioning.
+- **Date:** 2026-03-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/68c4b22d095070333eee3dab684aadbb4ccd056d
+
+---
+
+## v2.1.3
+
+- **Description:** Fixed the three-dot menu button being hidden on inactive tabs by setting a default reduced opacity.
+- **Date:** 2026-03-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/d6585ed251096eb490392ad2c277f3e6caae58f1
+
+---
+
+## v2.1.2
+
+- **Description:** Fixed Document tab bar visibility by replacing sticky positioning with relative on the app header.
+- **Date:** 2026-03-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/854f3262a29fe0935b42dab3f1f40cd19f958e2a
+
+---
+
+## v2.1.1
+
+- **Description:** Fixed document tab bar not appearing by adding z-index and position rules to the tab bar element.
+- **Date:** 2026-03-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/874d8f7f812aea5a6e584b40683bfba4e9875d9f
+
+---
+
+## v2.0.7
+
+- **Description:** Changed overflow properties on focusable elements to improve keyboard focus ring visibility.
+- **Date:** 2026-03-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/7d8b9c0081fdc3c7eeae1abfa4ce23e289ef7ac4
+
+---
+
+## v2.0.6
+
+- **Description:** Removed overflow:hidden from link styles and increased z-index to prevent dropdowns being clipped.
+- **Date:** 2026-03-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4d4a088668fda0c2473c5181c1079e2890747917
+
+---
+
+## v2.0.5
+
+- **Description:** Overhauled the document tabs UI with a three-dot context menu, sequential tab naming, a reset button, and delete-last-tab support.
+- **Date:** 2026-03-08
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/0663392b578780c0986dff52b9991596eeb4bb85
+
+---
+
+## v2.0.4
+
+- **Description:** Added three-dot context menu to tabs with rename, duplicate, and delete actions; sequential tab naming; and a reset all button.
+- **Date:** 2026-03-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/cedd372640d718aea697a88e210786ca0db17930
+
+---
+
+## v2.0.1
+
+- **Description:** Added multi-tab workspace with localStorage-based session persistence across page reloads.
+- **Date:** 2026-03-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/2fed8d5003bd008d34562cb8ddaaacf33f9cf515
+
+---
+
+## v2.0.0
+
+- **Description:** Implemented full Document Tabs and Session Management — tab bar, drag-and-drop reordering, localStorage persistence, keyboard shortcuts (Ctrl+T, Ctrl+W), and up to 20 tabs.
+- **Date:** 2026-03-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/d8510fc8ea736e1727fcab2cc1b54f6a968070db
+
+---
+
+## v1.8.8
+
+- **Description:** Implemented CSS-variable-based syntax highlighting for dark mode so code blocks theme correctly.
+- **Date:** 2026-03-07
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/01effc7d914129f2e71485de6871f28daf1027de
+
+---
+
+## v1.8.7
+
+- **Description:** Implemented CSS-variable-based syntax highlighting for dark mode — code blocks now adapt to the active theme without a hard-coded colour scheme.
+- **Date:** 2026-03-06
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a01f20473fbd5934617999ca8f9e616c75e99127
+
+---
+
+## v1.8.6
+
+- **Description:** Changed the Markdown logo image source to a more reliable CDN-hosted URL.
+- **Date:** 2026-03-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a46877e47593ffef464772e90dc6e44a91de90f0
+
+---
+
+## v1.8.5
+
+- **Description:** Updated the Markdown logo image source in the script to fix broken logo display.
+- **Date:** 2026-03-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/c2a5a64d2d072989440f2119738af05ed293ddde
+
+---
+
+## v1.8.4
+
+- **Description:** Fixed the Ctrl+C keyboard shortcut — it now respects text selection and only copies the full document when nothing is selected.
+- **Date:** 2026-03-05
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/c6df362d586fcc80235a3dff5f1fb6279449c043
+
+---
+
+## v1.8.3
+
+- **Description:** Fixed Ctrl+C to copy only the selected text when a selection exists, instead of always copying the full markdown source.
+- **Date:** 2026-03-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/ae80a331bb792ae4f1078e457c730771e305372c
+
+---
+
+## v1.8.1
+
+- **Description:** Removed the Share via URL modal — clicking the Share button now copies the URL directly to the clipboard.
+- **Date:** 2026-03-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/331c7d4c00409309d0850cfba1b0a310b320e06c
+
+---
+
+## v1.8.0
+
+- **Description:** Replaced the Share modal with a direct clipboard copy on button click for a faster sharing experience.
+- **Date:** 2026-03-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/e5fb3aeec59eca9cab897ea34dbd4c2eb555bc03
+
+---
+
+## v1.7.10
+
+- **Description:** Removed the generated URL display from the share modal in preparation for the modal-less share flow.
+- **Date:** 2026-03-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/b0e64a6d018dd4ac6427e238d81bcb9803a75552
+
+---
+
+## v1.7.9
+
+- **Description:** Removed URL display from the share modal (earlier iteration of the share UX cleanup).
+- **Date:** 2026-03-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/1215fdabf4c66aa530ca17e9e26993d381c9fad8
+
+---
+
+## v1.7.8
+
+- **Description:** Removed URL display from share modal — clean-up iteration.
+- **Date:** 2026-03-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/6f09af509e94e790bd109d8a663274f9a3dca173
+
+---
+
+## v1.7.7
+
+- **Description:** Replaced the blocking browser alert for large markdown share URLs with a proper share modal.
+- **Date:** 2026-03-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/65077841db3401cd8ae0b85968a30d21357e677a
+
+---
+
+## v1.7.5
+
+- **Description:** Fixed an invalid SRI integrity hash for pako.min.js and prevented highlight.js from double-highlighting already-processed code blocks.
+- **Date:** 2026-03-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/73579688c5be737feef83aa22cef176a0810ed88
+
+---
+
+## v1.7.4
+
+- **Description:** Fixed the invalid SRI hash for pako.min.js and added a guard to skip re-highlighting already processed code blocks.
+- **Date:** 2026-03-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/bfc7301f1813ebfb19dd40003c10cb271225cc9b
+
+---
+
+## v1.7.2
+
+- **Description:** Fixed an invalid Docker image tag that caused PR build failures by switching to a static sha- prefix.
+- **Date:** 2026-03-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/84249045eb1ccdb82f0823efa6d257c728f94b27
+
+---
+
+## v1.7.1
+
+- **Description:** Fixed invalid Docker image tag on PR events by using a static sha- prefix instead of a dynamic one.
+- **Date:** 2026-03-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/92c2fdf0094ceade0b1523c34576f95b387cec29
+
+---
+
+## v1.7.0
+
+- **Description:** Added a Share button that encodes the current markdown as a compressed URL using pako, allowing documents to be shared via link.
+- **Date:** 2026-03-04
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/c857f3b5ac62a98b0afc45098aa8daf3d89b0ee9
+
+---
+
+## v1.6.5
+
+- **Description:** Fixed Mermaid diagram toolbar — Copy button now visible, toolbar order corrected, modal resized, and Copy action in modal works.
+- **Date:** 2026-03-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/efa5f2b8d0904429d025409c0270f7d602dd2d45
+
+---
+
+## v1.6.4
+
+- **Description:** Updated Mermaid toolbar UI: corrected button label, fixed toolbar order, resized modal, and added a working Copy button inside the modal.
+- **Date:** 2026-03-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/3abcdd743e5d018818c5fde6ddb4ee7826d7f786
+
+---
+
+## v1.6.2
+
+- **Description:** Added copy, export (PNG/SVG), and zoom toolbar controls for rendered Mermaid diagrams.
+- **Date:** 2026-03-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/03b4b596047611f65b3033536fd6d0b41b2c8ead
+
+---
+
+## v1.6.1
+
+- **Description:** Addressed code review feedback — rounded diagram dimensions, used CSS variable backgrounds, and added timestamps to exported filenames.
+- **Date:** 2026-02-27
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4de24246577a00bfebff0cf25d087d6b6d84dd7b
+
+---
+
+## v1.6.0
+
+- **Description:** Added a toolbar above each rendered Mermaid diagram with copy, export, and zoom controls.
+- **Date:** 2026-02-27
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/d75d01cc803921014ccfa1b2aa9092d72ffba349
+
+---
+
+## v1.5.5
+
+- **Description:** Merged the Neutralinojs desktop app port — Markdown Viewer now ships as a native desktop application.
+- **Date:** 2026-02-18
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/cc0c33f4c3aa930ccc24b68b38bcfc018878d385
+
+---
+
+## v1.5.4
+
+- **Description:** Fixed desktop app build output being empty on fresh clones by adding an idempotent binary setup script.
+- **Date:** 2026-02-18
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/cbc523a0bd3a764c68d618cd612da703747bc84d
+
+---
+
+## v1.5.3
+
+- **Description:** Removed the local Neutralinojs dependency and switched all commands to use npx for a lighter setup.
+- **Date:** 2026-02-17
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/b782511070e844585562b76f9c508cbaefad4507
+
+---
+
+## v1.5.2
+
+- **Description:** Added the full Neutralinojs desktop app — build scripts, Dockerfile, GitHub Actions CI/CD workflow, and README.
+- **Date:** 2026-02-17
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/7b12698630f9176a0456d5e54f65afc20e28b212
+
+---
+
+## v1.5.0
+
+- **Description:** Initial implementation of the Markdown Viewer desktop application using Neutralinojs.
+- **Date:** 2026-02-10
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/167fb64bbab593895e78e6eed7641ddc0beede21
+
+---
+
+## v1.4.4
+
+- **Description:** Fixed the Tab key in the editor — it now inserts two spaces instead of moving focus to the next element.
+- **Date:** 2026-01-22
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/2745de84c5259ffa1bd83cb2d481da7e759c024f
+
+---
+
+## v1.4.3
+
+- **Description:** Added a Tab key handler to the editor textarea that inserts two spaces instead of triggering browser focus change.
+- **Date:** 2026-01-22
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/5abab83159e01a01cc10ff5a17597348aac50a63
+
+---
+
+## v1.4.1
+
+- **Description:** Merged community feature improvements — three view mode buttons, resizable split view, sync button scoped to split mode only, and improved PDF export.
+- **Date:** 2026-01-22
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/177805d1b51765f9b9143878a59eee9d17817f2c
+
+---
+
+## v1.4.0
+
+- **Description:** Added Editor/Split/Preview view mode buttons, resizable split pane, sync scrolling scoped to split mode, and improved PDF rendering for graphics, tables, and formulas.
+- **Date:** 2026-01-10
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/9ab5ad89217cbea79abebff444db9b9be4e1b7cb
+
+---
+
+## v1.3.7
+
+- **Description:** Merged various feature improvements from the feature branch into main.
+- **Date:** 2025-10-11
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/78e0dcad4f457113b7fafa8fda0b4885a4c46ed4
+
+---
+
+## v1.3.6
+
+- **Description:** Merged Docker support contributed by the community — Markdown Viewer can now be run in a container.
+- **Date:** 2025-10-11
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/525152bcfdfa491f327ee2c3cad494f8f5762423
+
+---
+
+## v1.3.5
+
+- **Description:** Refactored PDF export for better quality and improved UI responsiveness across screen sizes.
+- **Date:** 2025-09-29
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/294422e68c73fda69122095f5118a20b995353df
+
+---
+
+## v1.3.1
+
+- **Description:** Merged optimised PDF export with a progress UI and renamed the Toggle Mode button.
+- **Date:** 2025-05-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/8a8c93c4475c17056347a858d3fd1fe5ce230da0
+
+---
+
+## v1.3.0
+
+- **Description:** Optimised the PDF export pipeline, added a progress indicator during export, and renamed the Toggle Mode button.
+- **Date:** 2025-05-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/ae15e31900840156418799fcffe263c728d4d47a
+
+---
+
+## v1.2.3
+
+- **Description:** Improved PDF export rendering quality for complex markdown elements.
+- **Date:** 2025-05-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/fc6fdc036045d166b94404a4c087c660fb93ea6d
+
+---
+
+## v1.2.2
+
+- **Description:** Fixed PDF export by replacing html2pdf with jsPDF and html2canvas for more reliable output.
+- **Date:** 2025-05-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/f06b8d0491d643abfa9effd7f0832ed77714e368
+
+---
+
+## v1.1.3
+
+- **Description:** Updated meta tags and SEO attributes and made minor UI adjustments.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a443f1e4f758da011cdeacf71e1369ff8e445c60
+
+---
+
+## v1.1.2
+
+- **Description:** Updated media queries, added a GitHub link to the header, and removed the Tips page.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/55718693eaf48cccd56541cf40c858d0c1387418
+
+---
+
+## v1.1.1
+
+- **Description:** Updated page description and keyword meta tags for better SEO.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/28a346ab6269163c686fb56fa810fd04609aa837
+
+---
+
+## v1.1.0
+
+- **Description:** Removed an incorrect image tag link that was breaking the page layout.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a5d48820f39328f9666ff7e120c36bad36c87fed
+
+---
+
+## v1.0.9
+
+- **Description:** Removed a broken image tag link from the source.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/9d0f5be04e1d23726444795f63764d2086032b87
+
+---
+
+## v1.0.8
+
+- **Description:** Updated the welcome markdown example content shown on first load.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/95fc984aa345423f2d8e23eade316ecbc0384aa5
+
+---
+
+## v1.0.7
+
+- **Description:** Updated the default welcome markdown example to a simpler and cleaner demonstration.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a569b5bc8e4a9e46737cb7a9a4c2ed3d04cada59
+
+---
+
+## v1.0.6
+
+- **Description:** Added emoji support using the JoyPixels (emoji-toolkit) library for rendering emoji shortcodes.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/b834a2725ec3799238dd5cb7eb81c9482127c344
+
+---
+
+## v1.0.5
+
+- **Description:** Added support for rendering GitHub emoji shortcodes using the JoyPixels library.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/960fbc5e5705c4809cb14224a985ed06373508cf
+
+---
+
+## v1.0.4
+
+- **Description:** Updated Mermaid to v11.6.0 and fixed a dark mode initialisation bug that caused incorrect theme on first load.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/dbcfdd4c3de048d5e505f3697b2b443af9df1ceb
+
+---
+
+## v1.0.3
+
+- **Description:** Fixed the initial Mermaid diagram theme to correctly reflect the active light or dark mode on load.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/5380946a5a03b750fe2954fab743a92401a83272
+
+---
+
+## v1.0.2
+
+- **Description:** Updated Mermaid to the latest version for improved diagram compatibility and rendering.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/da92568df567392024679c88bab900da54d00836
+
+---
+
+## v1.0.1
+
+- **Description:** Merged Mermaid diagram and LaTeX math support into main.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4e1fb019ebd9e45087132987eef2c87414bad73f
+
+---
+
+## v1.0.0
+
+- **Description:** Added support for rendering Mermaid diagrams and LaTeX math expressions in the markdown preview.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/cc53828c4ac3a164c0f4df64cd34e78ae2d15fe7
+
+---
+
+## v0.5.6
+
+- **Description:** Updated the Copy button to copy raw Markdown instead of rendered HTML.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/e7a661106a282ef5abdda85be88efdc4f20e6377
+
+---
+
+## v0.5.5
+
+- **Description:** Changed the copy button from copyHtmlBtn to copyMdBtn — the button now copies the markdown source.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/6f838dfeddbd1918a0805f3e985a297cec0c2b43
+
+---
+
+## v0.5.4
+
+- **Description:** Added a mobile hamburger menu with full access to all editor features on small screens.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/a93c940aaa988d0dd4d8a1c3f7c54c26b4d10a37
+
+---
+
+## v0.5.3
+
+- **Description:** Updated the application name displayed in the navbar.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/c3a3fa465c6ce45462a472700ceb377f8be655cc
+
+---
+
+## v0.5.2
+
+- **Description:** Fixed UI layout issues for mobile media query breakpoints.
+- **Date:** 2025-05-03
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/85689c8a31ca10a0b3d4b064ea6698f72ca7f58f
+
+---
+
+## v0.5.1
+
+- **Description:** Added a favicon and SEO meta tags for improved branding and search engine discoverability.
+- **Date:** 2025-05-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/d54a170d27e772225ac0562231e1ea01361711e3
+
+---
+
+## v0.5.0
+
+- **Description:** Added a favicon and SEO meta tags (description, keywords, Open Graph) to the page.
+- **Date:** 2025-05-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/b761575f3b8ef85ec6c4306ca81667a396548432
+
+---
+
+## v0.4.10
+
+- **Description:** Removed unwanted padding from the editor panel for a cleaner layout.
+- **Date:** 2025-05-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/ec49a606141a7652c0a00f17e2af586a36199778
+
+---
+
+## v0.4.9
+
+- **Description:** Added a drag-and-drop toggle and applied various CSS fixes for the dropzone area.
+- **Date:** 2025-05-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/8734259c64b8a35f292c7f2b786bbac8adbacbbf
+
+---
+
+## v0.4.8
+
+- **Description:** Fixed a toolbar rendering issue causing misaligned buttons.
+- **Date:** 2025-05-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/5be5e86d04505549152cd4f737e37605866099be
+
+---
+
+## v0.4.7
+
+- **Description:** Added a close button to the drag-and-drop zone so it can be dismissed after use.
+- **Date:** 2025-05-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/9fc7eeca6142231ae03777b37f3f0abf6daac1ee
+
+---
+
+## v0.4.6
+
+- **Description:** Merged the new UI overhaul with theme toggle, dropzone support, and improved markdown rendering.
+- **Date:** 2025-05-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/6bf47347647cb7803bdf94e441d2bdce94d3e3c6
+
+---
+
+## v0.4.5
+
+- **Description:** Removed the legacy LivePage directory from the repository.
+- **Date:** 2025-05-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/ab4b5c05cfeadd2e388c25cf07dc80a3a6014e24
+
+---
+
+## v0.4.4
+
+- **Description:** Updated sync scrolling and the stats card display.
+- **Date:** 2025-05-01
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/bc0526b1210eb49b07ce9e4fbea07a05388ade29
+
+---
+
+## v0.4.3
+
+- **Description:** Updated sync scrolling behaviour and refined the stats card.
+- **Date:** 2025-04-30
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/8f72846e07f413014aca8cbcd260c1e13cd838d9
+
+---
+
+## v0.4.2
+
+- **Description:** Fixed code block bracket colouring in dark mode toggle.
+- **Date:** 2025-04-30
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/127d274a2ab78662f7d3752378f855dcc6d078b2
+
+---
+
+## v0.4.1
+
+- **Description:** Fixed an incorrect file path reference.
+- **Date:** 2025-04-30
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/1129d0efaf0569d20affd5272a59e37d4dd77287
+
+---
+
+## v0.4.0
+
+- **Description:** Introduced a new UI design for the editor.
+- **Date:** 2025-04-30
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/59c4a86cfb7825601787c4f05a259e0a5342a451
+
+---
+
+## v0.3.13
+
+- **Description:** Updated the index link in the navigation.
+- **Date:** 2024-08-23
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/8bdfb0d7270bb9ec060b0eeba8946da5712b1d2f
+
+---
+
+## v0.3.12
+
+- **Description:** Updated multiple source files with general improvements.
+- **Date:** 2024-08-23
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/3ac144069faf7ec17a9cdd8629eb6dd10581aac3
+
+---
+
+## v0.3.11
+
+- **Description:** Fixed a CSS styling issue.
+- **Date:** 2024-04-13
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/65401c5ae6ed5ebc6c640e5e8d2167e488b648a2
+
+---
+
+## v0.3.6
+
+- **Description:** Added tooltip support at the 1200px media query breakpoint.
+- **Date:** 2024-04-12
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/faf12c3723f61ae148c644c4164b845b1dd517f0
+
+---
+
+## v0.3.5
+
+- **Description:** Fixed a CSS layout issue.
+- **Date:** 2024-04-12
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/b370daac7b1ca68ad75fdeb27525c1a1e116e518
+
+---
+
+## v0.3.4
+
+- **Description:** Added additional toolbar buttons with tooltips.
+- **Date:** 2024-04-12
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/10b87b5cd6de5c8ecc8cb30bb0ad7e8f37dbe6ca
+
+---
+
+## v0.3.3
+
+- **Description:** Fixed a spelling error in the UI.
+- **Date:** 2024-04-12
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/4bef4bda95890d5ff696b11915f828993aab40b4
+
+---
+
+## v0.3.2
+
+- **Description:** Added a reading stats panel showing word count, character count, and estimated reading time.
+- **Date:** 2024-04-12
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/b60b9020912e81d8c00551de19155ab5e47a7f57
+
+---
+
+## v0.3.0
+
+- **Description:** Added a Tips page and updated media query handling.
+- **Date:** 2024-04-11
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/eceb56e475549d9582aef3a25ed18b403b93735a
+
+---
+
+## v0.2.4
+
+- **Description:** Updated CSS styles for layout refinements.
+- **Date:** 2024-04-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/7cd66bad4778f65c6932a8089bdb6a7072b28d83
+
+---
+
+## v0.2.3
+
+- **Description:** Updated CSS styles for additional layout fixes.
+- **Date:** 2024-04-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/1b19bc664c2968ea217d2dbd6e8de8b91ab5a2b0
+
+---
+
+## v0.2.2
+
+- **Description:** Removed unused JavaScript code for a cleaner codebase.
+- **Date:** 2024-04-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/bfd248c48044ccd4beae4977fc1e00d00bd9058f
+
+---
+
+## v0.2.1
+
+- **Description:** Added file import functionality to load markdown files into the editor.
+- **Date:** 2024-04-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/dd9548dcc76f64a9315143ee0d18b18315f566a9
+
+---
+
+## v0.2.0
+
+- **Description:** Added a live preview panel that renders markdown content in real time.
+- **Date:** 2024-04-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/3d6ff67f8fc0d52f35cb9e6a98cc3f197073356a
+
+---
+
+## v0.1.7
+
+- **Description:** Updated element colours for improved visual style.
+- **Date:** 2024-04-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/801f830768cee83ab70ead1882f78486edba05b9
+
+---
+
+## v0.1.6
+
+- **Description:** Fixed a border layout issue in the editor.
+- **Date:** 2024-04-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/29578f1fd0da242572d64a9daded27023516387e
+
+---
+
+## v0.1.5
+
+- **Description:** Fixed the page layout and added the logo to the navigation bar.
+- **Date:** 2024-04-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/9fad69c959aa64d56396c5948a8f2f5f34af5b11
+
+---
+
+## v0.1.4
+
+- **Description:** Fixed navbar and media query layout issues.
+- **Date:** 2024-04-09
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/42db836fc05cda88edd52668fa8f7f6b90ad955a
+
+---
+
+## v0.1.3
+
+- **Description:** Updated the overall page layout.
+- **Date:** 2024-04-08
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/ecf02617c6d244e2dddac170f0c5550e3e8dc7e1
+
+---
+
+## v0.1.2
+
+- **Description:** Added navigation and export functionality.
+- **Date:** 2024-04-08
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/37a3ef0e31900f24a71d323733496db5621b9464
+
+---
+
+## v0.1.1
+
+- **Description:** Added the initial markdown editor page.
+- **Date:** 2024-04-08
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/2b37ae90bb8e60418469b806cfc3b67ad9ff2059
+
+---
+
+## v0.1.0
+
+- **Description:** Initial commit — project created.
+- **Date:** 2024-04-08
+- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/5d34b992c1d8c4ad4dfd3cf79b5a627f28a163b1
+
+---

+ 55 - 0
Dockerfile

@@ -0,0 +1,55 @@
+# Use nginx as the base image for serving static files
+FROM nginx:alpine
+
+# PERF-019: Only copy necessary web files (exclude .git, desktop-app, wiki, etc.)
+COPY index.html /usr/share/nginx/html/
+COPY script.js /usr/share/nginx/html/
+COPY styles.css /usr/share/nginx/html/
+COPY sw.js /usr/share/nginx/html/
+COPY manifest.json /usr/share/nginx/html/
+COPY robots.txt /usr/share/nginx/html/
+COPY sitemap.xml /usr/share/nginx/html/
+COPY assets/icon.jpg /usr/share/nginx/html/assets/
+
+# Create a custom nginx configuration with compression and security
+# PERF-020: Added gzip compression for text-based assets
+RUN echo 'server { \
+    listen 80; \
+    server_name localhost; \
+    root /usr/share/nginx/html; \
+    index index.html; \
+    \
+    # Enable gzip compression (PERF-020) \
+    gzip on; \
+    gzip_vary on; \
+    gzip_proxied any; \
+    gzip_comp_level 6; \
+    gzip_min_length 256; \
+    gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml; \
+    \
+    # Handle client-side routing for SPA \
+    location / { \
+    try_files $uri $uri/ /index.html; \
+    } \
+    \
+    # Cache static assets \
+    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { \
+    expires 1y; \
+    add_header Cache-Control "public, immutable"; \
+    } \
+    \
+    # Security headers \
+    add_header X-Frame-Options "SAMEORIGIN" always; \
+    add_header X-Content-Type-Options "nosniff" always; \
+    add_header X-XSS-Protection "1; mode=block" always; \
+    add_header Referrer-Policy "strict-origin-when-cross-origin" always; \
+    # PERF-029: Content Security Policy for defense-in-depth \
+    add_header Content-Security-Policy "default-src '"'"'self'"'"'; script-src '"'"'self'"'"' cdnjs.cloudflare.com cdn.jsdelivr.net '"'"'unsafe-inline'"'"'; style-src '"'"'self'"'"' cdnjs.cloudflare.com cdn.jsdelivr.net '"'"'unsafe-inline'"'"'; img-src '"'"'self'"'"' https: data: blob:; font-src '"'"'self'"'"' cdn.jsdelivr.net; connect-src '"'"'self'"'"' api.github.com raw.githubusercontent.com;" always; \
+    }' > /etc/nginx/conf.d/default.conf
+
+# Expose port 80
+EXPOSE 80
+
+# Start nginx
+CMD ["nginx", "-g", "daemon off;"]
+

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2024 ThisIs-Developer
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 456 - 0
README.md

@@ -0,0 +1,456 @@
+<div align="center">
+<h1>Markdown Viewer</h1>
+  
+  <img src="assets/icon.jpg" alt="Markdown Viewer Logo" width="140" />
+</div>
+
+<div align="center">
+  <p><strong>A Markdown Editor That Lives in Your Browser, Desktop, and a Single URL.</strong></p>
+  <p>Fast GitHub-style Markdown editing with live preview, diagrams, LaTeX, syntax highlighting, PDF export, and multi-tab support across web, desktop, and Docker.</p>
+
+  <p>
+    <a href="https://markdownviewer.pages.dev/" target="_blank" rel="noopener noreferrer"><strong>Live Production Demo</strong></a> ·
+    <a href="wiki/Home" rel="noopener noreferrer">Documentation Wiki</a> ·
+    <a href="https://github.com/ThisIs-Developer/Markdown-Viewer/issues" target="_blank" rel="noopener noreferrer">Issue Tracker</a> ·
+    <a href="https://github.com/ThisIs-Developer/Markdown-Viewer/releases" target="_blank" rel="noopener noreferrer">Releases</a>
+  </p>
+
+  <p>
+    <img alt="License" src="https://img.shields.io/github/license/ThisIs-Developer/Markdown-Viewer?color=2ea043" />
+    <img alt="Latest release" src="https://img.shields.io/github/v/release/ThisIs-Developer/Markdown-Viewer" />
+    <img alt="Last commit" src="https://img.shields.io/github/last-commit/ThisIs-Developer/Markdown-Viewer" />
+    <img alt="Stars" src="https://img.shields.io/github/stars/ThisIs-Developer/Markdown-Viewer?style=flat" />
+  </p>
+
+  <p>
+    <a href="https://codewiki.google/github.com/thisis-developer/markdown-viewer" target="_blank" rel="noopener noreferrer">
+      <img src="https://img.shields.io/badge/CodeWiki-Explore-4285F4?logo=wikipedia&logoColor=white&style=flat" alt="CodeWiki" />
+    </a>
+    <a href="https://deepwiki.com/ThisIs-Developer/Markdown-Viewer" target="_blank" rel="noopener noreferrer">
+      <img src="https://deepwiki.com/badge.svg" alt="DeepWiki" />
+    </a>
+    <a href="https://oosmetrics.com/repo/ThisIs-Developer/Markdown-Viewer" target="_blank" rel="noopener noreferrer">
+      <img src="https://api.oosmetrics.com/api/v1/badge/achievement/b13c27be-447e-489d-a04d-55f7ccaf9175.svg" alt="OOSMetrics" />
+    </a>
+  </p>
+</div>
+
+<p align="center">
+  <img src="https://github.com/user-attachments/assets/7f4af5d3-ecae-47ac-9f27-2f91e4a6d866" alt="Markdown Viewer - Live split-screen Markdown editor and previewer with GFM rendering, tabbed multi-document workspace, and dark theme support" width="100%" />
+</p>
+
+
+## Table of Contents
+
+- [About the Project](#about-the-project)
+- [Key Features](#key-features)
+- [System Architecture](#system-architecture)
+  - [High-Level Architecture Diagram](#high-level-architecture-diagram)
+  - [Core File Walkthrough](#core-file-walkthrough)
+- [Under-the-Hood Subsystems Deep-Dive](#under-the-hood-subsystems-deep-dive)
+  - [1. Global State & Session Persistence](#1-global-state--session-persistence)
+  - [2. Document Tab & Session Lifecycle](#2-document-tab--session-lifecycle)
+  - [3. Tab Overflow & Navigation](#3-tab-overflow--navigation)
+  - [4. Responsive Pane Resizer & View Mode Layout Controller](#4-responsive-pane-resizer--view-mode-layout-controller)
+  - [5. Rich Text Editor History & Undo/Redo Engine](#5-rich-text-editor-history--undoredo-engine)
+  - [6. Dynamic Line-Number Gutter & Selection Highlighter](#6-dynamic-line-number-gutter--selection-highlighter)
+  - [7. Web Worker Segmented Markdown Compilation & Sanitization](#7-web-worker-segmented-markdown-compilation--sanitization)
+  - [8. Throttled Bidirectional Scroll Synchronization](#8-throttled-bidirectional-scroll-synchronization)
+  - [9. Interactive Mermaid Diagram & MathJax LaTeX Renderer](#9-interactive-mermaid-diagram--mathjax-latex-renderer)
+  - [10. Draggable Find/Replace Search & Diff Preview Engine](#10-draggable-findreplace-search--diff-preview-engine)
+  - [11. Layout-Aware PDF Export & URL Sharing Subsystem](#11-layout-aware-pdf-export--url-sharing-subsystem)
+- [Getting Started & Installation](#getting-started--installation)
+  - [Option 1: Docker (Pre-built Image)](#option-1-docker-pre-built-image)
+  - [Option 2: Docker Compose (Local Build)](#option-2-docker-compose-local-build)
+  - [Option 3: Local Static Web Server](#option-3-local-static-web-server)
+  - [Option 4: Desktop Application](#option-4-desktop-application)
+- [Usage Guide & Keyboard Shortcuts](#usage-guide--keyboard-shortcuts)
+- [Project Directory Structure](#project-directory-structure)
+- [Built With (Technology Stack)](#built-with-technology-stack)
+- [Contributing & Code Quality](#contributing--code-quality)
+- [Showcase & Community Projects](#showcase--community-projects)
+- [License](#license)
+- [Contact & Support](#contact--support)
+
+---
+
+## About the Project
+
+**Markdown Viewer** is an advanced, fully client-side editing suite and previewer optimized for a professional documentation workflow. Running completely inside the browser, it renders GitHub-Flavored Markdown (GFM), math formulas, and architectural diagrams in real time. 
+
+Designed with privacy and performance at its core, the application performs all parsing in a background worker thread, employs incremental DOM patching to minimize browser repaints, and supports native offline capabilities via a Service Worker proxy. It is also packaged as a lightweight native desktop shell using the Neutralinojs framework.
+
+---
+
+## Key Features
+
+*   **🖊️ Decoupled Split-Screen Editing:** Type Markdown in a customized text editor with a dynamic line-number gutter and see updates render instantly.
+*   **📐 LaTeX Math Notation:** Full integration with MathJax for typesetting inline and block mathematical formulas.
+    <p align="center">
+
+      <img src="https://github.com/user-attachments/assets/51831f45-33e8-4788-b9ad-b239a929a2e4" alt="Markdown Viewer - LaTeX math editor rendering display and inline mathematical equations using MathJax in dark mode" width="90%" />
+    </p>
+*   **📊 Interactive Mermaid Diagrams:** Render flowcharts, Gantt charts, sequence diagrams, and more. Diagrams feature a toolbar for zooming, panning, copying, and exporting.
+    <p align="center">
+      <img src="https://github.com/user-attachments/assets/da00943c-d00a-4b76-96e9-d7bc1bb7f86c" alt="Markdown Viewer - Rendered interactive Mermaid.js diagram flowchart in preview with zoom, pan, and SVG image export toolbar" width="90%" />
+      <img src="https://github.com/user-attachments/assets/3995e614-ffff-4cc0-843d-af73d840ca86" alt="Markdown Viewer - Rendered interactive Mermaid.js diagram flowchart in preview with zoom, pan, and SVG image export toolbar" width="90%" />
+    </p>
+*   **⚡ Off-Thread Parsing & Incremental Patching:** Document parsing is offloaded to a background Web Worker. Rendered updates patch only changed DOM nodes to keep browser resources free.
+*   **📤 Professional Export Suite:** Export documents as raw Markdown (`.md`), standalone formatted HTML (`.html`) with inline styles, or high-resolution paginated PDF (`.pdf`).
+*   **📥 Multi-Source File Import:** Drag & drop local files or browse and download multiple Markdown files from a public GitHub repository URL.
+    <p align="center">
+      <img src="https://github.com/user-attachments/assets/6edbfde9-82a8-472a-a2b5-d06ffb63bcea" alt="Markdown Viewer - Import Markdown files from public GitHub repository tree with recursive directory browser and tab integration" width="90%" />
+      <img src="https://github.com/user-attachments/assets/cba06ce4-a13b-4c4b-bc70-6d53a24a8f0f" alt="Markdown Viewer - Import Markdown files from public GitHub repository tree with recursive directory browser and tab integration" width="90%" />
+    </p>
+*   **🔗 URL-Encoded Compressed Sharing:** Compress your document's text utilizing DEFLATE compression and encode it directly into a shareable URL hash. No server-side database required.
+    <p align="center">
+      <img src="https://github.com/user-attachments/assets/10957066-4bc5-4b7d-9dc0-c28b7fc61a7e" alt="Markdown Viewer - Serverless document sharing using URL-encoded DEFLATE compressed markdown hash in edit or view-only mode" width="90%" />
+    </p>
+*   **💾 Multi-Document Tab bar:** Organize multiple files inside tab components featuring drag-and-drop reordering, title renaming, and local session persistence.
+*   **🔒 Privacy & Security Focus:** No analytics, tracking beacons, or backend servers. HTML output is sanitized via DOMPurify to eliminate Cross-Site Scripting (XSS) threats.
+
+---
+
+## System Architecture
+
+Markdown Viewer is structured as a client-side single-page application (SPA). The diagram below outlines how the UI thread, background worker, service worker, browser cache, native desktop bridges, and third-party libraries interact.
+
+### High-Level Architecture Diagram
+
+```mermaid
+graph TD
+    %% Client Interface Group
+    subgraph UI ["Client Interface (Main Thread)"]
+        HTML["index.html<br>(DOM Tree)"]
+        CSS["styles.css<br>(Custom Themes & Reset)"]
+        Script["script.js<br>(UI Orchestration)"]
+        Editor["Markdown Editor<br>(Textarea + Gutter)"]
+        Preview["Preview Pane<br>(Isolated Render Area)"]
+        Modal["Mermaid Modal<br>(Zoom & Drag-to-Pan)"]
+    end
+
+    %% Background Web Worker Group
+    subgraph Worker ["Web Worker (Background Thread)"]
+        PWorker["preview-worker.js<br>(Off-Thread Compiler)"]
+        MarkedLib["Marked.js<br>(GFM Parser)"]
+        HljsLib["Highlight.js<br>(Syntax Color)"]
+    end
+
+    %% Storage Group
+    subgraph Storage ["Local Storage & Network Proxy"]
+        LS["localStorage<br>(Tabs, Settings, Session)"]
+        Cache["Browser Cache<br>(Service Worker sw.js)"]
+    end
+
+    %% Third-Party Utilities
+    subgraph CDNs ["Third-Party CDN Libraries (Lazy Loaded)"]
+        MathJax["MathJax.js<br>(LaTeX Math)"]
+        Mermaid["Mermaid.js<br>(Diagrams)"]
+        PDF["jsPDF & html2canvas<br>(PDF Export Pipeline)"]
+        Pako["Pako.js<br>(zlib share encoder)"]
+    end
+
+    %% Native Desktop Layer
+    subgraph Desktop ["NeutralinoJS Desktop Shell"]
+        Neu["Neutralino.js Bridge<br>(Native File System APIs)"]
+    end
+
+    %% Interactions
+    Editor -- "1. Input Keystrokes" --> Script
+    Script -- "2. Size-Aware Debounced Text" --> PWorker
+    PWorker -- "3. Load Scripts" --> MarkedLib
+    PWorker -- "3. Load Scripts" --> HljsLib
+    PWorker -- "4. Returns Compiled HTML Blocks & Hashes" --> Script
+    Script -- "5. Incremental Patching (replaceWith)" --> Preview
+    Script -- "6. Debounced State Auto-Save" --> LS
+    
+    %% Dynamic Loading triggers
+    Script -- "Lazy Load (Math strings detected)" --> MathJax
+    Script -- "Lazy Load (Mermaid block detected)" --> Mermaid
+    Script -- "Lazy Load (On PDF Export click)" --> PDF
+    Script -- "Lazy Load (On Share click)" --> Pako
+    
+    %% Downstream Rendering outputs
+    MathJax -- "Inject Math formulas" --> Preview
+    Mermaid -- "Draw SVGs + Toolbars" --> Preview
+    Preview -- "Double click diagram" --> Modal
+    PDF -- "Capture sandboxed canvas" --> Script
+    
+    %% Network Proxy Caching
+    Cache -. "Network-First (App Assets)" .-> HTML
+    Cache -. "Network-First (App Assets)" .-> Script
+    Cache -. "Network-First (App Assets)" .-> CSS
+    Cache -. "Cache-First (5.4MB bundles)" .-> CDNs
+    
+    %% Desktop Logic
+    Script -- "Access OS API if wrapped" --> Neu
+```
+
+### Core File Walkthrough
+
+1.  **`index.html`**: Establishes layout structures, floating panel anchors, and imports CSS files alongside core scripts using defer hooks. It keeps the default fallback markdown inside a `<script type="text/markdown" id="default-markdown">` element.
+2.  **`script.js`**: Operates as the central controller on the main UI thread. It tracks active tab states, drives the split resizing loops, handles drag-and-drop file imports, coordinates communication with the preview Web Worker, manages the multi-pass PDF layout engine, and applies language mappings.
+3.  **`styles.css`**: Configures variables for Light/Dark themes, handles layout spacing, aligns the line number gutter visually with the text editor area, and provides theme stylings for code fences.
+4.  **`preview-worker.js`**: Operates on a background thread. It parses large text structures, calculates hashes for each section, compiles Markdown to HTML using `marked.js`, applies syntax highlighting via `highlight.js`, and posts parsed output back to the main UI thread.
+5.  **`sw.js`**: A Service Worker serving as a local network proxy. It intercepts requests to cache static files on the client's device, enabling the application to run offline.
+
+---
+
+## Under-the-Hood Subsystems Deep-Dive
+
+Markdown Viewer employs custom-engineered client-side engines to deliver production-grade performance. Below is a detailed breakdown of the 11 core subsystems. For full source code listings and in-depth details of each implementation, please check the [Features Wiki](wiki/Features).
+
+### 1. Global State & Session Persistence
+The global state manages application-wide preferences (such as theme, text direction, active tab, and split-pane ratio). It uses an **in-memory shadowing cache** to skip repeated parsing/serialization cycles over the synchronous `localStorage` block (preventing blocking disk I/O). 
+
+Theme switches write the theme attribute directly to the HTML document root to avoid visual flash or full-page layout reflows during loading. CSS transitions are strictly scoped to color properties to prevent costly layout recalculations:
+
+$$\text{document.documentElement.setAttribute("data-theme", theme)}$$
+
+### 2. Document Tab & Session Lifecycle
+Document files reside in an isolated document tab array structure. Tab dragging reorders tabs using the HTML5 Drag and Drop API, updating the underlying index array. Dropdown menus are positioned relative to the tab's bounding rectangle via `getBoundingClientRect()`. Keyboard accessibility mappings (`ArrowRight`, `ArrowLeft`, `Home`, `End`, `Enter`, `Space`) coordinate focus states inside the tab-list.
+
+### 3. Tab Overflow & Navigation
+When open tabs exceed the horizontal viewport, the tab bar switches to an overflow state. Vertical mouse scroll wheel inputs are intercepted and translated to horizontal scroll coordinates to enable side-scrolling:
+
+$$\Delta X_{\text{scroll}} = \Delta Y_{\text{wheel}}$$
+
+Overflow check checks the inequality `scrollWidth > clientWidth` to toggle the visibility of click-to-scroll navigation arrows.
+
+### 4. Responsive Pane Resizer & View Mode Layout Controller
+The horizontal resizer calculates the percentage width of the editor relative to its parent container during window resizing:
+
+$$\text{Percent}_{\text{editor}} = \text{clamp}\left(\frac{X_{\text{mouse}} - X_{\text{container-left}}}{W_{\text{container}}} \times 100, 20\\%, 80\\%\right)$$
+
+The event loop tracks global resizing states on window mouse and touch move events, updating layout grid constraints via CSS properties.
+
+### 5. Rich Text Editor History & Undo/Redo Engine
+To maintain separate command histories when navigating multiple documents, the history manager maintains tab-specific undo/redo stacks. Edits are batched to avoid bloated memory allocations; updates are pushed to the history stack only when transition boundaries occur, word borders (spaces) are typed, or keyboard idle time exceeds 300ms.
+
+### 6. Dynamic Line-Number Gutter & Selection Highlighter
+To keep line numbers in the gutter aligned with wrapped text in the transparent editor area, the gutter employs font-size wrap calculations:
+
+$$\text{LineHeight} = \text{fontSize} \times 1.5$$
+
+$$\text{wrapCount} = \text{Math.ceil}\left(\frac{\text{TextLength} \times \text{CharWidth}}{\text{EditorWidth}}\right)$$
+
+DOM gutter paints are scheduled via `requestAnimationFrame` to prevent layout thrashing. A background overlay matches the text scroll coordinates to highlight find-and-replace queries.
+
+### 7. Web Worker Segmented Markdown Compilation & Sanitization
+Document parsing is offloaded to `preview-worker.js` on a background thread. Before offloading, the system runs safety checks to ensure the document contains no global definitions or complex footnotes. If safe, the worker tokenizes the text into blocks on double-newlines, calculates 32-bit FNV-1a hashes for each segment:
+
+$$H_i = (H_{i-1} \oplus d_i) \times p$$
+
+where $p = 16777619$ (FNV prime) and $H_0 = 2166136261$ (offset basis). Only modified blocks are re-parsed, saving substantial CPU cycles.
+
+### 8. Throttled Bidirectional Scroll Synchronization
+Proportional scrolling coordinates positions between the text editor and preview pane:
+
+$$Y_{\text{target}} = \frac{Y_{\text{source}}}{H_{\text{source-scroll}} - H_{\text{source-client}}} \times (H_{\text{target-scroll}} - H_{\text{target-client}})$$
+
+Scrolling feedback loops are prevented using state locks and decoupled animation schedules using `requestAnimationFrame` with a 50ms release timeout.
+
+### 9. Interactive Mermaid Diagram & MathJax LaTeX Renderer
+MathJax typesets equations asynchronously. A cleanup script strips MathJax's default assistive markup elements to prevent duplicate accessibility readings. Rendered SVG diagrams are manipulated in zoom modals using transform translation matrices:
+
+$$\mathbf{T}_{\text{svg}} = \text{translate}(X_{\text{pan}}, Y_{\text{pan}}) \times \text{scale}(S_{\text{zoom}})$$
+
+### 10. Draggable Find/Replace Search & Diff Preview Engine
+Regular expression searches are parsed inside try-catch validation locks to avoid breaking runtime operations. The floating panel coordinates are clamped inside the window bounds:
+
+$$X_{\text{clamp}} = \max(0, \min(X_{\text{mouse}}, W_{\text{window}} - W_{\text{panel}}))$$
+
+$$Y_{\text{clamp}} = \max(0, \min(Y_{\text{mouse}}, H_{\text{window}} - H_{\text{panel}}))$$
+
+A diff comparison engine computes modified line buffers to display a red/green visual preview before applying replacements.
+
+<p align="center">
+  <img src="https://github.com/user-attachments/assets/b4314cf0-8059-40f1-a445-9d24f00a23b0" alt="Markdown Viewer - Draggable find and replace panel with scoped search filters for Mermaid diagrams, LaTeX equations, and raw text diff preview" width="90%" />
+</p>
+
+### 11. Layout-Aware PDF Export & URL Sharing Subsystem
+PDF generation uses a multi-pass stabilization cascade loop (up to 10 iterations) to align layout components:
+- Inline SVGs are converted to Base64 PNGs.
+- Header elements near borders are shifted down via `.pdf-page-break-spacer` to prevent orphan tags.
+- Tables are dynamically split and `<thead>` elements are re-injected onto split tables.
+- Text element slicing is prevented by shifting lines downward past page cuts:
+
+$$\text{Shift} = (Y_{\text{boundary}} - Y_{\text{line-top}}) + 4\text{px}$$
+
+Documents are shared database-free via zlib DEFLATE compressed Base64 hashes.
+
+---
+
+## Getting Started & Installation
+
+### Option 1: Docker (Pre-built Image)
+Deploy the pre-compiled image hosted on the GitHub Container Registry (GHCR):
+
+```bash
+docker pull ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+docker run -d \
+  --name markdown-viewer \
+  -p 8080:80 \
+  --restart unless-stopped \
+  ghcr.io/thisis-developer/markdown-viewer:latest
+```
+
+Open **http://localhost:8080** in your browser.
+
+### Option 2: Docker Compose (Local Build)
+Clone the repository and spin up the container using Compose:
+
+```bash
+git clone https://github.com/ThisIs-Developer/Markdown-Viewer.git
+cd Markdown-Viewer
+docker compose up -d
+```
+The application will start on **http://localhost:8080**.
+
+### Option 3: Local Static Web Server
+Because the code runs completely client-side, you can host the root directory using any static web server:
+
+```bash
+# Clone the repository
+git clone https://github.com/ThisIs-Developer/Markdown-Viewer.git
+cd Markdown-Viewer
+
+# Open VSCode IDE
+open index.html
+and run on localhost http://127.0.0.1:5500 in your browser.
+
+# OR Serve with Python (built-in, no dependencies)
+python3 -m http.server 8080
+
+# Serve with Node.js serve
+npx serve . -p 8080
+```
+Open **http://localhost:8080**.
+
+### Option 4: Desktop Application
+Pre-built desktop binaries are available on the [Releases](https://github.com/ThisIs-Developer/Markdown-Viewer/releases) page for Windows, Linux, and macOS.
+
+To build the desktop application locally from source:
+1. Navigate to the `desktop-app/` directory.
+2. Run `npm install` followed by `node setup-binaries.js` to download Neutralino binaries.
+3. Synchronize files with `node prepare.js`.
+4. Compile using `npm run build` (for Windows embedded) or `npm run build:portable`.
+
+For detailed desktop app settings, see the [Desktop App Wiki](wiki/Desktop-App).
+
+---
+
+## Usage Guide & Keyboard Shortcuts
+
+1.  **Write Markdown** in the left editor pane.
+2.  **Toggle Split/Editor/Preview** modes using the view controls in the top toolbar.
+3.  **Insert elements** (tables, images, checklists, alerts) using the Markdown formatting toolbar.
+4.  **Save or export** your files using the Export dropdown.
+
+### Keyboard Shortcuts Reference
+
+| Action | Windows / Linux | macOS |
+| :--- | :--- | :--- |
+| **Export raw Markdown** | `Ctrl + S` | `⌘ + S` |
+| **Copy Rich HTML** | `Ctrl + C` (with no text selected) | `⌘ + C` (with no text selected) |
+| **Toggle Scroll Sync** | `Ctrl + Shift + S` | `⌘ + Shift + S` |
+| **Open a New Tab** | `Ctrl + T` | `⌘ + T` |
+| **Close the Active Tab** | `Ctrl + W` | `⌘ + W` |
+| **Open Find & Replace** | `Ctrl + F` | `⌘ + F` |
+| **Undo Last Edit** | `Ctrl + Z` | `⌘ + Z` |
+| **Redo Last Edit** | `Ctrl + Shift + Z` / `Ctrl + Y` | `⌘ + Shift + Z` / `⌘ + Y` |
+| **Insert Code Block** | `Ctrl + Shift + C` | `⌘ + Shift + C` |
+| **Toggle Fullscreen Editor** | `F11` | `F11` |
+| **Insert 2-space Indent** | `Tab` | `Tab` |
+| **Outdent Line** | `Shift + Tab` | `Shift + Tab` |
+
+---
+
+## Project Directory Structure
+
+```
+Markdown-Viewer/
+├── index.html              # Core application DOM structure & CDN scripts
+├── script.js               # Main thread controller, state orchestrator, scroll sync
+├── preview-worker.js       # Background web worker for Markdown compilation
+├── styles.css              # Theme stylesheets, layout grids, print layouts
+├── sw.js                   # Progressive Web App (PWA) offline Service Worker
+├── Dockerfile              # Production Nginx Docker configuration
+├── docker-compose.yml      # Port mappings and local Compose orchestrator
+├── README.md               # Main repository readme
+├── LICENSE                 # Apache 2.0 license file
+├── assets/                 # Image assets, gifs, and screenshots
+├── wiki/                   # Markdown documentation pages for GitHub Wiki
+└── desktop-app/            # Native Neutralinojs desktop configuration & binaries
+    ├── package.json        # Node packaging and scripts
+    ├── neutralino.config.json # Neutralino runtime configuration
+    ├── prepare.js          # Synchronizes root web files with desktop workspace
+    └── resources/          # Copied workspace assets compiled into desktop app
+```
+
+---
+
+## Built With (Technology Stack)
+
+| Library Name | Version | Role in App | Loading Method |
+| :--- | :--- | :--- | :--- |
+| **Marked.js** | 9.1.6 | Parses markdown content to HTML elements. | Defer (Upfront) |
+| **Highlight.js** | 11.9.0 | Adds syntax highlighting to code sections. | Defer (Upfront) |
+| **DOMPurify** | 3.0.9 | Sanitizes HTML outputs. | Defer (Upfront) |
+| **FileSaver.js** | 2.0.5 | Manages file saving on the client side. | Defer (Upfront) |
+| **js-yaml** | 4.1.0 | Parses YAML frontmatter headers. | Defer (Upfront) |
+| **Bootstrap** | 5.3.2 | Provides component structures and modal panels. | Upfront Script |
+| **Mermaid.js** | 11.15.0 | Renders diagrams and charts. | Lazy-loaded on diagram find |
+| **MathJax** | 3.2.2 | Renders math formulas. | Lazy-loaded on math find |
+| **jsPDF** | 2.5.1 | Generates PDF documents. | Lazy-loaded on PDF request |
+| **html2canvas** | 1.4.1 | Captures HTML layouts as canvas objects. | Lazy-loaded on PDF request |
+| **pako.js** | 2.1.0 | Compresses shared links. | Lazy-loaded on share request |
+| **JoyPixels** | 9.0.1 | Renders emoji sets. | Lazy-loaded on emoji select |
+
+---
+
+## Contributing & Code Quality
+
+We welcome community contributions! Please check our [Contributing Guidelines Wiki](wiki/Contributing) before creating a pull request.
+
+### Core Workflow Summary:
+1.  **Fork** the repository and create a feature branch (`git checkout -b feature/your-feature`).
+2.  **Verify Code Style:** Maintain a clean 2-space indentation style across HTML, CSS, and JS files. Ensure raw HTML structures are semantic. Avoid direct DOM queries inside processing workers.
+3.  **Conventional Commits:** Write clear commit messages prefixed with `feat:`, `fix:`, `docs:`, `style:`, `refactor:`, `perf:`, or `chore:`.
+4.  **Testing:** Test your revisions across Chrome, Firefox, Edge, and Safari viewports.
+
+---
+
+## Showcase & Community Projects
+
+*   **[Markdown Desk](https://github.com/jhrepo/markdown-desk):** A native macOS wrapper built using Tauri that adds native file-system handlers, menu bar integration, and auto-reload capabilities.
+
+---
+
+## Contributors
+
+Thanks to everyone who has contributed to Markdown Viewer.
+
+<a href="https://github.com/ThisIs-Developer/Markdown-Viewer/graphs/contributors" target="_blank" rel="noopener noreferrer">
+  <img src="https://contrib.rocks/image?repo=ThisIs-Developer/Markdown-Viewer" alt="Contributors" />
+</a>
+
+---
+
+## 📈 Development Journey
+
+Markdown Viewer has grown from a lightweight Markdown parser into a full-featured, professional application with advanced rendering, workflow, and export capabilities. Compare the <a href="https://markdownviewer.pages.dev/" target="_blank" rel="noopener noreferrer">current version</a> with the <a href="https://a1b91221.markdownviewer.pages.dev/" target="_blank" rel="noopener noreferrer">original version</a> to see the progress in UI design, performance optimization, and feature depth.
+
+---
+
+## License
+
+This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for the complete terms and conditions.
+
+---
+
+## Contact & Support
+
+Developed and maintained by **[ThisIs-Developer](https://github.com/ThisIs-Developer)**.
+*   **Bug Reports & Requests:** [Submit an Issue](https://github.com/ThisIs-Developer/Markdown-Viewer/issues)
+*   **Documentation:** [Browse the Wiki](wiki/Home)

BIN
assets/Black and Beige Simple Coming Soon Banner.png


BIN
assets/code.png


BIN
assets/github.png


BIN
assets/icon.jpg


BIN
assets/live-peview.gif


BIN
assets/mathexp.png


BIN
assets/mermaid.png


BIN
assets/table.png


+ 21 - 0
desktop-app/.dockerignore

@@ -0,0 +1,21 @@
+# Build-generated resources
+resources/js/script.js
+resources/styles.css
+resources/assets/
+resources/index.html
+
+# Git
+.git
+.gitignore
+.gitattributes
+
+# Neutralinojs builds and binaries
+bin/
+dist/
+node_modules/
+
+# Logs and temp files
+*.log
+.storage
+.tmp
+.lite_workspace.lua

+ 28 - 0
desktop-app/.gitignore

@@ -0,0 +1,28 @@
+# Dependencies
+node_modules/
+
+# Developer tools' files
+.lite_workspace.lua
+
+# Neutralinojs binaries and builds
+/bin
+/dist
+
+# Neutralinojs client (minified)
+neutralino.js
+
+# Build-generated resources (copied from root by prepare.js)
+/resources/js/script.js
+/resources/styles.css
+/resources/assets/
+/resources/index.html
+
+# Neutralinojs related files
+.storage
+*.log
+
+# neutralinojs tmp files
+.tmp
+
+# Docker build output
+/output

+ 25 - 0
desktop-app/Dockerfile

@@ -0,0 +1,25 @@
+# Build Neutralinojs desktop app binaries
+#
+# Context: repo root (see docker-compose.yml)
+# Output: /output/ contains dist/ artifacts
+
+FROM node:lts-alpine AS build
+
+WORKDIR /app
+
+# Copy the entire repo (context is the repo root)
+COPY . .
+
+WORKDIR /app/desktop-app
+
+# Setup (download binaries + prepare resources) and build all variants
+RUN npm run build:all
+
+# Final stage: Export the dist artifacts
+FROM alpine:latest
+
+WORKDIR /output
+
+COPY --from=build /app/desktop-app/dist/ .
+
+CMD ["echo", "Build artifacts are in /output. Use 'docker cp' to extract them."]

+ 21 - 0
desktop-app/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Neutralinojs and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 112 - 0
desktop-app/README.md

@@ -0,0 +1,112 @@
+# Markdown Viewer Desktop App Port
+
+This is a desktop app port of [Markdown Viewer](https://github.com/ThisIs-Developer/Markdown-Viewer), see [README](../README.md). It is built using [Neutralinojs](https://github.com/neutralinojs/neutralinojs).
+
+## Architecture
+
+The desktop app **shares** its core files (`script.js`, `styles.css`, `assets/`) with the browser version in the repo root. A build script (`prepare.js`) copies these files into `resources/` and injects Neutralinojs-specific additions into `index.html` at build time.
+
+Neutralinojs platform binaries are managed by `setup-binaries.js`, which downloads them on first use and caches them in `bin/` (gitignored). The download is version-locked to `cli.binaryVersion` in `neutralino.config.json` and only re-triggered when that version changes.
+
+Desktop-only files (not generated):
+
+- `resources/js/main.js` — Neutralinojs lifecycle, tray menu, window events
+- `resources/js/neutralino.js` — Neutralinojs client library
+- `neutralino.config.json` — App configuration
+- `setup-binaries.js` — Idempotent binary setup (downloads on first use)
+
+## Development
+
+### Requirements
+
+- [Node.js](https://nodejs.org/)
+
+### Setup
+
+No installation is required. The app is built and run using `npx` (via npm scripts).
+
+Neutralinojs platform binaries are downloaded automatically on first build or dev run. To manually trigger the download:
+
+```bash
+npm run setup
+```
+
+Binaries are cached in `bin/` (gitignored) and only re-downloaded when `cli.binaryVersion` in `neutralino.config.json` changes.
+
+### Running the app
+
+```bash
+npm run dev
+```
+
+This automatically runs `setup` (downloads binaries if needed and prepares resources) before starting the app. Hot-reload is enabled by default. Enable the browser inspector by setting `"enableInspector": true` in `neutralino.config.json`.
+
+For more information, see the [Neutralinojs documentation](https://neutralino.js.org/docs/cli/neu-cli#installation).
+
+### Building the app
+
+**Default / Windows** - Single-file Windows executable with embedded resources:
+
+```bash
+npm run build
+```
+
+**Portable** - ZIP bundle with separate `resources.neu` file:
+
+```bash
+npm run build:portable
+```
+
+**Both** - Build the portable bundle and a Windows embedded EXE in one step:
+
+```bash
+npm run build:all
+```
+
+Build output is placed in `dist/`.
+
+Note: `npm run build` now uses the Windows-only embedded helper and writes
+`dist/markdown-viewer/markdown-viewer-win_x64.exe`. The helper temporarily hides
+non-Windows Neutralino binaries so the CLI does not run out of memory while
+embedding every platform before it reaches the Windows target. Use
+`npm run build:portable` for the all-platform portable ZIP with `resources.neu`;
+`npm run build:all` writes that ZIP plus a Windows embedded EXE at
+`dist/windows-embedded/markdown-viewer/markdown-viewer-win_x64.exe`.
+
+For more information, see the [Neutralinojs documentation](https://neutralino.js.org/docs/cli/neu-cli#neu-build).
+
+### Building with Docker
+
+Build binaries without installing Node.js locally:
+
+```bash
+docker compose up --build
+```
+
+Build artifacts will be output to `desktop-app/output/`.
+
+## Releases
+
+Prebuilt binaries are automatically built and published as GitHub Releases when a tag matching `desktop-v*` is pushed (e.g., `desktop-v1.0.0`). See [`.github/workflows/desktop-build.yml`](../.github/workflows/desktop-build.yml).
+
+Each release includes:
+
+| Asset | Description |
+| ----- | ----------- |
+| `markdown-viewer-win_x64.exe` | Windows x64 executable |
+| `markdown-viewer-release.zip` | Portable bundle with `resources.neu` (all platforms) |
+| `source.tar.gz` | Desktop app source archive |
+| `SHA256SUMS.txt` | Checksums for all release assets |
+
+## License
+
+**MIT**.
+
+The desktop version uses [Neutralinojs](https://github.com/neutralinojs/neutralinojs), which is also licensed under the MIT License.
+
+- [Neutralinojs](https://github.com/neutralinojs/neutralinojs): [LICENSE (MIT)](LICENSE)
+- [Markdown Viewer & Desktop Port](https://github.com/ThisIs-Developer/Markdown-Viewer): [LICENSE (MIT)](../LICENSE)
+
+## Contributors
+
+[![Contributors](https://contrib.rocks/image?repo=ThisIs-Developer/Markdown-Viewer)](https://github.com/ThisIs-Developer/Markdown-Viewer/graphs/contributors)

+ 124 - 0
desktop-app/build-windows.js

@@ -0,0 +1,124 @@
+#!/usr/bin/env node
+
+/**
+ * Build a Windows-only embedded Neutralino executable.
+ *
+ * `neu build --embed-resources` embeds every platform binary it finds in bin/.
+ * With this app's offline libraries, embedding all platforms can exhaust Node's
+ * heap before the Windows binary is reached, leaving a stale Windows EXE in
+ * dist/. Temporarily hiding non-Windows binaries makes the CLI embed only the
+ * target users actually run on Windows.
+ */
+
+const fs = require("fs");
+const path = require("path");
+const { spawnSync } = require("child_process");
+
+const APP_DIR = __dirname;
+const BIN_DIR = path.join(APP_DIR, "bin");
+const CONFIG_FILE = path.join(APP_DIR, "neutralino.config.json");
+const WIN_BINARY = "neutralino-win_x64.exe";
+const NEU_CLI = "@neutralinojs/neu@11.7.0";
+
+function getArgValue(name) {
+  const prefix = `${name}=`;
+  for (let i = 2; i < process.argv.length; i += 1) {
+    const arg = process.argv[i];
+    if (arg === name) return process.argv[i + 1] || "";
+    if (arg.startsWith(prefix)) return arg.slice(prefix.length);
+  }
+  return "";
+}
+
+function createConfigOverride(distributionPath) {
+  if (!distributionPath) return CONFIG_FILE;
+
+  const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
+  config.cli = config.cli || {};
+  config.cli.distributionPath = distributionPath.replace(/\\/g, "/");
+
+  const tmpDir = path.join(APP_DIR, ".tmp");
+  fs.mkdirSync(tmpDir, { recursive: true });
+
+  const tempConfigFile = path.join(tmpDir, `neutralino.windows.${process.pid}.config.json`);
+  fs.writeFileSync(tempConfigFile, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
+  return tempConfigFile;
+}
+
+function hideNonWindowsBinaries(tempDir) {
+  const hidden = [];
+  fs.mkdirSync(tempDir, { recursive: true });
+
+  for (const entry of fs.readdirSync(BIN_DIR, { withFileTypes: true })) {
+    if (!entry.isFile()) continue;
+    if (!entry.name.startsWith("neutralino-")) continue;
+    if (entry.name === WIN_BINARY) continue;
+
+    const from = path.join(BIN_DIR, entry.name);
+    const to = path.join(tempDir, entry.name);
+    fs.renameSync(from, to);
+    hidden.push({ from, to });
+  }
+
+  return hidden;
+}
+
+function restoreHiddenBinaries(hidden) {
+  for (let i = hidden.length - 1; i >= 0; i -= 1) {
+    const item = hidden[i];
+    if (fs.existsSync(item.to)) {
+      fs.renameSync(item.to, item.from);
+    }
+  }
+}
+
+function main() {
+  const windowsBinaryPath = path.join(BIN_DIR, WIN_BINARY);
+  if (!fs.existsSync(windowsBinaryPath)) {
+    console.error(`Missing ${WIN_BINARY}. Run npm run setup before building.`);
+    process.exit(1);
+  }
+
+  const distributionPath = getArgValue("--dist");
+  const tempDir = path.join(BIN_DIR, `.nonwin-disabled-${process.pid}`);
+  const configFile = createConfigOverride(distributionPath);
+  const hidden = hideNonWindowsBinaries(tempDir);
+  let exitCode = 0;
+
+  try {
+    const npx = "npx";
+    const args = [
+      "-y",
+      NEU_CLI,
+      "build",
+      "--embed-resources",
+      "--clean",
+      "--config-file",
+      configFile,
+    ];
+
+    const result = spawnSync(npx, args, {
+      cwd: APP_DIR,
+      stdio: "inherit",
+      env: process.env,
+      shell: process.platform === "win32",
+    });
+
+    if (result.error) {
+      console.error(result.error.message);
+      exitCode = 1;
+    } else {
+      exitCode = result.status || 0;
+    }
+  } finally {
+    restoreHiddenBinaries(hidden);
+    fs.rmSync(tempDir, { recursive: true, force: true });
+    if (configFile !== CONFIG_FILE) {
+      fs.rmSync(configFile, { force: true });
+    }
+  }
+
+  process.exit(exitCode);
+}
+
+main();

+ 14 - 0
desktop-app/docker-compose.yml

@@ -0,0 +1,14 @@
+services:
+  desktop-build:
+    build:
+      context: ..
+      dockerfile: desktop-app/Dockerfile
+    container_name: markdown-viewer-desktop-build
+    volumes:
+      - ./output:/export
+    entrypoint:
+      [
+        "sh",
+        "-c",
+        "cp -r /output/* /export/ && echo '✓ Build artifacts copied to desktop-app/output/'",
+      ]

+ 68 - 0
desktop-app/neutralino.config.json

@@ -0,0 +1,68 @@
+{
+  "$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json",
+  "applicationId": "com.markdownviewer.desktop",
+  "version": "3.7.4",
+  "defaultMode": "window",
+  "port": 0,
+  "documentRoot": "/resources/",
+  "url": "/",
+  "enableServer": true,
+  "enableNativeAPI": true,
+  "tokenSecurity": "one-time",
+  "logging": {
+    "enabled": false,
+    "writeToLogFile": false
+  },
+  "nativeAllowList": [
+    "app.exit",
+    "os.showOpenDialog",
+    "os.showSaveDialog",
+    "os.showMessageBox",
+    "os.open",
+    "os.setTray",
+    "filesystem.readFile",
+    "filesystem.writeFile"
+  ],
+  "globalVariables": {},
+  "modes": {
+    "window": {
+      "title": "Markdown Viewer",
+      "width": 1280,
+      "height": 720,
+      "minWidth": 400,
+      "minHeight": 200,
+      "center": true,
+      "fullScreen": false,
+      "alwaysOnTop": false,
+      "icon": "/resources/assets/icon.jpg",
+      "enableInspector": false,
+      "borderless": false,
+      "maximize": false,
+      "hidden": false,
+      "resizable": true,
+      "exitProcessOnClose": true
+    },
+    "browser": {
+      "globalVariables": {},
+      "nativeBlockList": ["filesystem.*"]
+    },
+    "cloud": {
+      "url": "/resources/#cloud",
+      "nativeAllowList": ["app.*"]
+    },
+    "chrome": {
+      "width": 1280,
+      "height": 720,
+      "args": "--user-agent=\"Neutralinojs chrome mode\"",
+      "nativeBlockList": ["filesystem.*", "os.*"]
+    }
+  },
+  "cli": {
+    "binaryName": "markdown-viewer",
+    "resourcesPath": "/resources/",
+    "extensionsPath": "/extensions/",
+    "clientLibrary": "/resources/js/neutralino.js",
+    "binaryVersion": "6.5.0",
+    "clientVersion": "6.5.0"
+  }
+}

+ 12 - 0
desktop-app/package-lock.json

@@ -0,0 +1,12 @@
+{
+  "name": "markdown-viewer-desktop",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "markdown-viewer-desktop",
+      "version": "1.0.0"
+    }
+  }
+}

+ 21 - 0
desktop-app/package.json

@@ -0,0 +1,21 @@
+{
+  "name": "markdown-plusplus-desktop",
+  "version": "3.7.4",
+  "private": true,
+  "description": "A premium client-side GitHub-style Markdown editor and live preview tool for desktop, featuring math rendering, diagrams, syntax highlighting, and PDF/HTML exports.",
+  "scripts": {
+    "setup": "node setup-binaries.js",
+    "postsetup": "node prepare.js",
+    "predev": "npm run setup",
+    "dev": "npx -y @neutralinojs/neu@11.7.0 run",
+    "prebuild": "npm run setup",
+    "build": "node build-windows.js",
+    "prebuild:windows": "npm run setup",
+    "build:windows": "node build-windows.js",
+    "prebuild:portable": "npm run setup",
+    "build:portable": "npx -y @neutralinojs/neu@11.7.0 build --release --clean",
+    "prebuild:all": "npm run setup",
+    "build:all": "npx -y @neutralinojs/neu@11.7.0 build --release --clean && node build-windows.js --dist dist/windows-embedded"
+  },
+  "dependencies": {}
+}

+ 238 - 0
desktop-app/prepare.js

@@ -0,0 +1,238 @@
+#!/usr/bin/env node
+
+/**
+ * prepare.js — Build script for the Neutralinojs desktop app.
+ *
+ * Copies shared browser-version files (script.js, styles.css, assets/)
+ * from the repo root into desktop-app/resources/, downloads all remote CDN
+ * libraries locally for 100% offline capabilities, validates their cryptographic
+ * integrity using SRI hashes (SHA-384), and generates a Neutralinojs-compatible index.html.
+ */
+
+const fs = require("fs");
+const path = require("path");
+const https = require("https");
+const crypto = require("crypto");
+
+const ROOT_DIR = path.resolve(__dirname, "..");
+const RESOURCES_DIR = path.resolve(__dirname, "resources");
+const jsDest = path.join(RESOURCES_DIR, "js");
+const LIBS_DIR = path.join(RESOURCES_DIR, "libs");
+
+// Create directories
+fs.mkdirSync(jsDest, { recursive: true });
+fs.mkdirSync(LIBS_DIR, { recursive: true });
+
+function copyDirSync(src, dest, excludePatterns) {
+  if (!excludePatterns) excludePatterns = [];
+  fs.mkdirSync(dest, { recursive: true });
+  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
+    const srcPath = path.join(src, entry.name);
+    const destPath = path.join(dest, entry.name);
+    // PERF-027: Skip files matching exclusion patterns (e.g., large demo GIFs)
+    if (excludePatterns.some(p => entry.name.match(p))) {
+      console.log(`  ⊘ Skipped ${entry.name} (excluded from desktop build)`);
+      continue;
+    }
+    if (entry.isDirectory()) {
+      copyDirSync(srcPath, destPath, excludePatterns);
+    } else {
+      fs.copyFileSync(srcPath, destPath);
+    }
+  }
+}
+
+// Copy shared assets
+fs.copyFileSync(path.join(ROOT_DIR, "script.js"), path.join(jsDest, "script.js"));
+console.log("✓ Copied script.js → resources/js/script.js");
+
+fs.copyFileSync(path.join(ROOT_DIR, "preview-worker.js"), path.join(jsDest, "preview-worker.js"));
+console.log("Copied preview-worker.js to resources/js/preview-worker.js");
+
+fs.copyFileSync(path.join(ROOT_DIR, "styles.css"), path.join(RESOURCES_DIR, "styles.css"));
+console.log("✓ Copied styles.css → resources/styles.css");
+
+// PERF-027: Exclude large demo assets (GIFs) from desktop build to reduce binary size
+copyDirSync(path.join(ROOT_DIR, "assets"), path.join(RESOURCES_DIR, "assets"), [/\.gif$/i]);
+console.log("✓ Copied assets/ → resources/assets/ (excluding GIF demos)");
+
+/**
+ * Validates the cryptographic integrity of a file against an expected SHA-384 hash.
+ */
+function verifyIntegrity(filePath, expectedSha384) {
+  return new Promise((resolve, reject) => {
+    if (!expectedSha384) {
+      resolve(true); // Skip validation if no hash is provided (e.g., relative fonts)
+      return;
+    }
+
+    const hash = crypto.createHash("sha384");
+    const stream = fs.createReadStream(filePath);
+
+    stream.on("data", data => hash.update(data));
+    stream.on("end", () => {
+      const calculated = "sha384-" + hash.digest("base64");
+      if (calculated === expectedSha384) {
+        resolve(true);
+      } else {
+        reject(new Error(`Integrity mismatch for ${path.basename(filePath)}:\nExpected: ${expectedSha384}\nCalculated: ${calculated}`));
+      }
+    });
+    stream.on("error", reject);
+  });
+}
+
+/**
+ * Downloads a file from a URL and verifies its integrity.
+ */
+function downloadFile(url, destPath, expectedSha384) {
+  return new Promise((resolve, reject) => {
+    // If file already exists, verify its integrity before skipping
+    if (fs.existsSync(destPath) && fs.statSync(destPath).size > 0) {
+      verifyIntegrity(destPath, expectedSha384)
+        .then(() => resolve())
+        .catch(() => {
+          console.log(`↻ Cached file ${path.basename(destPath)} failed integrity check. Re-downloading...`);
+          fs.unlinkSync(destPath);
+          downloadAndVerify();
+        });
+      return;
+    }
+
+    downloadAndVerify();
+
+    function downloadAndVerify() {
+      console.log(`Downloading offline dependency: ${path.basename(destPath)}...`);
+      const req = https.get(url, (res) => {
+        if (res.statusCode !== 200) {
+          res.resume(); // Drain response to free up the socket
+          reject(new Error(`Failed to load ${url} (${res.statusCode})`));
+          return;
+        }
+        const stream = fs.createWriteStream(destPath);
+        
+        // Handle stream and response errors
+        stream.on("error", reject);
+        res.on("error", reject);
+
+        res.pipe(stream);
+        stream.on("finish", () => {
+          stream.close();
+
+          // Verify integrity of downloaded file
+          verifyIntegrity(destPath, expectedSha384)
+            .then(() => resolve())
+            .catch(err => {
+              // Delete corrupted file
+              if (fs.existsSync(destPath)) {
+                fs.unlinkSync(destPath);
+              }
+              reject(err);
+            });
+        });
+      });
+      req.on("error", reject);
+    }
+  });
+}
+
+async function prepareOfflineDependencies() {
+  console.log("\nStarting Secure Offline Assets Preparation...");
+  let html = fs.readFileSync(path.join(ROOT_DIR, "index.html"), "utf-8");
+  
+  // Find all CDN script and link tags that match standard script/stylesheet declarations
+  const tagRegex = /<(link|script)[^>]+(?:href|src)="https:\/\/(?:cdnjs\.cloudflare\.com|cdn\.jsdelivr\.net)\/[^"]+"[^>]*>/g;
+  let match;
+  const downloads = [];
+  const replacements = [];
+
+  while ((match = tagRegex.exec(html)) !== null) {
+    const fullTag = match[0];
+    
+    // Extract url
+    const urlMatch = /(?:href|src)="([^"]+)"/.exec(fullTag);
+    if (!urlMatch) continue;
+    const url = urlMatch[1];
+
+    // Extract integrity hash
+    const integrityMatch = /integrity="([^"]+)"/.exec(fullTag);
+    const expectedSha384 = integrityMatch ? integrityMatch[1] : null;
+
+    if (!expectedSha384) {
+      console.warn(`⚠ Warning: CDN dependency is missing an integrity hash: ${url}`);
+      throw new Error(`CDN dependency is missing an integrity hash: ${url}`);
+    }
+
+    // Determine local filename - sanitize package version tags or query strings
+    const urlPath = new URL(url).pathname;
+    let filename = path.basename(urlPath);
+    if (url.includes("bootstrap-icons")) {
+      filename = "bootstrap-icons.min.css";
+    }
+    
+    const localDest = path.join(LIBS_DIR, filename);
+    downloads.push(downloadFile(url, localDest, expectedSha384));
+    
+    // Queue replacement in HTML to point to local libs folder
+    const attr = fullTag.includes("href=") ? "href" : "src";
+    replacements.push({
+      original: `${attr}="${url}"`,
+      replaced: `${attr}="/libs/${filename}"`
+    });
+  }
+
+  // Also download the relative fonts loaded by bootstrap-icons (these are loaded by the stylesheet and do not have SRI tags)
+  const fontDir = path.join(LIBS_DIR, "fonts");
+  fs.mkdirSync(fontDir, { recursive: true });
+  downloads.push(downloadFile("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff2", path.join(fontDir, "bootstrap-icons.woff2"), null));
+  downloads.push(downloadFile("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff", path.join(fontDir, "bootstrap-icons.woff"), null));
+
+  // Wait for all downloads and cryptographic validations to finish
+  try {
+    await Promise.all(downloads);
+    console.log("✓ All offline libraries successfully downloaded and cryptographically validated.");
+  } catch (err) {
+    console.error("✗ Critical Security Error: Dependency integrity check failed!", err.message);
+    process.exit(1); // Abort execution if a download fails validation
+  }
+
+  // Apply replacements in HTML
+  replacements.forEach(rep => {
+    html = html.replace(rep.original, rep.replaced);
+  });
+
+  // Fix relative assets
+  html = html.replace(/href="assets\//g, 'href="/assets/');
+  html = html.replace(/href="styles\.css"/g, 'href="/styles.css"');
+  
+  // PERF-034: Strip web-specific SEO tags, canonical, hreflang, preconnect, manifest and JSON-LD structured data for desktop build
+  html = html.replace(/<!-- DNS Prefetch & Preconnect CDN Origins to Warm Up Latency -->[\s\S]*?<!-- PERF-015:/i, '<!-- PERF-015:');
+  html = html.replace(/<!-- Canonical Link -->[\s\S]*?<!-- PWA Web Manifest -->/i, '<!-- PWA Web Manifest -->');
+  html = html.replace(/<link rel="manifest" href="manifest\.json">/i, '');
+  html = html.replace(/<!-- Primary Meta Tags -->[\s\S]*?<!-- JSON-LD Structured Data Schema/i, '<!-- JSON-LD Structured Data Schema');
+  html = html.replace(/<script type="application\/ld\+json">[\s\S]*?<\/script>/i, '');
+
+  // Inject Neutralino script tags
+  html = html.replace(
+    /<script\s+src="script\.js"\s*><\/script>/i,
+    '<script src="/js/neutralino.js"></script>\n    <script src="/js/main.js"></script>\n    <script src="/js/script.js"></script>',
+  );
+
+  // Inject app-info element
+  html = html.replace(
+    '<div class="app-container">',
+    `<div class="app-container">
+        <div id="neutralino-app">
+          <div id="neutralino-info"></div>
+        </div>`,
+  );
+
+  fs.writeFileSync(path.join(RESOURCES_DIR, "index.html"), html, "utf-8");
+  console.log("✓ Generated resources/index.html (Offline replacements & injections applied)");
+  console.log("\nDone! Run `npm run dev` to start the desktop app.");
+}
+
+prepareOfflineDependencies().catch(err => {
+  console.error("✗ Fatal Prepare Error:", err);
+  process.exit(1);
+});

+ 121 - 0
desktop-app/resources/js/main.js

@@ -0,0 +1,121 @@
+// Markdown Viewer Desktop — Neutralino.js integration layer
+// Handles system tray, window close confirmation, and file association
+
+/*
+    Function to set up a system tray menu with options specific to the window mode.
+    This function checks if the application is running in window mode, and if so,
+    it defines the tray menu items and sets up the tray accordingly.
+*/
+function setTray() {
+  // Tray menu is only available in window mode
+  if (typeof NL_MODE === "undefined" || NL_MODE != "window") {
+    console.log("INFO: Tray menu is only available in the window mode.");
+    return;
+  }
+
+  // Define tray menu items
+  let tray = {
+    icon: "/resources/assets/icon.jpg",
+    menuItems: [
+      { id: "VERSION", text: "Get version" },
+      { id: "SEP", text: "-" },
+      { id: "QUIT", text: "Quit" },
+    ],
+  };
+
+  // Set the tray menu
+  try {
+    Neutralino.os.setTray(tray);
+  } catch (e) {
+    console.warn("Failed to set system tray:", e);
+  }
+}
+
+/*
+    Function to handle click events on the tray menu items.
+    This function performs different actions based on the clicked item's ID,
+    such as displaying version information or exiting the application.
+*/
+function onTrayMenuItemClicked(event) {
+  switch (event.detail.id) {
+    case "VERSION":
+      // Display version information
+      Neutralino.os.showMessageBox(
+        "Version information",
+        `Neutralinojs server: v${NL_VERSION} | Neutralinojs client: v${NL_CVERSION}`,
+      );
+      break;
+    case "QUIT":
+      // Exit the application
+      Neutralino.app.exit();
+      break;
+  }
+}
+
+async function onWindowClose() {
+  try {
+    let response = await Neutralino.os.showMessageBox(
+      "Exit Markdown Viewer",
+      "Are you sure you want to close the application? Any unsaved changes in your tabs may be lost.",
+      "YES_NO",
+      "QUESTION"
+    );
+    if (response === "YES") {
+      Neutralino.app.exit();
+    }
+  } catch (e) {
+    Neutralino.app.exit();
+  }
+}
+
+function isNeutralinoRuntime() {
+  if (typeof Neutralino === 'undefined' || typeof NL_PORT === 'undefined') {
+    return false;
+  }
+
+  try {
+    return typeof NL_TOKEN !== 'undefined' || Boolean(sessionStorage.getItem('NL_TOKEN'));
+  } catch (e) {
+    return typeof NL_TOKEN !== 'undefined';
+  }
+}
+
+// Initialize Neutralino if in native environment
+if (isNeutralinoRuntime()) {
+  Neutralino.init();
+
+  // Register event listeners
+  Neutralino.events.on("trayMenuItemClicked", onTrayMenuItemClicked);
+  Neutralino.events.on("windowClose", onWindowClose);
+
+  // Conditional initialization: Set up system tray if not running on macOS
+  if (typeof NL_OS !== 'undefined' && NL_OS != "Darwin") {
+    // TODO: Fix https://github.com/neutralinojs/neutralinojs/issues/615
+    setTray();
+  }
+}
+
+// Open file passed as command-line argument (e.g. when double-clicking a .md file)
+(async function loadInitialFile() {
+  if (!isNeutralinoRuntime() || typeof NL_ARGS === 'undefined') return;
+  const args = Array.isArray(NL_ARGS) ? NL_ARGS : (() => { try { return JSON.parse(NL_ARGS); } catch(e) { return []; } })();
+  const filePath = args.find(a => typeof a === 'string' && /\.(md|markdown)$/i.test(a));
+  if (!filePath) return;
+
+  try {
+    const content = await Neutralino.filesystem.readFile(filePath);
+    const fileName = filePath.split(/[/\\]/).pop().replace(/\.(md|markdown)$/i, '');
+    
+    window.NL_INITIAL_FILE_CONTENT = {
+      name: fileName,
+      content: content
+    };
+
+    // Callback hook in case script.js loaded first
+    if (window.NL_IMPORT_EXTERNAL_FILE) {
+      window.NL_IMPORT_EXTERNAL_FILE(content, fileName);
+    }
+  } catch (e) {
+    console.warn('Could not open initial file:', e);
+  }
+})();

+ 531 - 0
desktop-app/resources/js/neutralino.d.ts

@@ -0,0 +1,531 @@
+export declare enum LoggerType {
+	WARNING = "WARNING",
+	ERROR = "ERROR",
+	INFO = "INFO"
+}
+export declare enum Icon {
+	WARNING = "WARNING",
+	ERROR = "ERROR",
+	INFO = "INFO",
+	QUESTION = "QUESTION"
+}
+export declare enum MessageBoxChoice {
+	OK = "OK",
+	OK_CANCEL = "OK_CANCEL",
+	YES_NO = "YES_NO",
+	YES_NO_CANCEL = "YES_NO_CANCEL",
+	RETRY_CANCEL = "RETRY_CANCEL",
+	ABORT_RETRY_IGNORE = "ABORT_RETRY_IGNORE"
+}
+export declare enum ClipboardFormat {
+	unknown = "unknown",
+	text = "text",
+	image = "image"
+}
+export declare enum Mode {
+	window = "window",
+	browser = "browser",
+	cloud = "cloud",
+	chrome = "chrome"
+}
+export declare enum OperatingSystem {
+	Linux = "Linux",
+	Windows = "Windows",
+	Darwin = "Darwin",
+	FreeBSD = "FreeBSD",
+	Unknown = "Unknown"
+}
+export declare enum Architecture {
+	x64 = "x64",
+	arm = "arm",
+	itanium = "itanium",
+	ia32 = "ia32",
+	unknown = "unknown"
+}
+export interface DirectoryEntry {
+	entry: string;
+	path: string;
+	type: string;
+}
+export interface FileReaderOptions {
+	pos: number;
+	size: number;
+}
+export interface DirectoryReaderOptions {
+	recursive: boolean;
+}
+export interface OpenedFile {
+	id: number;
+	eof: boolean;
+	pos: number;
+	lastRead: number;
+}
+export interface Stats {
+	size: number;
+	isFile: boolean;
+	isDirectory: boolean;
+	createdAt: number;
+	modifiedAt: number;
+}
+export interface Watcher {
+	id: number;
+	path: string;
+}
+export interface CopyOptions {
+	recursive: boolean;
+	overwrite: boolean;
+	skip: boolean;
+}
+export interface PathParts {
+	rootName: string;
+	rootDirectory: string;
+	rootPath: string;
+	relativePath: string;
+	parentPath: string;
+	filename: string;
+	stem: string;
+	extension: string;
+}
+interface Permissions$1 {
+	all: boolean;
+	ownerAll: boolean;
+	ownerRead: boolean;
+	ownerWrite: boolean;
+	ownerExec: boolean;
+	groupAll: boolean;
+	groupRead: boolean;
+	groupWrite: boolean;
+	groupExec: boolean;
+	othersAll: boolean;
+	othersRead: boolean;
+	othersWrite: boolean;
+	othersExec: boolean;
+}
+export type PermissionsMode = "ADD" | "REPLACE" | "REMOVE";
+declare function createDirectory(path: string): Promise<void>;
+declare function remove(path: string): Promise<void>;
+declare function writeFile(path: string, data: string): Promise<void>;
+declare function appendFile(path: string, data: string): Promise<void>;
+declare function writeBinaryFile(path: string, data: ArrayBuffer): Promise<void>;
+declare function appendBinaryFile(path: string, data: ArrayBuffer): Promise<void>;
+declare function readFile(path: string, options?: FileReaderOptions): Promise<string>;
+declare function readBinaryFile(path: string, options?: FileReaderOptions): Promise<ArrayBuffer>;
+declare function openFile(path: string): Promise<number>;
+declare function createWatcher(path: string): Promise<number>;
+declare function removeWatcher(id: number): Promise<number>;
+declare function getWatchers(): Promise<Watcher[]>;
+declare function updateOpenedFile(id: number, event: string, data?: any): Promise<void>;
+declare function getOpenedFileInfo(id: number): Promise<OpenedFile>;
+declare function readDirectory(path: string, options?: DirectoryReaderOptions): Promise<DirectoryEntry[]>;
+declare function copy(source: string, destination: string, options?: CopyOptions): Promise<void>;
+declare function move(source: string, destination: string): Promise<void>;
+declare function getStats(path: string): Promise<Stats>;
+declare function getAbsolutePath(path: string): Promise<string>;
+declare function getRelativePath(path: string, base?: string): Promise<string>;
+declare function getPathParts(path: string): Promise<PathParts>;
+declare function getPermissions(path: string): Promise<Permissions$1>;
+declare function setPermissions(path: string, permissions: Permissions$1, mode: PermissionsMode): Promise<void>;
+declare function getJoinedPath(...paths: string[]): Promise<string>;
+declare function getNormalizedPath(path: string): Promise<string>;
+declare function getUnnormalizedPath(path: string): Promise<string>;
+export interface ExecCommandOptions {
+	stdIn?: string;
+	background?: boolean;
+	cwd?: string;
+}
+export interface ExecCommandResult {
+	pid: number;
+	stdOut: string;
+	stdErr: string;
+	exitCode: number;
+}
+export interface SpawnedProcess {
+	id: number;
+	pid: number;
+}
+export interface SpawnedProcessOptions {
+	cwd?: string;
+	envs?: Record<string, string>;
+}
+export interface Envs {
+	[key: string]: string;
+}
+export interface OpenDialogOptions {
+	multiSelections?: boolean;
+	filters?: Filter[];
+	defaultPath?: string;
+}
+export interface FolderDialogOptions {
+	defaultPath?: string;
+}
+export interface SaveDialogOptions {
+	forceOverwrite?: boolean;
+	filters?: Filter[];
+	defaultPath?: string;
+}
+export interface Filter {
+	name: string;
+	extensions: string[];
+}
+export interface TrayOptions {
+	icon: string;
+	menuItems: TrayMenuItem[];
+}
+export interface TrayMenuItem {
+	id?: string;
+	text: string;
+	isDisabled?: boolean;
+	isChecked?: boolean;
+}
+export type KnownPath = "config" | "data" | "cache" | "documents" | "pictures" | "music" | "video" | "downloads" | "savedGames1" | "savedGames2" | "temp";
+declare function execCommand(command: string, options?: ExecCommandOptions): Promise<ExecCommandResult>;
+declare function spawnProcess(command: string, options?: SpawnedProcessOptions): Promise<SpawnedProcess>;
+declare function updateSpawnedProcess(id: number, event: string, data?: any): Promise<void>;
+declare function getSpawnedProcesses(): Promise<SpawnedProcess[]>;
+declare function getEnv(key: string): Promise<string>;
+declare function getEnvs(): Promise<Envs>;
+declare function showOpenDialog(title?: string, options?: OpenDialogOptions): Promise<string[]>;
+declare function showFolderDialog(title?: string, options?: FolderDialogOptions): Promise<string>;
+declare function showSaveDialog(title?: string, options?: SaveDialogOptions): Promise<string>;
+declare function showNotification(title: string, content: string, icon?: Icon): Promise<void>;
+declare function showMessageBox(title: string, content: string, choice?: MessageBoxChoice, icon?: Icon): Promise<string>;
+declare function setTray(options: TrayOptions): Promise<void>;
+declare function open$1(url: string): Promise<void>;
+declare function getPath(name: KnownPath): Promise<string>;
+export interface MemoryInfo {
+	physical: {
+		total: number;
+		available: number;
+	};
+	virtual: {
+		total: number;
+		available: number;
+	};
+}
+export interface KernelInfo {
+	variant: string;
+	version: string;
+}
+export interface OSInfo {
+	name: string;
+	description: string;
+	version: string;
+}
+export interface CPUInfo {
+	vendor: string;
+	model: string;
+	frequency: number;
+	architecture: string;
+	logicalThreads: number;
+	physicalCores: number;
+	physicalUnits: number;
+}
+export interface Display {
+	id: number;
+	resolution: Resolution;
+	dpi: number;
+	bpp: number;
+	refreshRate: number;
+}
+export interface Resolution {
+	width: number;
+	height: number;
+}
+export interface MousePosition {
+	x: number;
+	y: number;
+}
+declare function getMemoryInfo(): Promise<MemoryInfo>;
+declare function getArch(): Promise<string>;
+declare function getKernelInfo(): Promise<KernelInfo>;
+declare function getOSInfo(): Promise<OSInfo>;
+declare function getCPUInfo(): Promise<CPUInfo>;
+declare function getDisplays(): Promise<Display[]>;
+declare function getMousePosition(): Promise<MousePosition>;
+declare function setData(key: string, data: string | null): Promise<void>;
+declare function getData(key: string): Promise<string>;
+declare function removeData(key: string): Promise<void>;
+declare function getKeys(): Promise<string[]>;
+declare function clear(): Promise<void>;
+declare function log(message: string, type?: LoggerType): Promise<void>;
+export interface OpenActionOptions {
+	url: string;
+}
+export interface RestartOptions {
+	args: string;
+}
+declare function exit(code?: number): Promise<void>;
+declare function killProcess(): Promise<void>;
+declare function restartProcess(options?: RestartOptions): Promise<void>;
+declare function getConfig(): Promise<any>;
+declare function broadcast(event: string, data?: any): Promise<void>;
+declare function readProcessInput(readAll?: boolean): Promise<string>;
+declare function writeProcessOutput(data: string): Promise<void>;
+declare function writeProcessError(data: string): Promise<void>;
+export interface WindowOptions extends WindowSizeOptions, WindowPosOptions {
+	title?: string;
+	icon?: string;
+	fullScreen?: boolean;
+	alwaysOnTop?: boolean;
+	enableInspector?: boolean;
+	borderless?: boolean;
+	maximize?: boolean;
+	hidden?: boolean;
+	maximizable?: boolean;
+	useSavedState?: boolean;
+	exitProcessOnClose?: boolean;
+	extendUserAgentWith?: string;
+	injectGlobals?: boolean;
+	injectClientLibrary?: boolean;
+	injectScript?: string;
+	processArgs?: string;
+}
+export interface WindowSizeOptions {
+	width?: number;
+	height?: number;
+	minWidth?: number;
+	minHeight?: number;
+	maxWidth?: number;
+	maxHeight?: number;
+	resizable?: boolean;
+}
+export interface WindowPosOptions {
+	x?: number;
+	y?: number;
+	center?: boolean;
+}
+export interface WindowMenu extends Array<WindowMenuItem> {
+}
+export interface WindowMenuItem {
+	id?: string;
+	text: string;
+	action?: string;
+	shortcut?: string;
+	isDisabled?: boolean;
+	isChecked?: boolean;
+	menuItems?: WindowMenuItem[];
+}
+declare function setTitle(title: string): Promise<void>;
+declare function getTitle(): Promise<string>;
+declare function maximize(): Promise<void>;
+declare function unmaximize(): Promise<void>;
+declare function isMaximized(): Promise<boolean>;
+declare function minimize(): Promise<void>;
+declare function unminimize(): Promise<void>;
+declare function isMinimized(): Promise<boolean>;
+declare function setFullScreen(): Promise<void>;
+declare function exitFullScreen(): Promise<void>;
+declare function isFullScreen(): Promise<boolean>;
+declare function show(): Promise<void>;
+declare function hide(): Promise<void>;
+declare function isVisible(): Promise<boolean>;
+declare function focus$1(): Promise<void>;
+declare function setIcon(icon: string): Promise<void>;
+declare function move$1(x: number, y: number): Promise<void>;
+declare function center(): Promise<void>;
+declare function beginDrag(screenX?: number, screenY?: number): Promise<void>;
+declare function setDraggableRegion(DOMElementOrId: string | HTMLElement, options?: {
+	exclude?: Array<string | HTMLElement>;
+}): Promise<{
+	success: true;
+	message: string;
+	exclusions: {
+		add(elements: Array<string | HTMLElement>): void;
+		remove(elements: Array<string | HTMLElement>): void;
+		removeAll(): void;
+	};
+}>;
+declare function unsetDraggableRegion(DOMElementOrId: string | HTMLElement): Promise<{
+	success: true;
+	message: string;
+}>;
+declare function setSize(options: WindowSizeOptions): Promise<void>;
+declare function getSize(): Promise<WindowSizeOptions>;
+declare function getPosition(): Promise<WindowPosOptions>;
+declare function setAlwaysOnTop(onTop: boolean): Promise<void>;
+declare function setBorderless(borderless: boolean): Promise<void>;
+declare function create(url: string, options?: WindowOptions): Promise<void>;
+declare function snapshot(path: string): Promise<void>;
+declare function setMainMenu(options: WindowMenu): Promise<void>;
+declare function print$1(): Promise<void>;
+interface Response$1 {
+	success: boolean;
+	message: string;
+}
+export type Builtin = "ready" | "trayMenuItemClicked" | "windowClose" | "serverOffline" | "clientConnect" | "clientDisconnect" | "appClientConnect" | "appClientDisconnect" | "extClientConnect" | "extClientDisconnect" | "extensionReady" | "neuDev_reloadApp";
+declare function on(event: string, handler: (ev: CustomEvent) => void): Promise<Response$1>;
+declare function off(event: string, handler: (ev: CustomEvent) => void): Promise<Response$1>;
+declare function dispatch(event: string, data?: any): Promise<Response$1>;
+declare function broadcast$1(event: string, data?: any): Promise<void>;
+export interface ExtensionStats {
+	loaded: string[];
+	connected: string[];
+}
+declare function dispatch$1(extensionId: string, event: string, data?: any): Promise<void>;
+declare function broadcast$2(event: string, data?: any): Promise<void>;
+declare function getStats$1(): Promise<ExtensionStats>;
+export interface Manifest {
+	applicationId: string;
+	version: string;
+	resourcesURL: string;
+}
+declare function checkForUpdates(url: string): Promise<Manifest>;
+declare function install(): Promise<void>;
+export interface ClipboardImage {
+	width: number;
+	height: number;
+	bpp: number;
+	bpr: number;
+	redMask: number;
+	greenMask: number;
+	blueMask: number;
+	redShift: number;
+	greenShift: number;
+	blueShift: number;
+	data: ArrayBuffer;
+}
+declare function getFormat(): Promise<ClipboardFormat>;
+declare function readText(): Promise<string>;
+declare function readImage(format?: string): Promise<ClipboardImage | null>;
+declare function writeText(data: string): Promise<void>;
+declare function writeImage(image: ClipboardImage): Promise<void>;
+declare function readHTML(): Promise<string>;
+declare function writeHTML(data: string): Promise<void>;
+declare function clear$1(): Promise<void>;
+interface Stats$1 {
+	size: number;
+	isFile: boolean;
+	isDirectory: boolean;
+}
+declare function getFiles(): Promise<string[]>;
+declare function getStats$2(path: string): Promise<Stats$1>;
+declare function extractFile(path: string, destination: string): Promise<void>;
+declare function extractDirectory(path: string, destination: string): Promise<void>;
+declare function readFile$1(path: string): Promise<string>;
+declare function readBinaryFile$1(path: string): Promise<ArrayBuffer>;
+declare function mount(path: string, target: string): Promise<void>;
+declare function unmount(path: string): Promise<void>;
+declare function getMounts(): Promise<Record<string, string>>;
+declare function getMethods(): Promise<string[]>;
+export interface InitOptions {
+	exportCustomMethods?: boolean;
+}
+export declare function init(options?: InitOptions): void;
+export type ErrorCode = "NE_FS_DIRCRER" | "NE_FS_RMDIRER" | "NE_FS_FILRDER" | "NE_FS_FILWRER" | "NE_FS_FILRMER" | "NE_FS_NOPATHE" | "NE_FS_COPYFER" | "NE_FS_MOVEFER" | "NE_OS_INVMSGA" | "NE_OS_INVKNPT" | "NE_ST_INVSTKY" | "NE_ST_STKEYWE" | "NE_RT_INVTOKN" | "NE_RT_NATPRME" | "NE_RT_APIPRME" | "NE_RT_NATRTER" | "NE_RT_NATNTIM" | "NE_CL_NSEROFF" | "NE_EX_EXTNOTC" | "NE_UP_CUPDMER" | "NE_UP_CUPDERR" | "NE_UP_UPDNOUF" | "NE_UP_UPDINER";
+interface Error$1 {
+	code: ErrorCode;
+	message: string;
+}
+declare global {
+	interface Window {
+		/** Mode of the application: window, browser, cloud, or chrome */
+		NL_MODE: Mode;
+		/** Application port */
+		NL_PORT: number;
+		/** Command-line arguments */
+		NL_ARGS: string[];
+		/** Basic authentication token */
+		NL_TOKEN: string;
+		/** Neutralinojs client version */
+		NL_CVERSION: string;
+		/** Application identifier */
+		NL_APPID: string;
+		/** Application version */
+		NL_APPVERSION: string;
+		/** Application path */
+		NL_PATH: string;
+		/** Application data path */
+		NL_DATAPATH: string;
+		/** Returns true if extensions are enabled */
+		NL_EXTENABLED: boolean;
+		/** Returns true if the client library is injected */
+		NL_GINJECTED: boolean;
+		/** Returns true if globals are injected */
+		NL_CINJECTED: boolean;
+		/** Operating system name: Linux, Windows, Darwin, FreeBSD, or Uknown */
+		NL_OS: OperatingSystem;
+		/** CPU architecture: x64, arm, itanium, ia32, or unknown */
+		NL_ARCH: Architecture;
+		/** Neutralinojs server version */
+		NL_VERSION: string;
+		/** Current working directory */
+		NL_CWD: string;
+		/** Identifier of the current process */
+		NL_PID: string;
+		/** Source of application resources: bundle or directory */
+		NL_RESMODE: string;
+		/** Release commit of the client library */
+		NL_CCOMMIT: string;
+		/** An array of custom methods */
+		NL_CMETHODS: string[];
+	}
+	/** Neutralino global object for custom methods **/
+	const Neutralino: any;
+}
+
+declare namespace custom {
+	export { getMethods };
+}
+declare namespace filesystem {
+	export { appendBinaryFile, appendFile, copy, createDirectory, createWatcher, getAbsolutePath, getJoinedPath, getNormalizedPath, getOpenedFileInfo, getPathParts, getPermissions, getRelativePath, getStats, getUnnormalizedPath, getWatchers, move, openFile, readBinaryFile, readDirectory, readFile, remove, removeWatcher, setPermissions, updateOpenedFile, writeBinaryFile, writeFile };
+}
+declare namespace os {
+	export { execCommand, getEnv, getEnvs, getPath, getSpawnedProcesses, open$1 as open, setTray, showFolderDialog, showMessageBox, showNotification, showOpenDialog, showSaveDialog, spawnProcess, updateSpawnedProcess };
+}
+declare namespace computer {
+	export { getArch, getCPUInfo, getDisplays, getKernelInfo, getMemoryInfo, getMousePosition, getOSInfo };
+}
+declare namespace storage {
+	export { clear, getData, getKeys, removeData, setData };
+}
+declare namespace debug {
+	export { log };
+}
+declare namespace app {
+	export { broadcast, exit, getConfig, killProcess, readProcessInput, restartProcess, writeProcessError, writeProcessOutput };
+}
+declare namespace window$1 {
+	export { beginDrag, center, create, exitFullScreen, focus$1 as focus, getPosition, getSize, getTitle, hide, isFullScreen, isMaximized, isMinimized, isVisible, maximize, minimize, move$1 as move, print$1 as print, setAlwaysOnTop, setBorderless, setDraggableRegion, setFullScreen, setIcon, setMainMenu, setSize, setTitle, show, snapshot, unmaximize, unminimize, unsetDraggableRegion };
+}
+declare namespace events {
+	export { broadcast$1 as broadcast, dispatch, off, on };
+}
+declare namespace extensions {
+	export { broadcast$2 as broadcast, dispatch$1 as dispatch, getStats$1 as getStats };
+}
+declare namespace updater {
+	export { checkForUpdates, install };
+}
+declare namespace clipboard {
+	export { clear$1 as clear, getFormat, readHTML, readImage, readText, writeHTML, writeImage, writeText };
+}
+declare namespace resources {
+	export { extractDirectory, extractFile, getFiles, getStats$2 as getStats, readBinaryFile$1 as readBinaryFile, readFile$1 as readFile };
+}
+declare namespace server {
+	export { getMounts, mount, unmount };
+}
+
+export {
+	Error$1 as Error,
+	Permissions$1 as Permissions,
+	Response$1 as Response,
+	app,
+	clipboard,
+	computer,
+	custom,
+	debug,
+	events,
+	extensions,
+	filesystem,
+	os,
+	resources,
+	server,
+	storage,
+	updater,
+	window$1 as window,
+};
+
+export as namespace Neutralino;
+
+export {};

+ 497 - 0
desktop-app/resources/js/preview-worker.js

@@ -0,0 +1,497 @@
+/* global importScripts, marked, hljs */
+
+let librariesLoaded = false;
+let markedConfigured = false;
+let mermaidIdCounter = 0;
+
+const markedOptions = {
+  gfm: true,
+  breaks: true,
+  pedantic: false,
+  sanitize: false,
+  smartypants: false,
+  xhtml: false,
+  headerIds: true,
+  mangle: false,
+};
+
+const BLOCK_MATH_MARKER_PATTERN = /^\$\$/m;
+const BLOCK_MATH_PATTERN = /^\$\$[ \t]*\n?([\s\S]*?)\n?\$\$[ \t]*(?:\n|$)/;
+const DEFINITION_LIST_ITEM_PATTERN = /^:[ \t]+(.*)$/;
+const SUPERSCRIPT_PATTERN = /^\^(?!\s)([^^\n]*?\S)\^(?!\^)/;
+const SUBSCRIPT_PATTERN = /^~(?!~)(?!\s)([^~\n]*?\S)~(?!~)/;
+const HIGHLIGHT_PATTERN = /^==(?=\S)([\s\S]*?\S)==/;
+const MARKDOWN_LIST_MARKER_PATTERN = /^(\s*)(?:[-*+]\s+|\d+\.\s+|>\s+)/;
+const EMPTY_LINE_PATTERN = /^\s*$/;
+
+let suppressFootnotePreprocess = false;
+const footnoteDefinitions = new Map();
+const footnoteOrder = [];
+const footnoteRefCounts = new Map();
+const footnoteFirstRefId = new Map();
+let anonymousFootnoteCounter = 0;
+
+function escapeHtml(str) {
+  return String(str)
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;");
+}
+
+function escapeHtmlAttribute(value) {
+  return String(value)
+    .replace(/&/g, "&amp;")
+    .replace(/"/g, "&quot;")
+    .replace(/'/g, "&#39;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;");
+}
+
+function resetExtendedMarkdownState() {
+  footnoteDefinitions.clear();
+  footnoteOrder.length = 0;
+  footnoteRefCounts.clear();
+  footnoteFirstRefId.clear();
+  anonymousFootnoteCounter = 0;
+}
+
+function normalizeFootnoteId(id) {
+  const normalized = String(id || "")
+    .trim()
+    .toLowerCase()
+    .replace(/[^a-z0-9_-]+/g, "-")
+    .replace(/^-+|-+$/g, "");
+  if (normalized) return normalized;
+  anonymousFootnoteCounter += 1;
+  return `footnote-${anonymousFootnoteCounter}`;
+}
+
+function parseInlineWithoutFootnotes(text) {
+  suppressFootnotePreprocess = true;
+  try {
+    return marked.parseInline(text);
+  } finally {
+    suppressFootnotePreprocess = false;
+  }
+}
+
+function renderDefinitionContent(content, options) {
+  const appendHtml = options && options.appendHtml ? options.appendHtml : "";
+  const paragraphs = String(content || "")
+    .split(/\n(?:[ \t]*\n)+/)
+    .map((paragraph) => paragraph.trim())
+    .filter(Boolean);
+
+  if (appendHtml) {
+    if (paragraphs.length === 0) {
+      paragraphs.push(appendHtml);
+    } else {
+      paragraphs[paragraphs.length - 1] = `${paragraphs[paragraphs.length - 1]} ${appendHtml}`;
+    }
+  }
+
+  return paragraphs
+    .map((paragraph) => `<p>${parseInlineWithoutFootnotes(paragraph)}</p>`)
+    .join("");
+}
+
+function extractFootnoteDefinitions(markdown) {
+  const lines = markdown.split("\n");
+  const preservedLines = [];
+  let index = 0;
+
+  while (index < lines.length) {
+    const match = /^([ \t]{0,3})\[\^([^\]\n]+)\]:[ \t]*(.*)$/.exec(lines[index]);
+    if (!match) {
+      preservedLines.push(lines[index]);
+      index += 1;
+      continue;
+    }
+
+    const baseIndent = match[1] || "";
+    const id = match[2].trim();
+    const definitionLines = [match[3] || ""];
+    index += 1;
+
+    while (index < lines.length) {
+      const line = lines[index];
+      if (!line.startsWith(baseIndent)) break;
+      const lineAfterBase = line.slice(baseIndent.length);
+      const indentedMatch = /^(?: {2,}|\t)(.*)$/.exec(lineAfterBase);
+      if (indentedMatch) {
+        definitionLines.push(indentedMatch[1]);
+        index += 1;
+        continue;
+      }
+      if (lineAfterBase.trim() === "") {
+        const nextLine = lines[index + 1] || "";
+        const nextAfterBase = nextLine.startsWith(baseIndent) ? nextLine.slice(baseIndent.length) : "";
+        if (/^(?: {2,}|\t)/.test(nextAfterBase)) {
+          definitionLines.push("");
+          index += 1;
+          continue;
+        }
+      }
+      break;
+    }
+
+    footnoteDefinitions.set(id, definitionLines.join("\n").trim());
+  }
+
+  return preservedLines.join("\n");
+}
+
+function applyFootnotes(markdown) {
+  const markdownWithReferences = markdown.replace(/\[\^([^\]\n]+)\]/g, function(match, idText) {
+    const id = idText.trim();
+    if (!id) return match;
+    if (!footnoteOrder.includes(id)) footnoteOrder.push(id);
+
+    const refCount = (footnoteRefCounts.get(id) || 0) + 1;
+    footnoteRefCounts.set(id, refCount);
+
+    const normalizedId = normalizeFootnoteId(id);
+    const refId = `fnref-${normalizedId}${refCount > 1 ? `-${refCount}` : ""}`;
+    if (!footnoteFirstRefId.has(id)) footnoteFirstRefId.set(id, refId);
+
+    const noteNumber = footnoteOrder.indexOf(id) + 1;
+    return `<sup id="${escapeHtmlAttribute(refId)}" class="footnote-ref"><a href="#fn-${escapeHtmlAttribute(normalizedId)}" aria-label="Footnote ${noteNumber}">[${noteNumber}]</a></sup>`;
+  });
+
+  const footnotesHtml = footnoteOrder
+    .filter((id) => footnoteDefinitions.has(id))
+    .map((id) => {
+      const normalizedId = normalizeFootnoteId(id);
+      const backRefId = footnoteFirstRefId.get(id) || `fnref-${normalizedId}`;
+      const backRefHtml = `<a href="#${escapeHtmlAttribute(backRefId)}" class="footnote-backref" aria-label="Back to content">&#8592;</a>`;
+      const noteHtml = renderDefinitionContent(footnoteDefinitions.get(id) || "", { appendHtml: backRefHtml });
+      return `<li id="fn-${escapeHtmlAttribute(normalizedId)}">${noteHtml}</li>`;
+    })
+    .join("");
+
+  if (!footnotesHtml) return markdownWithReferences;
+  return `${markdownWithReferences}\n\n<section class="footnotes"><hr><ol>${footnotesHtml}</ol></section>`;
+}
+
+function configureMarked() {
+  if (markedConfigured) return;
+
+  const renderer = new marked.Renderer();
+  const blockMathExtension = {
+    name: "blockMath",
+    level: "block",
+    start(src) {
+      const match = src.match(BLOCK_MATH_MARKER_PATTERN);
+      return match ? match.index : undefined;
+    },
+    tokenizer(src) {
+      const match = BLOCK_MATH_PATTERN.exec(src);
+      if (!match) return undefined;
+      return { type: "blockMath", raw: match[0], text: match[1] };
+    },
+    renderer(token) {
+      return `<div class="math-block">$$\n${token.text}\n$$</div>\n`;
+    },
+  };
+
+  const definitionListExtension = {
+    name: "definitionList",
+    level: "block",
+    start(src) {
+      const match = src.match(/\n:[ \t]+/);
+      return match ? match.index + 1 : undefined;
+    },
+    tokenizer(src) {
+      const lines = src.split("\n");
+      if (lines.length < 2) return undefined;
+
+      const term = lines[0];
+      if (EMPTY_LINE_PATTERN.test(term) || MARKDOWN_LIST_MARKER_PATTERN.test(term)) return undefined;
+      if (!DEFINITION_LIST_ITEM_PATTERN.test(lines[1])) return undefined;
+
+      const definitions = [];
+      const rawLines = [term];
+      let index = 1;
+      while (index < lines.length) {
+        const itemMatch = DEFINITION_LIST_ITEM_PATTERN.exec(lines[index]);
+        if (!itemMatch) break;
+
+        rawLines.push(lines[index]);
+        const definitionLines = [itemMatch[1]];
+        index += 1;
+
+        while (index < lines.length) {
+          const line = lines[index];
+          if (DEFINITION_LIST_ITEM_PATTERN.test(line)) break;
+          if (EMPTY_LINE_PATTERN.test(line)) {
+            const nextLine = lines[index + 1] || "";
+            if (/^(?: {2,}|\t)/.test(nextLine)) {
+              rawLines.push(line);
+              definitionLines.push("");
+              index += 1;
+              continue;
+            }
+            break;
+          }
+          const continuationMatch = /^(?: {2,}|\t)(.*)$/.exec(line);
+          if (!continuationMatch) break;
+          rawLines.push(line);
+          definitionLines.push(continuationMatch[1]);
+          index += 1;
+        }
+
+        definitions.push(definitionLines.join("\n").trim());
+      }
+
+      if (definitions.length === 0) return undefined;
+      let raw = rawLines.join("\n");
+      if (src.startsWith(raw + "\n")) raw += "\n";
+      return { type: "definitionList", raw, term: term.trim(), definitions };
+    },
+    renderer(token) {
+      const termHtml = parseInlineWithoutFootnotes(token.term);
+      const definitionHtml = token.definitions
+        .map((definition) => `<dd>${renderDefinitionContent(definition)}</dd>`)
+        .join("");
+      return `<dl><dt>${termHtml}</dt>${definitionHtml}</dl>\n`;
+    },
+  };
+
+  const superscriptExtension = {
+    name: "superscript",
+    level: "inline",
+    start(src) {
+      const index = src.indexOf("^");
+      return index >= 0 ? index : undefined;
+    },
+    tokenizer(src) {
+      const match = SUPERSCRIPT_PATTERN.exec(src);
+      return match ? { type: "superscript", raw: match[0], text: match[1] } : undefined;
+    },
+    renderer(token) {
+      return `<sup>${marked.parseInline(token.text)}</sup>`;
+    },
+  };
+
+  const subscriptExtension = {
+    name: "subscript",
+    level: "inline",
+    start(src) {
+      const index = src.indexOf("~");
+      return index >= 0 ? index : undefined;
+    },
+    tokenizer(src) {
+      const match = SUBSCRIPT_PATTERN.exec(src);
+      return match ? { type: "subscript", raw: match[0], text: match[1] } : undefined;
+    },
+    renderer(token) {
+      return `<sub>${marked.parseInline(token.text)}</sub>`;
+    },
+  };
+
+  const highlightExtension = {
+    name: "highlight",
+    level: "inline",
+    start(src) {
+      const index = src.indexOf("==");
+      return index >= 0 ? index : undefined;
+    },
+    tokenizer(src) {
+      const match = HIGHLIGHT_PATTERN.exec(src);
+      return match ? { type: "highlight", raw: match[0], text: match[1] } : undefined;
+    },
+    renderer(token) {
+      return `<mark>${marked.parseInline(token.text)}</mark>`;
+    },
+  };
+
+  renderer.code = function(code, language) {
+    if (language === "mermaid") {
+      const uniqueId = `mermaid-diagram-worker-${mermaidIdCounter++}`;
+      return `<div class="mermaid-container is-loading"><div class="mermaid" id="${uniqueId}" data-original-code="${encodeURIComponent(code)}">${escapeHtml(code)}</div></div>`;
+    }
+
+    const validLanguage = hljs && hljs.getLanguage(language) ? language : "plaintext";
+    const highlightedCode = hljs
+      ? hljs.highlight(code, { language: validLanguage }).value
+      : escapeHtml(code);
+    return `<pre><code class="hljs ${escapeHtmlAttribute(validLanguage)}">${highlightedCode}</code></pre>`;
+  };
+
+  renderer.heading = function(text, level, raw) {
+    let id = raw
+      .toLowerCase()
+      .trim()
+      .replace(/<[^>]*>/g, '')
+      .replace(/\s+/g, '-')
+      .replace(/[^\w-]/g, '')
+      .replace(/-+/g, '-');
+    if (!id) {
+      id = `heading-worker-${Math.random().toString(36).substr(2, 9)}`;
+    }
+    return `<h${level} id="${id}">${text}</h${level}>`;
+  };
+
+  marked.use({
+    extensions: [
+      blockMathExtension,
+      definitionListExtension,
+      superscriptExtension,
+      subscriptExtension,
+      highlightExtension,
+    ],
+    hooks: {
+      preprocess(markdown) {
+        if (suppressFootnotePreprocess) return markdown;
+        resetExtendedMarkdownState();
+        const protectedMarkdown = markdown.replace(/\\\$/g, "&#36;");
+        return applyFootnotes(extractFootnoteDefinitions(protectedMarkdown));
+      },
+    },
+  });
+
+  marked.setOptions(Object.assign({}, markedOptions, { renderer }));
+  markedConfigured = true;
+}
+
+function ensureLibraries(urls) {
+  if (!librariesLoaded) {
+    importScripts(urls.marked, urls.highlight);
+    librariesLoaded = true;
+  }
+  configureMarked();
+}
+
+function isSegmentedPreviewSafe(markdown) {
+  if (/^\s*---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/.test(markdown)) return false;
+  if (/^\[[^\]\n]+\]:\s+\S+/m.test(markdown)) return false;
+  if (/\[\^[^\]\n]+\]/.test(markdown)) return false;
+  if (/\n:[ \t]+/.test(markdown)) return false;
+  if (/^\s{0,3}<\/?[a-zA-Z][\w:-]*(?:\s|>|\/>)/m.test(markdown)) return false;
+  return true;
+}
+
+function hashString(value) {
+  let hash = 2166136261;
+  for (let i = 0; i < value.length; i += 1) {
+    hash ^= value.charCodeAt(i);
+    hash = Math.imul(hash, 16777619);
+  }
+  return (hash >>> 0).toString(36);
+}
+
+function splitMarkdownBlocks(markdown) {
+  const normalized = String(markdown || "").replace(/\r\n/g, "\n");
+  const lines = normalized.split("\n");
+  const blocks = [];
+  let buffer = [];
+  let startLine = 1;
+  let inFence = false;
+  let fenceChar = "";
+  let fenceLength = 0;
+  let inMathBlock = false;
+
+  function flush(endLine) {
+    const source = buffer.join("\n").trimEnd();
+    if (source.trim()) {
+      blocks.push({
+        source,
+        startLine,
+        endLine,
+      });
+    }
+    buffer = [];
+  }
+
+  for (let index = 0; index < lines.length; index += 1) {
+    const line = lines[index];
+    const lineNumber = index + 1;
+    const fenceMatch = /^ {0,3}(`{3,}|~{3,})/.exec(line);
+    const trimmed = line.trim();
+
+    if (fenceMatch) {
+      const marker = fenceMatch[1];
+      if (!inFence) {
+        inFence = true;
+        fenceChar = marker[0];
+        fenceLength = marker.length;
+      } else if (marker[0] === fenceChar && marker.length >= fenceLength) {
+        inFence = false;
+      }
+    }
+
+    if (!inFence && trimmed === "$$") {
+      inMathBlock = !inMathBlock;
+    }
+
+    if (!inFence && !inMathBlock && trimmed === "") {
+      flush(lineNumber);
+      startLine = lineNumber + 1;
+      continue;
+    }
+
+    if (buffer.length === 0) startLine = lineNumber;
+    buffer.push(line);
+  }
+
+  flush(lines.length);
+  return blocks;
+}
+
+function renderSegmentedMarkdown(markdown, options) {
+  if (!isSegmentedPreviewSafe(markdown)) {
+    return { mode: "full-required", reason: "unsafe-markdown" };
+  }
+
+  const blocks = splitMarkdownBlocks(markdown);
+  if (blocks.length < (options.minimumBlocks || 1)) {
+    return { mode: "full-required", reason: "too-few-blocks" };
+  }
+
+  const seenHashes = new Map();
+  const renderedBlocks = blocks.map((block) => {
+    const hash = hashString(block.source);
+    const seenCount = seenHashes.get(hash) || 0;
+    seenHashes.set(hash, seenCount + 1);
+    const html = marked.parse(block.source);
+    return {
+      id: `preview-block-${hash}-${seenCount}`,
+      hash,
+      html,
+      htmlLength: html.length,
+      sourceLength: block.source.length,
+      startLine: block.startLine,
+      endLine: block.endLine,
+    };
+  });
+
+  return {
+    mode: "segmented",
+    blocks: renderedBlocks,
+    blockCount: renderedBlocks.length,
+  };
+}
+
+self.onmessage = function(event) {
+  const data = event.data || {};
+  if (data.type !== "render") return;
+
+  try {
+    const options = data.options || {};
+    ensureLibraries(options.libraryUrls || {});
+    mermaidIdCounter = 0;
+    const result = renderSegmentedMarkdown(data.markdown || "", options);
+    self.postMessage({
+      type: "render-result",
+      requestId: data.requestId,
+      result,
+    });
+  } catch (error) {
+    self.postMessage({
+      type: "render-error",
+      requestId: data.requestId,
+      error: error && error.message ? error.message : "Preview worker render failed.",
+    });
+  }
+};

+ 60 - 0
desktop-app/setup-binaries.js

@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+
+/**
+ * setup-binaries.js — Idempotent Neutralinojs binary setup.
+ *
+ * Ensures the bin/ folder contains platform binaries matching the version
+ * pinned in neutralino.config.json (cli.binaryVersion). Downloads them
+ * via `neu update` only when missing or when the pinned version changes.
+ *
+ * A version marker (bin/.version) tracks the installed version so that
+ * repeated builds and dev runs skip the download entirely.
+ *
+ * Run from the desktop-app/ directory:
+ *   node setup-binaries.js
+ */
+
+const fs = require("fs");
+const path = require("path");
+const { execSync } = require("child_process");
+
+const CONFIG_FILE = path.resolve(__dirname, "neutralino.config.json");
+const BIN_DIR = path.resolve(__dirname, "bin");
+const VERSION_MARKER = path.join(BIN_DIR, ".version");
+
+/** Neu CLI package — same version used across all npm scripts */
+const NEU_CLI = "@neutralinojs/neu@11.7.0";
+
+const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
+const expectedVersion = config.cli.binaryVersion;
+
+if (!expectedVersion) {
+  console.error("✗ cli.binaryVersion not set in neutralino.config.json");
+  process.exit(1);
+}
+
+/** Check if binaries are already present and match the expected version */
+if (fs.existsSync(VERSION_MARKER)) {
+  const installed = fs.readFileSync(VERSION_MARKER, "utf-8").trim();
+  if (installed === expectedVersion) {
+    console.log(
+      `✓ Neutralinojs binaries v${expectedVersion} already present — skipping download`,
+    );
+    process.exit(0);
+  }
+  console.log(
+    `↻ Version changed (${installed} → ${expectedVersion}) — re-downloading`,
+  );
+}
+
+/** Download binaries + client library via neu update */
+console.log(`⬇ Downloading Neutralinojs v${expectedVersion} binaries...`);
+execSync(`npx -y ${NEU_CLI} update`, {
+  cwd: __dirname,
+  stdio: "inherit",
+});
+
+/** Write version marker so subsequent runs are no-ops */
+fs.mkdirSync(BIN_DIR, { recursive: true });
+fs.writeFileSync(VERSION_MARKER, expectedVersion, "utf-8");
+console.log(`✓ Neutralinojs binaries v${expectedVersion} ready`);

+ 14 - 0
docker-compose.yml

@@ -0,0 +1,14 @@
+# PERF-035: Removed deprecated 'version' key (Docker Compose v2+ ignores it)
+
+services:
+  markdown-viewer:
+    build:
+      context: .
+      dockerfile: Dockerfile
+    ports:
+      - "8080:80"
+    container_name: markdown-plusplus
+    restart: unless-stopped
+    environment:
+      - NGINX_HOST=localhost
+      - NGINX_PORT=80

+ 1117 - 0
index.html

@@ -0,0 +1,1117 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <!-- DNS Prefetch & Preconnect CDN Origins to Warm Up Latency -->
+    <link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
+    <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
+    <link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
+    <link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
+
+    <!-- PERF-015: Preload critical-path resources for faster discovery -->
+    <link rel="preload" href="styles.css" as="style">
+    <link rel="preload" href="script.js" as="script">
+
+    <!-- Canonical Link -->
+    <link rel="canonical" href="https://markdownplusplus.pages.dev/">
+
+    <!-- Multilingual Hreflang Tags for Search Crawlers -->
+    <link rel="alternate" hreflang="x-default" href="https://markdownplusplus.pages.dev/" />
+    <link rel="alternate" hreflang="en" href="https://markdownplusplus.pages.dev/?lang=en" />
+    <link rel="alternate" hreflang="zh-Hans" href="https://markdownplusplus.pages.dev/?lang=zh" />
+    <link rel="alternate" hreflang="ja" href="https://markdownplusplus.pages.dev/?lang=ja" />
+    <link rel="alternate" hreflang="ko" href="https://markdownplusplus.pages.dev/?lang=ko" />
+    <link rel="alternate" hreflang="pt-BR" href="https://markdownplusplus.pages.dev/?lang=pt" />
+    <link rel="alternate" hreflang="es" href="https://markdownplusplus.pages.dev/?lang=es" />
+    <link rel="alternate" hreflang="fr" href="https://markdownplusplus.pages.dev/?lang=fr" />
+    <link rel="alternate" hreflang="de" href="https://markdownplusplus.pages.dev/?lang=de" />
+    <link rel="alternate" hreflang="ru" href="https://markdownplusplus.pages.dev/?lang=ru" />
+    <link rel="alternate" hreflang="it" href="https://markdownplusplus.pages.dev/?lang=it" />
+    <link rel="alternate" hreflang="tr" href="https://markdownplusplus.pages.dev/?lang=tr" />
+    <link rel="alternate" hreflang="pl" href="https://markdownplusplus.pages.dev/?lang=pl" />
+    <link rel="alternate" hreflang="zh-Hant" href="https://markdownplusplus.pages.dev/?lang=tw" />
+    <link rel="alternate" hreflang="uk" href="https://markdownplusplus.pages.dev/?lang=uk" />
+    <!-- PWA Web Manifest -->
+    <link rel="manifest" href="manifest.json">
+
+    <!-- Primary Meta Tags -->
+    <meta name="title" content="Markdown ++">
+    <meta name="description" content="Markdown ++ is a powerful GitHub-style Markdown rendering tool with live preview, LaTeX math, Mermaid diagrams, syntax highlighting, dark mode, and export options to PDF, HTML, and MD—all fully client-side and secure.">
+    <meta name="keywords" content="Markdown ++, GitHub-style markdown, live preview, markdown editor, LaTeX support, Mermaid diagrams, PDF export, syntax highlighting, markdown to HTML, secure markdown tool, client-side Markdown ++, Parv Ashwani, advanced markdown parser, future Markdown ++, next-gen markdown tool">
+    <meta name="author" content="Parv Ashwani">
+    <meta name="robots" content="index, follow">
+    <meta name="language" content="English">
+    <meta name="distribution" content="global">
+    <meta name="rating" content="general">
+
+    <!-- Open Graph / Facebook -->
+    <meta property="og:type" content="website">
+    <meta property="og:url" content="https://markdownplusplus.pages.dev/">
+    <meta property="og:title" content="Markdown ++">
+    <meta property="og:description" content="Markdown ++ is a powerful GitHub-style Markdown rendering tool with live preview, LaTeX math, Mermaid diagrams, syntax highlighting, dark mode, and export options to PDF, HTML, and MD—all fully client-side and secure.">
+    <meta property="og:image" content="https://markdownplusplus.pages.dev/assets/icon.jpg">
+
+    <!-- Twitter -->
+    <meta property="twitter:card" content="summary_large_image">
+    <meta property="twitter:url" content="https://markdownplusplus.pages.dev/">
+    <meta property="twitter:title" content="Markdown ++">
+    <meta property="twitter:description" content="Markdown ++ is a powerful GitHub-style Markdown rendering tool with live preview, LaTeX math, Mermaid diagrams, syntax highlighting, dark mode, and export options to PDF, HTML, and MD—all fully client-side and secure.">
+    <meta property="twitter:image" content="https://markdownplusplus.pages.dev/assets/icon.jpg">
+
+    <!-- JSON-LD Structured Data Schema for Search Rich Snippets -->
+    <script type="application/ld+json">
+    {
+      "@context": "https://schema.org",
+      "@type": "WebApplication",
+      "name": "Markdown ++",
+      "url": "https://markdownplusplus.pages.dev/",
+      "image": "https://markdownplusplus.pages.dev/assets/icon.jpg",
+      "description": "A powerful GitHub-style Markdown rendering tool with live preview, LaTeX, Mermaid, syntax highlighting, and PDF export.",
+      "applicationCategory": "DeveloperApplication",
+      "operatingSystem": "All",
+      "browserRequirements": "Requires HTML5 compatible browser",
+      "author": {
+        "@type": "Organization",
+        "name": "Parv Ashwani",
+        "url": "https://github.com/Parv Ashwani"
+      },
+      "offers": {
+        "@type": "Offer",
+        "price": "0.00",
+        "priceCurrency": "USD"
+      }
+    }
+    </script>
+
+    <title>Markdown ++</title>
+    <link href="assets/icon.jpg" rel="icon" type="image/jpg">
+    <!-- Updated libraries to latest versions with Subresource Integrity (SRI) -->
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown.min.css" integrity="sha384-hZuxRjC/Dsr4zEx1JlUhDQqkvqBPp2VLHsgXfnxPq1ULDy1eIdWCiux7nvO1RIZP" crossorigin="anonymous">
+    <link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" as="style" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous" onload="this.onload=null;this.rel='stylesheet'">
+    <noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous"></noscript>
+    <link rel="stylesheet" href="styles.css">
+    
+    <!-- Loading order optimized - ensure libraries are loaded asynchronously using defer -->
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js" integrity="sha384-odPBjvtXVM/5hOYIr3A1dB+flh0c3wAT3bSesIOqEGmyUA4JoKf/YTWy0XKOYAY7" crossorigin="anonymous" defer></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp" crossorigin="anonymous" defer></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.9/purify.min.js" integrity="sha384-3HPB1XT51W3gGRxAmZ+qbZwRpRlFQL632y8x+adAqCr4Wp3TaWwCLSTAJJKbyWEK" crossorigin="anonymous" defer></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js" integrity="sha384-PlRSzpewlarQuj5alIadXwjNUX+2eNMKwr0f07ShWYLy8B6TjEbm7ZlcN/ScSbwy" crossorigin="anonymous" defer></script>
+    <!-- PERF-002: MathJax, Mermaid, JoyPixels, jsPDF, html2canvas, pako are now lazy-loaded by script.js on first use -->
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js" integrity="sha384-+pxiN6T7yvpryuJmE1gM9PX7yQit15auDb+ZwwvJOd/4be2Cie5/IuVXgQb/S9du" crossorigin="anonymous" defer></script>
+</head>
+<body>
+    <div class="app-container">
+        <header class="app-header">
+            <div class="container-fluid d-flex justify-content-between align-items-center header-container">
+                <!-- Left section: Logo, GitHub, Stats -->
+                <div class="d-flex align-items-center header-left">
+                    <h1 class="h4 mb-0 me-2">Markdown ++</h1>
+                    <a href="https://git.4parv.in/parv.ashwani/markdownplusplus/" class="github-link" title="View on GitHub" target="_blank" rel="noopener noreferrer">
+                        <i class="bi bi-github"></i>
+                    </a>
+                    <div id="stats-container" class="stats-container d-flex align-items-center d-none d-md-flex">
+                        <div class="stat-item me-3">
+                            <i class="bi bi-clock me-1"></i> <span id="reading-time">0</span> <span id="lbl-min-read">Min Read</span>
+                        </div>
+                        <div class="stat-item me-3">
+                            <i class="bi bi-text-paragraph me-1"></i> <span id="word-count">0</span> <span id="lbl-words">Words</span>
+                        </div>
+                        <div class="stat-item">
+                            <i class="bi bi-fonts me-1"></i> <span id="char-count">0</span> <span id="lbl-chars">Chars</span>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- Right section: Toolbar -->
+                <div class="toolbar d-none d-md-flex header-right">
+                    <div class="toolbar-group view-toolbar" role="group" aria-label="View mode">
+                        <button class="tool-button view-toggle-btn" data-view-mode="editor" aria-pressed="false" title="Editor only" aria-label="Editor only">
+                            <i class="bi bi-file-text"></i>
+                        </button>
+                        <button class="tool-button view-toggle-btn is-active" data-view-mode="split" aria-pressed="true" title="Split view" aria-label="Split view">
+                            <i class="bi bi-layout-split"></i>
+                        </button>
+                        <button class="tool-button view-toggle-btn" data-view-mode="preview" aria-pressed="false" title="Preview only" aria-label="Preview only">
+                            <i class="bi bi-eye"></i>
+                        </button>
+                    </div>
+                    <span class="toolbar-divider" aria-hidden="true"></span>
+                    <button id="toggle-sync" class="tool-button sync-enabled sync-active" title="Toggle Sync Scrolling">
+                        <i class="bi bi-link"></i> <span class="btn-text">Sync Off</span>
+                    </button>
+                    <div class="dropdown">
+                        <button class="tool-button dropdown-toggle" type="button" id="importDropdown" data-bs-toggle="dropdown" aria-expanded="false" title="Import Markdown">
+                            <i class="bi bi-upload"></i> <span class="btn-text">Import</span>
+                        </button>
+                        <ul class="dropdown-menu" aria-labelledby="importDropdown">
+                            <li><a class="dropdown-item" href="#" id="import-from-file"><i class="bi bi-upload me-2"></i>From files</a></li>
+                            <li><a class="dropdown-item" href="#" id="import-from-github"><i class="bi bi-github me-2"></i>From GitHub</a></li>
+                        </ul>
+                    </div>
+                    <input type="file" id="file-input" class="file-input" accept=".md,.markdown,text/markdown">
+                    
+                    <div class="dropdown">
+                        <button class="tool-button dropdown-toggle" type="button" id="exportDropdown" data-bs-toggle="dropdown" aria-expanded="false" title="Export Markdown">
+                            <i class="bi bi-download"></i> <span class="btn-text">Export</span>
+                        </button>
+                        <ul class="dropdown-menu" aria-labelledby="exportDropdown">
+                            <li><a class="dropdown-item" href="#" id="export-md"><i class="bi bi-file-earmark-text me-2"></i>Markdown (.md)</a></li>
+                            <li><a class="dropdown-item" href="#" id="export-html"><i class="bi bi-file-earmark-code me-2"></i>HTML</a></li>
+                            <li><a class="dropdown-item" href="#" id="export-pdf"><i class="bi bi-file-earmark-pdf me-2"></i>PDF</a></li>
+                        </ul>
+                    </div>
+                    
+                    <button id="copy-markdown-button" class="tool-button" title="Copy Markdown">
+                        <i class="bi bi-clipboard"></i> <span class="btn-text">Copy</span>
+                    </button>
+                    
+                    <button id="share-button" class="tool-button" title="Share via URL">
+                        <i class="bi bi-share"></i> <span class="btn-text">Share</span>
+                    </button>
+                    
+                    <div class="dropdown me-1">
+                        <button class="tool-button dropdown-toggle" type="button" id="languageDropdown" data-bs-toggle="dropdown" aria-expanded="false" title="Switch Language">
+                            <i class="bi bi-translate"></i> <span id="current-lang-label" class="btn-text">English</span>
+                        </button>
+                        <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="languageDropdown">
+                            <li><a class="dropdown-item lang-select-item active" href="#" data-lang="en">🇺🇸 English</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="zh">🇨🇳 简体中文</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="ja">🇯🇵 日本語</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="ko">🇰🇷 한국어</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="pt">🇧🇷 Português (Brasil)</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="es">🇪🇸 Español</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="fr">🇫🇷 Français</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="de">🇩🇪 Deutsch</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="ru">🇷🇺 Русский</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="it">🇮🇹 Italiano</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="tr">🇹🇷 Türkçe</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="pl">🇵🇱 Polski</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="tw">🇹🇼 繁體中文</a></li>
+                            <li><a class="dropdown-item lang-select-item" href="#" data-lang="uk">🇺🇦 Українська</a></li>
+                        </ul>
+                    </div>
+
+                    <button id="theme-toggle" class="tool-button" title="Toggle Dark Mode">
+                        <i class="bi bi-moon"></i>
+                    </button>
+                </div>
+                
+                <!-- Hamburger menu for mobile -->
+                <div class="mobile-menu d-md-none">
+                    <button id="mobile-menu-toggle" class="tool-button" title="Menu">
+                        <i class="bi bi-list"></i>
+                    </button>
+                    
+                    <div id="mobile-menu-panel" class="mobile-menu-panel">
+                        <div class="mobile-menu-header">
+                            <h2 class="h5 m-0">Menu</h2>
+                            <button id="close-mobile-menu" class="tool-button" aria-label="Close menu">
+                                <i class="bi bi-x-lg"></i>
+                            </button>
+                        </div>
+
+                        <!-- Mobile Document Tabs Section -->
+                        <div class="mobile-tabs-section mb-3">
+                            <div class="mobile-tabs-header">
+                                <span class="mobile-tabs-label"><i class="bi bi-files me-1"></i> Documents</span>
+                                <button id="mobile-new-tab-btn" class="mobile-new-tab-btn" title="New document" aria-label="New document">
+                                    <i class="bi bi-plus-lg"></i>
+                                </button>
+                            </div>
+                            <div id="mobile-tab-list" class="mobile-tab-list" role="tablist" aria-label="Document tabs">
+                                <!-- Dynamically populated by JavaScript -->
+                            </div>
+                            <button id="mobile-tab-reset-btn" class="tab-reset-btn mt-2 w-100" title="Reset all files" aria-label="Reset all files">
+                                <i class="bi bi-arrow-counterclockwise"></i> Reset all files
+                            </button>
+                        </div>
+
+                        <!-- Story 1.4: Mobile View Mode Buttons -->
+                        <div class="mobile-view-mode-group mb-3" role="group" aria-label="View mode">
+                            <button class="mobile-view-mode-btn" data-mode="editor" aria-pressed="false" title="Editor only">
+                                <i class="bi bi-file-text"></i>
+                                <span>Editor</span>
+                            </button>
+                            <button class="mobile-view-mode-btn active" data-mode="split" aria-pressed="true" title="Split view">
+                                <i class="bi bi-layout-split"></i>
+                                <span>Split</span>
+                            </button>
+                            <button class="mobile-view-mode-btn" data-mode="preview" aria-pressed="false" title="Preview only">
+                                <i class="bi bi-eye"></i>
+                                <span>Preview</span>
+                            </button>
+                        </div>
+
+                        <div class="mobile-stats-container mb-3">
+                            <div class="stat-item mb-2">
+                                <i class="bi bi-clock me-1"></i> <span id="mobile-reading-time">0</span> <span id="lbl-mobile-min-read">Min Read</span>
+                            </div>
+                            <div class="stat-item mb-2">
+                                <i class="bi bi-text-paragraph me-1"></i> <span id="mobile-word-count">0</span> <span id="lbl-mobile-words">Words</span>
+                            </div>
+                            <div class="stat-item">
+                                <i class="bi bi-fonts me-1"></i> <span id="mobile-char-count">0</span> <span id="lbl-mobile-chars">Chars</span>
+                            </div>
+                        </div>
+
+                        <div class="mobile-menu-items">
+                            <button id="mobile-toggle-sync" class="mobile-menu-item tool-button sync-enabled sync-active" title="Toggle Sync Scrolling">
+                                <i class="bi bi-link"></i> Sync Off
+                            </button>
+                            
+                            <button id="mobile-import-button" class="mobile-menu-item" title="Import from files">
+                                <i class="bi bi-upload me-2"></i> Import from files
+                            </button>
+                            
+                            <button id="mobile-import-github-button" class="mobile-menu-item" title="Import from GitHub">
+                                <i class="bi bi-github me-2"></i> Import from GitHub
+                            </button>
+                            
+                            <button id="mobile-export-md" class="mobile-menu-item" title="Export as Markdown">
+                                <i class="bi bi-file-earmark-text me-2"></i> Export as Markdown
+                            </button>
+                            
+                            <button id="mobile-export-html" class="mobile-menu-item" title="Export as HTML">
+                                <i class="bi bi-file-earmark-code me-2"></i> Export as HTML
+                            </button>
+                            
+                            <button id="mobile-export-pdf" class="mobile-menu-item" title="Export as PDF">
+                                <i class="bi bi-file-earmark-pdf me-2"></i> Export as PDF
+                            </button>
+                            
+                            <button id="mobile-copy-markdown" class="mobile-menu-item" title="Copy Markdown">
+                                <i class="bi bi-clipboard me-2"></i> Copy
+                            </button>
+                            
+                            <button id="mobile-share-button" class="mobile-menu-item" title="Share via URL">
+                                <i class="bi bi-share me-2"></i> Share
+                            </button>
+                            
+                            <div class="mobile-menu-item dropdown w-100 p-0 border-0">
+                                <button class="mobile-menu-item w-100 text-start dropdown-toggle" type="button" id="mobileLanguageDropdown" data-bs-toggle="dropdown" aria-expanded="false" title="Switch Language">
+                                    <i class="bi bi-translate me-2"></i> Language: <span id="mobile-current-lang-label">English</span>
+                                </button>
+                                <ul class="dropdown-menu w-100" aria-labelledby="mobileLanguageDropdown">
+                                    <li><a class="dropdown-item lang-select-item active" href="#" data-lang="en">us English</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="zh">CN 简体中文</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="ja">JP 日本語</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="ko">KR 한국어</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="pt">BR Português (Brasil)</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="es">ES Español</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="fr">FR Français</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="de">DE Deutsch</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="ru">RU Русский</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="it">IT Italiano</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="tr">TR Türkçe</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="pl">PL Polski</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="tw">TW 繁體中文</a></li>
+                                    <li><a class="dropdown-item lang-select-item" href="#" data-lang="uk">UK Українська</a></li>
+                                </ul>
+                            </div>
+                            <button id="mobile-theme-toggle" class="mobile-menu-item" title="Toggle Dark Mode">
+                                <i class="bi bi-moon me-2"></i> Dark Mode
+                            </button>
+                        </div>
+                    </div>
+                    
+                    <div id="mobile-menu-overlay"></div>
+                </div>
+            </div>
+        </header>
+
+        <!-- Tab Bar -->
+        <div class="tab-bar" id="tab-bar">
+          <div class="tab-list" id="tab-list" role="tablist" aria-label="Document tabs"></div>
+          <button class="tab-new-btn" id="tab-new-btn" title="New Tab (Ctrl+T)" aria-label="Open new tab">
+            <i class="bi bi-plus-lg"></i> New Tab
+          </button>
+          <button class="tab-reset-btn" id="tab-reset-btn" title="Reset all files" aria-label="Reset all files">
+            <i class="bi bi-arrow-counterclockwise"></i> Reset
+          </button>
+        </div>
+
+        <!-- Markdown Formatting Toolbar -->
+        <div class="markdown-format-toolbar" id="markdown-format-toolbar" role="toolbar" aria-label="Markdown formatting toolbar">
+          <div class="markdown-toolbar-group">
+            <button type="button" class="markdown-tool-btn" data-md-action="undo" title="Undo" aria-label="Undo"><i class="bi bi-arrow-counterclockwise"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="redo" title="Redo" aria-label="Redo"><i class="bi bi-arrow-clockwise"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="clear-formatting" title="Clear document" aria-label="Clear document"><i class="bi bi-eraser"></i></button>
+          </div>
+          <div class="markdown-toolbar-group">
+            <button type="button" class="markdown-tool-btn" data-md-action="bold" title="Bold" aria-label="Bold"><i class="bi bi-type-bold"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="strike" title="Strikethrough" aria-label="Strikethrough"><i class="bi bi-type-strikethrough"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="italic" title="Italic" aria-label="Italic"><i class="bi bi-type-italic"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="quote" title="Blockquote" aria-label="Blockquote"><i class="bi bi-quote"></i></button>
+            <button type="button" class="markdown-tool-btn text-tool" data-md-action="title-case" title="Title case" aria-label="Title case">Aa</button>
+            <button type="button" class="markdown-tool-btn text-tool" data-md-action="uppercase" title="Uppercase" aria-label="Uppercase">A</button>
+            <button type="button" class="markdown-tool-btn text-tool" data-md-action="lowercase" title="Lowercase" aria-label="Lowercase">a</button>
+          </div>
+          <div class="markdown-toolbar-group">
+            <button type="button" class="markdown-tool-btn" data-md-action="align-left" title="Align left" aria-label="Align left"><i class="bi bi-text-left"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="align-center" title="Align center" aria-label="Align center"><i class="bi bi-text-center"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="align-right" title="Align right" aria-label="Align right"><i class="bi bi-text-right"></i></button>
+            <button type="button" id="direction-toggle" class="markdown-tool-btn text-tool" title="Switch to RTL" aria-label="Toggle text direction" aria-pressed="false">L</button>
+          </div>
+          <div class="markdown-toolbar-group heading-group">
+            <button type="button" class="markdown-tool-btn text-tool" data-md-action="heading" data-md-level="1" title="Heading 1" aria-label="Heading 1">H1</button>
+            <button type="button" class="markdown-tool-btn text-tool" data-md-action="heading" data-md-level="2" title="Heading 2" aria-label="Heading 2">H2</button>
+            <button type="button" class="markdown-tool-btn text-tool" data-md-action="heading" data-md-level="3" title="Heading 3" aria-label="Heading 3">H3</button>
+            <button type="button" class="markdown-tool-btn text-tool" data-md-action="heading" data-md-level="4" title="Heading 4" aria-label="Heading 4">H4</button>
+            <button type="button" class="markdown-tool-btn text-tool" data-md-action="heading" data-md-level="5" title="Heading 5" aria-label="Heading 5">H5</button>
+            <button type="button" class="markdown-tool-btn text-tool" data-md-action="heading" data-md-level="6" title="Heading 6" aria-label="Heading 6">H6</button>
+          </div>
+          <div class="markdown-toolbar-group">
+            <button type="button" class="markdown-tool-btn" data-md-action="unordered-list" title="Bulleted list" aria-label="Bulleted list"><i class="bi bi-list-ul"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="ordered-list" title="Numbered list" aria-label="Numbered list"><i class="bi bi-list-ol"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="horizontal-rule" title="Horizontal rule" aria-label="Horizontal rule"><i class="bi bi-dash-lg"></i></button>
+          </div>
+          <div class="markdown-toolbar-group">
+            <button type="button" class="markdown-tool-btn" data-md-action="link" title="Link" aria-label="Link"><i class="bi bi-link-45deg"></i></button>
+            <button type="button" class="markdown-tool-btn text-tool" data-md-action="reference" title="Reference" aria-label="Reference"><i></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="image" title="Image" aria-label="Image"><i class="bi bi-card-image"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="inline-code" title="Inline code" aria-label="Inline code"><i class="bi bi-code-slash"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="code-block" title="Code block" aria-label="Code block"><i class="bi bi-file-code"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="terminal-block" title="Terminal block" aria-label="Terminal block"><i class="bi bi-terminal"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="table" title="Table" aria-label="Table"><i class="bi bi-table"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="date-time" title="Date and time" aria-label="Date and time"><i class="bi bi-clock"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="emoji" title="Emoji shortcode" aria-label="Emoji shortcode"><i class="bi bi-emoji-smile"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="symbols" title="Symbols &amp; HTML entities" aria-label="Symbols &amp; HTML entities"><i class="bi bi-c-circle"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="alert" title="Markdown alert" aria-label="Markdown alert"><i class="bi bi-newspaper"></i></button>
+          </div>
+          <div class="markdown-toolbar-group">
+            <button type="button" class="markdown-tool-btn" data-md-action="fullscreen" title="Fullscreen" aria-label="Fullscreen"><i class="bi bi-arrows-fullscreen"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="find" title="Find &amp; Replace" aria-label="Find &amp; Replace"><i class="bi bi-search"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="help" title="Help" aria-label="Help"><i class="bi bi-question-circle"></i></button>
+            <button type="button" class="markdown-tool-btn" data-md-action="info" title="About Markdown" aria-label="About Markdown"><i class="bi bi-info-circle"></i></button>
+          </div>
+        </div>
+
+        <!-- Reset Confirmation Modal -->
+        <div id="reset-confirm-modal" class="reset-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="reset-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--wide">
+            <p id="reset-modal-title" class="reset-modal-message">Are you sure you want to delete all files?</p>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="reset-modal-cancel">Cancel</button>
+              <button class="reset-modal-btn reset-modal-confirm" id="reset-modal-confirm">Delete All</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- Clear Markdown Formatting Modal -->
+        <div id="clear-formatting-modal" class="reset-modal-overlay modal-overlay" role="dialog" aria-modal="true" aria-labelledby="clear-formatting-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--wide modal-box">
+            <div class="modal-header">
+              <p id="clear-formatting-title" class="reset-modal-message">Clear active document?</p>
+              <button type="button" class="modal-close-btn" id="clear-formatting-close" aria-label="Close clear document dialog">
+                <i class="bi bi-x-lg"></i>
+              </button>
+            </div>
+            <p class="modal-subtext">Are you sure you want to clear the content of the active document? The document itself will not be deleted.</p>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="clear-formatting-cancel">Cancel</button>
+              <button class="reset-modal-btn reset-modal-confirm" id="clear-formatting-confirm">Clear Document</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- Find & Replace Floating Panel -->
+        <div id="find-replace-modal" class="find-replace-panel" role="region" aria-label="Find and Replace" style="display:none;">
+          <div class="find-replace-header" id="find-replace-drag-handle">
+            <span class="find-replace-title"><i class="bi bi-search me-1"></i> Find &amp; Replace</span>
+            <div class="find-replace-header-actions">
+              <button type="button" class="panel-icon-btn" id="find-replace-reset" title="Reset Position" aria-label="Reset Position"><i class="bi bi-arrow-counterclockwise"></i></button>
+              <button type="button" class="panel-icon-btn" id="find-replace-dock" title="Toggle Dock Mode" aria-label="Toggle Dock Mode"><i class="bi bi-layout-sidebar-reverse"></i></button>
+              <button type="button" class="panel-icon-btn" id="find-replace-close-icon" aria-label="Close find and replace panel"><i class="bi bi-x-lg"></i></button>
+            </div>
+          </div>
+          <div class="find-replace-body">
+            <!-- Find Row -->
+            <div class="find-replace-field-row">
+              <div class="find-input-container">
+                <input type="text" id="find-replace-input" class="find-input-field" placeholder="Find" aria-label="Find query" />
+                <div class="find-options-group">
+                  <button type="button" class="find-option-btn" id="find-case" title="Match Case (Aa)" aria-label="Match Case">Aa</button>
+                  <button type="button" class="find-option-btn" id="find-word" title="Match Whole Word (W)" aria-label="Match Whole Word">W</button>
+                  <button type="button" class="find-option-btn" id="find-regex" title="Use Regular Expression (.*)" aria-label="Use Regular Expression">.*</button>
+                  <button type="button" class="find-option-btn" id="find-sel" title="Find in Selection (Sel)" aria-label="Find in Selection"><i class="bi bi-align-start"></i></button>
+                </div>
+              </div>
+            </div>
+            
+            <!-- Real-time Regex Error Box -->
+            <div id="find-replace-error" class="find-error-drawer" style="display:none;">
+              <i class="bi bi-exclamation-triangle-fill me-1"></i> <span id="regex-error-msg"></span>
+            </div>
+
+            <!-- Replace Row -->
+            <div class="find-replace-field-row">
+              <div class="replace-input-container">
+                <input type="text" id="find-replace-with" class="find-input-field" placeholder="Replace" aria-label="Replace with" />
+                <div class="find-options-group">
+                  <button type="button" class="find-option-btn" id="replace-preserve-case" title="Preserve Case (Ab)" aria-label="Preserve Case">Ab</button>
+                  <button type="button" class="find-option-btn active" id="find-wrap" title="Wrap Around (Wrap)" aria-label="Wrap Around" aria-pressed="true">Wrap</button>
+                </div>
+              </div>
+            </div>
+
+            <!-- Meta Controls Row -->
+            <div class="find-replace-meta-row">
+              <span id="find-replace-count" class="find-match-count" role="status" aria-live="polite">0 of 0 matches</span>
+              <div class="find-nav-group">
+                <button type="button" class="find-nav-arrow-btn" id="find-prev" title="Previous match (Shift+Enter)" aria-label="Previous match" disabled>
+                  <i class="bi bi-chevron-up"></i>
+                </button>
+                <button type="button" class="find-nav-arrow-btn" id="find-next" title="Next match (Enter)" aria-label="Next match" disabled>
+                  <i class="bi bi-chevron-down"></i>
+                </button>
+              </div>
+            </div>
+
+            <!-- Advanced Drawer Toggle -->
+            <div class="find-drawer-toggle-row">
+              <button type="button" class="drawer-toggle-btn" id="fr-drawer-toggle" aria-expanded="false">
+                <i class="bi bi-chevron-right me-1"></i> Advanced Options
+              </button>
+            </div>
+
+            <!-- Collapsible Drawer -->
+            <div id="fr-drawer-content" class="find-replace-drawer-content" style="display:none;">
+              <div class="drawer-field">
+                <label for="find-replace-scope" class="drawer-label">Scope Filter</label>
+                <select id="find-replace-scope" class="drawer-select">
+                  <option value="entire">Entire Document</option>
+                  <option value="heading">Headings only</option>
+                  <option value="code">Code blocks only</option>
+                  <option value="latex">LaTeX blocks only</option>
+                  <option value="mermaid">Mermaid blocks only</option>
+                  <option value="plain">Plain text only</option>
+                </select>
+              </div>
+              <div class="drawer-field">
+                <label for="find-replace-history" class="drawer-label">Search History</label>
+                <select id="find-replace-history" class="drawer-select">
+                  <option value="">Recent queries...</option>
+                </select>
+              </div>
+              <div class="drawer-field check-field">
+                <input type="checkbox" id="find-replace-diff-toggle" class="drawer-checkbox" />
+                <label for="find-replace-diff-toggle" class="drawer-label-checkbox">Show Diff Preview before replace</label>
+              </div>
+            </div>
+          </div>
+
+          <!-- Actions Footer -->
+          <div class="find-replace-actions-footer">
+            <button type="button" class="fr-action-btn" id="find-replace-current" disabled>Replace</button>
+            <button type="button" class="fr-action-btn" id="find-replace-all" disabled>Replace All</button>
+            <button type="button" class="fr-action-btn secondary" id="find-replace-reset-footer" title="Reset Position">Reset</button>
+            <button type="button" class="fr-action-btn secondary" id="find-replace-close">Close</button>
+          </div>
+        </div>
+
+        <!-- Diff Preview Modal -->
+        <div id="find-replace-diff-modal" class="reset-modal-overlay modal-overlay" role="dialog" aria-modal="true" aria-labelledby="diff-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--xl modal-box">
+            <div class="modal-header">
+              <p id="diff-modal-title" class="reset-modal-message">Replace All Diff Preview</p>
+              <button type="button" class="modal-close-btn" id="find-replace-diff-close-icon" aria-label="Close diff preview dialog">
+                <i class="bi bi-x-lg"></i>
+              </button>
+            </div>
+            <div class="modal-body diff-preview-body">
+              <p class="modal-subtext">Review the proposed changes before committing them to the document.</p>
+              <div class="diff-container" id="find-replace-diff-container">
+                <!-- Populated dynamically -->
+              </div>
+            </div>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="find-replace-diff-cancel">Cancel</button>
+              <button class="reset-modal-btn reset-modal-confirm" id="find-replace-diff-confirm">Commit Replacements</button>
+            </div>
+          </div>
+        </div>
+
+
+        <!-- Help Modal -->
+        <div id="help-modal" class="reset-modal-overlay modal-overlay" role="dialog" aria-modal="true" aria-labelledby="help-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--xl modal-box">
+            <div class="modal-header">
+              <p id="help-modal-title" class="reset-modal-message">Markdown ++ Help</p>
+              <button type="button" class="modal-close-btn" id="help-modal-close-icon" aria-label="Close help dialog">
+                <i class="bi bi-x-lg"></i>
+              </button>
+            </div>
+            <div class="modal-body">
+              <div class="modal-section">
+                <h3 class="modal-section-title">Application shortcuts</h3>
+                <ul class="modal-list">
+                  <li>Use the view buttons in the toolbar to switch between Editor, Split, and Preview modes.</li>
+                  <li>Sync scrolling is available in Split view to keep the editor and preview aligned.</li>
+                  <li>Import, Export, Copy, Share, and Theme toggle actions are always available in the header toolbar.</li>
+                </ul>
+              </div>
+              <div class="modal-section">
+                <h3 class="modal-section-title">Toolbar descriptions</h3>
+                <ul class="modal-list">
+                  <li>Undo/Redo and Clear Formatting help you manage editing changes quickly.</li>
+                  <li>Text styling tools cover bold, italic, strikethrough, quotes, headings, and list formatting.</li>
+                  <li>Insert helpers add links, images, code blocks, tables, emojis, symbols, and alerts.</li>
+                  <li>Use Fullscreen, Find &amp; Replace, Help, and About Markdown for focused editing.</li>
+                </ul>
+              </div>
+              <div class="modal-section">
+                <h3 class="modal-section-title">Markdown tips</h3>
+                <ul class="modal-list">
+                  <li>Create headings with <code>#</code>, <code>##</code>, and <code>###</code> prefixes.</li>
+                  <li>Emphasize text with <code>**bold**</code>, <code>*italic*</code>, or <code>~~strikethrough~~</code>.</li>
+                  <li>Use <code>-</code> or <code>1.</code> to build lists, and triple backticks for code blocks.</li>
+                </ul>
+              </div>
+              <div class="modal-section">
+                <h3 class="modal-section-title">Keyboard shortcuts</h3>
+                <ul class="modal-list">
+                  <li><kbd>Ctrl</kbd>/<kbd>⌘</kbd> + <kbd>Z</kbd> &mdash; Undo</li>
+                  <li><kbd>Ctrl</kbd>/<kbd>⌘</kbd> + <kbd>Shift</kbd> + <kbd>Z</kbd> &mdash; Redo</li>
+                  <li><kbd>Ctrl</kbd>/<kbd>⌘</kbd> + <kbd>F</kbd> &mdash; Open Find &amp; Replace</li>
+                  <li><kbd>Ctrl</kbd>/<kbd>⌘</kbd> + <kbd>C</kbd>/<kbd>V</kbd> &mdash; Copy/Paste</li>
+                </ul>
+              </div>
+            </div>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="help-modal-close">Close</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- About Markdown Modal -->
+        <div id="about-modal" class="reset-modal-overlay modal-overlay" role="dialog" aria-modal="true" aria-labelledby="about-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--xl modal-box">
+            <div class="modal-header">
+              <p id="about-modal-title" class="reset-modal-message">About Markdown</p>
+              <button type="button" class="modal-close-btn" id="about-modal-close-icon" aria-label="Close about dialog">
+                <i class="bi bi-x-lg"></i>
+              </button>
+            </div>
+            <div class="modal-body">
+              <div class="about-header">
+                <img src="assets/icon.jpg" alt="Markdown ++ logo" class="about-logo" width="64" height="64" />
+                <div class="about-details">
+                  <h3 class="about-title">Markdown ++</h3>
+                  <p class="about-description">A GitHub-style Markdown editor with live preview, diagrams, math, syntax highlighting, and export tools.</p>
+                  <p class="about-meta">Version <span id="about-version"></span> &bull; Apache License 2.0 &bull; Open source</p>
+                </div>
+              </div>
+              <div class="modal-section">
+                <h3 class="modal-section-title">Open-source links</h3>
+                <ul class="modal-list">
+                  <li><a href="https://git.4parv.in/parv.ashwani/markdownplusplus/blob/main/LICENSE" target="_blank" rel="noopener noreferrer">Apache License 2.0</a></li>
+                  <li><a href="https://git.4parv.in/parv.ashwani/markdownplusplus" target="_blank" rel="noopener noreferrer">GitHub Repository</a></li>
+                  <li><a href="https://git.4parv.in/parv.ashwani/markdownplusplus/wiki/Contributing" target="_blank" rel="noopener noreferrer">Contribution Guide</a></li>
+                  <li><a href="https://git.4parv.in/parv.ashwani/markdownplusplus/issues/new/choose" target="_blank" rel="noopener noreferrer">Report an Issue</a></li>
+                  <li><a href="https://git.4parv.in/parv.ashwani/markdownplusplus/discussions" target="_blank" rel="noopener noreferrer">Community &amp; Support</a></li>
+                </ul>
+                <p class="modal-subtext">For feature requests, open a GitHub issue or start a discussion so the community can weigh in.</p>
+              </div>
+              <div class="modal-section">
+                <h3 class="modal-section-title">Markdown resources</h3>
+                <ul class="modal-list">
+                  <li><a href="https://git.4parv.in/parv.ashwani/markdownplusplus/wiki/Markdown-Reference" target="_blank" rel="noopener noreferrer">Markdown syntax reference</a></li>
+                </ul>
+              </div>
+              <div class="modal-section">
+                <h3 class="modal-section-title">Technology stack</h3>
+                <ul class="modal-list">
+                  <li>HTML, CSS, and JavaScript with Bootstrap 5 for layout and styling.</li>
+                  <li>Marked, DOMPurify, Highlight.js, Mermaid, MathJax, and Emoji Toolkit for rendering.</li>
+                </ul>
+              </div>
+              <div class="modal-section">
+                <h3 class="modal-section-title">Open-source credits</h3>
+                <p class="modal-subtext">Markdown ++ is built on top of open-source libraries maintained by the community. Thank you to every contributor and maintainer.</p>
+              </div>
+            </div>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="about-modal-close">Close</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- Share Modal -->
+        <div id="share-modal" class="reset-modal-overlay modal-overlay" role="dialog" aria-modal="true" aria-labelledby="share-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--wide modal-box">
+            <div class="modal-header">
+              <p id="share-modal-title" class="reset-modal-message">Share Document</p>
+              <button type="button" class="modal-close-btn" id="share-modal-close-icon" aria-label="Close share dialog">
+                <i class="bi bi-x-lg"></i>
+              </button>
+            </div>
+            <div class="modal-body">
+              <p class="share-modal-description">Choose how recipients can interact with this document.</p>
+              <div class="share-mode-cards">
+                <label class="share-mode-card" id="share-card-view" for="share-mode-view">
+                  <input type="radio" id="share-mode-view" name="share-mode" value="view" checked />
+                  <span class="share-card-icon"><i class="bi bi-eye"></i></span>
+                  <span class="share-card-body">
+                    <span class="share-card-title">View only</span>
+                    <span class="share-card-desc">Opens in preview mode. The editor is hidden.</span>
+                  </span>
+                  <span class="share-card-check"><i class="bi bi-check-lg"></i></span>
+                </label>
+                <label class="share-mode-card" id="share-card-edit" for="share-mode-edit">
+                  <input type="radio" id="share-mode-edit" name="share-mode" value="edit" />
+                  <span class="share-card-icon"><i class="bi bi-pencil-square"></i></span>
+                  <span class="share-card-body">
+                    <span class="share-card-title">Edit</span>
+                    <span class="share-card-desc">Opens in split editor + preview mode.</span>
+                  </span>
+                  <span class="share-card-check"><i class="bi bi-check-lg"></i></span>
+                </label>
+              </div>
+              <div class="share-url-row">
+                <input type="text" id="share-url-input" class="rename-modal-input share-url-input" readonly placeholder="Generating link…" aria-label="Share URL" />
+                <button class="reset-modal-btn share-copy-btn" id="share-copy-btn" title="Copy link">
+                  <i class="bi bi-clipboard"></i>
+                </button>
+              </div>
+              <p class="share-modal-notice"><i class="bi bi-info-circle"></i> The entire document is encoded in the URL. No data is sent to any server.</p>
+            </div>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="share-modal-close">Close</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- Rename Modal -->
+        <div id="rename-modal" class="reset-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="rename-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--wide">
+            <p id="rename-modal-title" class="reset-modal-message">Rename file</p>
+            <input type="text" id="rename-modal-input" class="rename-modal-input" placeholder="File name" />
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="rename-modal-cancel">Cancel</button>
+              <button class="reset-modal-btn reset-modal-confirm" id="rename-modal-confirm">Rename</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- Link Modal -->
+        <div id="link-modal" class="reset-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="link-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--wide">
+            <p id="link-modal-title" class="reset-modal-message">Insert link</p>
+            <div class="reset-modal-field">
+              <label class="reset-modal-label" for="link-modal-url">Address / URL</label>
+              <input type="url" id="link-modal-url" class="rename-modal-input" value="https://" />
+            </div>
+            <div class="reset-modal-field">
+              <label class="reset-modal-label" for="link-modal-text">Link Text</label>
+              <input type="text" id="link-modal-text" class="rename-modal-input" placeholder="Link title" />
+            </div>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="link-modal-cancel">Cancel</button>
+              <button class="reset-modal-btn" id="link-modal-apply">Apply</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- Reference Modal -->
+        <div id="reference-modal" class="reset-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="reference-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--wide">
+            <p id="reference-modal-title" class="reset-modal-message">Insert reference</p>
+            <div class="reset-modal-field">
+              <label class="reset-modal-label" for="reference-modal-number">Reference Number</label>
+              <input type="text" id="reference-modal-number" class="rename-modal-input" value="[1]" />
+            </div>
+            <div class="reset-modal-field">
+              <label class="reset-modal-label" for="reference-modal-url">Address / Link</label>
+              <input type="url" id="reference-modal-url" class="rename-modal-input" value="https://" />
+            </div>
+            <div class="reset-modal-field">
+              <label class="reset-modal-label" for="reference-modal-title-input">Title</label>
+              <input type="text" id="reference-modal-title-input" class="rename-modal-input" placeholder="Reference title" />
+            </div>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="reference-modal-cancel">Cancel</button>
+              <button class="reset-modal-btn" id="reference-modal-apply">Insert</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- Image Modal -->
+        <div id="image-modal" class="reset-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="image-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--wide">
+            <p id="image-modal-title" class="reset-modal-message">Insert image</p>
+            <div class="reset-modal-toggle-group">
+              <label class="reset-modal-option">
+                <input type="radio" name="image-source" id="image-source-upload" value="upload">
+                Upload from Device
+              </label>
+              <label class="reset-modal-option">
+                <input type="radio" name="image-source" id="image-source-url" value="url" checked>
+                External Image (URL)
+              </label>
+            </div>
+            <div id="image-upload-fields" class="reset-modal-field-group" style="display:none;">
+              <input type="file" id="image-modal-file" class="rename-modal-input" accept="image/*" />
+            </div>
+            <div id="image-url-fields" class="reset-modal-field-group">
+              <div class="reset-modal-field">
+                <label class="reset-modal-label" for="image-modal-url">Image URL</label>
+                <input type="url" id="image-modal-url" class="rename-modal-input" value="https://" />
+              </div>
+            </div>
+            <div class="reset-modal-field">
+              <label class="reset-modal-label" for="image-modal-alt">Alt Text (used for title)</label>
+              <input type="text" id="image-modal-alt" class="rename-modal-input" placeholder="Image description" />
+            </div>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="image-modal-cancel">Cancel</button>
+              <button class="reset-modal-btn" id="image-modal-insert">Insert</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- Table Modal -->
+        <div id="table-modal" class="reset-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="table-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--wide">
+            <p id="table-modal-title" class="reset-modal-message">Insert table</p>
+            <div class="reset-modal-field">
+              <label class="reset-modal-label" for="table-modal-columns">Column count</label>
+              <input type="number" id="table-modal-columns" class="rename-modal-input" min="1" max="20" value="3" />
+            </div>
+            <div class="reset-modal-field">
+              <label class="reset-modal-label" for="table-modal-rows">Row count</label>
+              <input type="number" id="table-modal-rows" class="rename-modal-input" min="1" max="20" value="1" />
+            </div>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="table-modal-cancel">Cancel</button>
+              <button class="reset-modal-btn" id="table-modal-insert">Insert</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- Emoji Modal -->
+        <div id="emoji-modal" class="reset-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="emoji-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--wide reset-modal-box--xl">
+            <p id="emoji-modal-title" class="reset-modal-message">GitHub Emojis</p>
+            <div class="reset-modal-field">
+              <label class="reset-modal-label" for="emoji-modal-search">Search</label>
+              <input type="text" id="emoji-modal-search" class="rename-modal-input" placeholder="Search emojis" />
+            </div>
+            <div id="emoji-modal-grid" class="emoji-grid" role="listbox" aria-label="Emoji list"></div>
+            <p id="emoji-modal-empty" class="modal-empty" style="display:none;">No emojis found.</p>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="emoji-modal-cancel">Cancel</button>
+              <button class="reset-modal-btn" id="emoji-modal-insert">Insert</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- Symbols Modal -->
+        <div id="symbols-modal" class="reset-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="symbols-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--wide reset-modal-box--xl">
+            <p id="symbols-modal-title" class="reset-modal-message">Symbols &amp; HTML Entities</p>
+            <div class="reset-modal-field">
+              <label class="reset-modal-label" for="symbols-modal-search">Search</label>
+              <input type="text" id="symbols-modal-search" class="rename-modal-input" placeholder="Search symbols" />
+            </div>
+            <div id="symbols-modal-grid" class="symbol-grid" role="listbox" aria-label="Symbol list"></div>
+            <p id="symbols-modal-empty" class="modal-empty" style="display:none;">No symbols found.</p>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="symbols-modal-cancel">Cancel</button>
+              <button class="reset-modal-btn" id="symbols-modal-insert">Insert</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- Markdown Alert Modal -->
+        <div id="alert-modal" class="reset-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="alert-modal-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box reset-modal-box--wide">
+            <p id="alert-modal-title" class="reset-modal-message">Markdown alerts</p>
+            <div id="alert-modal-grid" class="alert-grid" role="listbox" aria-label="Markdown alert types"></div>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="alert-modal-cancel">Cancel</button>
+              <button class="reset-modal-btn" id="alert-modal-insert">Insert</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- GitHub Import Modal -->
+        <div id="github-import-modal" class="reset-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="github-import-title" aria-hidden="true" style="display:none;">
+          <div class="reset-modal-box">
+            <p id="github-import-title" class="reset-modal-message">Import Markdown from GitHub</p>
+            <input type="url" id="github-import-url" class="rename-modal-input" placeholder="https://github.com/owner/repo/blob/main/README.md" />
+            <select id="github-import-file-select" class="rename-modal-input" style="display:none;"></select>
+            <div id="github-import-selection-toolbar" class="github-import-selection-toolbar" style="display:none;">
+              <span id="github-import-selected-count" class="github-import-selected-count">0 selected</span>
+              <button type="button" class="reset-modal-btn" id="github-import-select-all">Select All</button>
+            </div>
+            <div id="github-import-tree" class="github-import-tree" style="display:none;"></div>
+            <p id="github-import-error" class="github-import-error" style="display:none;"></p>
+            <div class="reset-modal-actions">
+              <button class="reset-modal-btn reset-modal-cancel" id="github-import-cancel">Cancel</button>
+              <button class="reset-modal-btn" id="github-import-submit">Import</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- Full-screen drag overlay (shown when a file is dragged over the window) -->
+        <div id="drag-overlay" class="drag-overlay" aria-hidden="true">
+            <div class="drag-overlay-inner">
+                <i class="bi bi-cloud-arrow-up drag-overlay-icon"></i>
+                <p class="drag-overlay-text">Drop your Markdown file anywhere</p>
+                <p class="drag-overlay-sub">.md or .markdown files supported</p>
+            </div>
+        </div>
+
+        <main class="content-container">
+            <div class="editor-pane is-loading">
+                <div id="line-numbers" class="line-numbers" aria-hidden="true" inert></div>
+                <div id="editor-highlight-layer" class="editor-highlight-layer" aria-hidden="true" tabindex="-1" inert></div>
+                <div class="editor-skeleton" id="editor-skeleton" aria-hidden="true">
+                    <div class="skeleton-placeholder skeleton-title"></div>
+                    <div class="skeleton-placeholder skeleton-line skeleton-w90"></div>
+                    <div class="skeleton-placeholder skeleton-line skeleton-w85"></div>
+                    <div class="skeleton-placeholder skeleton-line skeleton-w60"></div>
+                    
+                    <div class="skeleton-placeholder skeleton-subtitle"></div>
+                    <div class="skeleton-placeholder skeleton-line skeleton-w88"></div>
+                    <div class="skeleton-placeholder skeleton-line skeleton-w92"></div>
+                    <div class="skeleton-placeholder skeleton-line skeleton-w45"></div>
+                </div>
+                <textarea id="markdown-editor" placeholder="Type or paste your Markdown here..."></textarea>
+                <div class="drop-hint" aria-hidden="true">
+                    <i class="bi bi-cloud-arrow-up me-1"></i>Drop a .md file anywhere to open it
+                </div>
+            </div>
+            <!-- Resize Divider - Story 1.3 -->
+            <div class="resize-divider" role="separator" aria-orientation="vertical" aria-label="Resize panes" tabindex="0">
+                <div class="resize-divider-handle"></div>
+            </div>
+            <div class="preview-pane">
+                <div id="markdown-preview" class="markdown-body">
+                    <!-- Initial dynamic article skeleton loader -->
+                    <div class="skeleton-preview-container" id="markdown-preview-skeleton" aria-hidden="true">
+                        <div class="skeleton-placeholder skeleton-title"></div>
+                        <div class="skeleton-placeholder skeleton-line skeleton-w90"></div>
+                        <div class="skeleton-placeholder skeleton-line skeleton-w85"></div>
+                        <div class="skeleton-placeholder skeleton-line skeleton-w60"></div>
+                        
+                        <div class="skeleton-placeholder skeleton-subtitle"></div>
+                        <div class="skeleton-placeholder skeleton-line skeleton-w88"></div>
+                        <div class="skeleton-placeholder skeleton-line skeleton-w92"></div>
+                        <div class="skeleton-placeholder skeleton-line skeleton-w45"></div>
+                    </div>
+                </div>
+            </div>
+        </main>
+    </div>
+
+    <!-- Mermaid Zoom Modal -->
+    <div id="mermaid-zoom-modal" role="dialog" aria-modal="true" aria-label="Diagram zoom view" aria-hidden="true">
+        <div class="mermaid-modal-content">
+            <div class="mermaid-modal-header">
+                <span>Diagram</span>
+                <button class="mermaid-modal-close" id="mermaid-modal-close" title="Close" aria-label="Close diagram modal">
+                    <i class="bi bi-x-lg"></i>
+                </button>
+            </div>
+            <div class="mermaid-modal-diagram" id="mermaid-modal-diagram"></div>
+            <div class="mermaid-modal-controls">
+                <button class="mermaid-toolbar-btn" id="mermaid-modal-zoom-in" title="Zoom in">
+                    <i class="bi bi-zoom-in"></i> Zoom In
+                </button>
+                <button class="mermaid-toolbar-btn" id="mermaid-modal-zoom-out" title="Zoom out">
+                    <i class="bi bi-zoom-out"></i> Zoom Out
+                </button>
+                <button class="mermaid-toolbar-btn" id="mermaid-modal-zoom-reset" title="Reset zoom">
+                    <i class="bi bi-arrows-angle-contract"></i> Reset
+                </button>
+                <button class="mermaid-toolbar-btn" id="mermaid-modal-copy" title="Copy image">
+                    <i class="bi bi-clipboard-image"></i> Copy
+                </button>
+                <button class="mermaid-toolbar-btn" id="mermaid-modal-download-png" title="Download PNG">
+                    <i class="bi bi-file-image"></i> PNG
+                </button>
+                <button class="mermaid-toolbar-btn" id="mermaid-modal-download-svg" title="Download SVG">
+                    <i class="bi bi-filetype-svg"></i> SVG
+                </button>
+            </div>
+        </div>
+    </div>
+
+    <script type="text/markdown" id="default-markdown">---
+title: Welcome to Markdown Plus Plus
+description: A GitHub-style Markdown renderer with live preview, math, diagrams, and export support.
+author: Parv Ashwani
+tags: ["markdown", "preview", "mermaid", "latex", "open-source"]
+---
+
+# Welcome to Markdown ++
+
+## ✨ Key Features
+- **Live Preview** with GitHub styling
+- **Smart Import/Export** (MD, HTML, PDF)
+- **Mermaid Diagrams** for visual documentation
+- **LaTeX Math Support** for scientific notation
+- **Emoji Support** 😄 👍 🎉
+
+## 💻 Code with Syntax Highlighting
+```javascript
+  function renderMarkdown() {
+    const markdown = markdownEditor.value;
+    const html = marked.parse(markdown);
+    const sanitizedHtml = DOMPurify.sanitize(html);
+    markdownPreview.innerHTML = sanitizedHtml;
+    
+    // Syntax highlighting is handled automatically
+    // during the parsing phase by the marked renderer.
+    // Themes are applied instantly via CSS variables.
+  }
+```
+
+## 🧮 Mathematical Expressions
+Write complex formulas with LaTeX syntax:
+
+Inline equation: $$E = mc^2$$
+
+Display equations:
+$$\frac{\partial f}{\partial x} = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}$$
+
+$$\sum_{i=1}^{n} i^2 = \frac{n(n+1)(2n+1)}{6}$$
+
+## 📊 Mermaid Diagrams
+Create powerful visualizations directly in markdown:
+
+```mermaid
+flowchart LR
+    A[Start] --> B{Is it working?}
+    B -->|Yes| C[Great!]
+    B -->|No| D[Debug]
+    C --> E[Deploy]
+    D --> B
+```
+
+### Sequence Diagram Example
+```mermaid
+sequenceDiagram
+    User->>Editor: Type markdown
+    Editor->>Preview: Render content
+    User->>Editor: Make changes
+    Editor->>Preview: Update rendering
+    User->>Export: Save as PDF
+```
+
+## 📋 Task Management
+- [x] Create responsive layout
+- [x] Implement live preview with GitHub styling
+- [x] Add syntax highlighting for code blocks
+- [x] Support math expressions with LaTeX
+- [x] Enable mermaid diagrams
+
+## 🆚 Feature Comparison
+
+| Feature                  | Markdown ++ (Ours) | Other Markdown Editors  |
+|:-------------------------|:----------------------:|:-----------------------:|
+| Live Preview             | ✅ GitHub-Styled       | ✅                     |
+| Sync Scrolling           | ✅ Two-way             | 🔄 Partial/None        |
+| Mermaid Support          | ✅                     | ❌/Limited             |
+| LaTeX Math Rendering     | ✅                     | ❌/Limited             |
+
+### 📝 Multi-row Headers Support
+
+<table>
+  <thead>
+    <tr>
+      <th rowspan="2">Document Type</th>
+      <th colspan="2">Support</th>
+    </tr>
+    <tr>
+      <th>Markdown ++ (Ours)</th>
+      <th>Other Markdown Editors</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td>Technical Docs</td>
+      <td>Full + Diagrams</td>
+      <td>Limited/Basic</td>
+    </tr>
+    <tr>
+      <td>Research Notes</td>
+      <td>Full + Math</td>
+      <td>Partial</td>
+    </tr>
+    <tr>
+      <td>Developer Guides</td>
+      <td>Full + Export Options</td>
+      <td>Basic</td>
+    </tr>
+  </tbody>
+</table>
+
+## 📝 Text Formatting Examples
+
+### Text Formatting
+
+Text can be formatted in various ways for ~~strikethrough~~, **bold**, *italic*, or ***bold italic***.
+
+For highlighting important information, use <mark>highlighted text</mark> or add <u>underlines</u> where appropriate.
+
+### Superscript and Subscript
+
+Chemical formulas: H<sub>2</sub>O, CO<sub>2</sub>  
+Mathematical notation: x<sup>2</sup>, e<sup>iπ</sup>
+
+### Keyboard Keys
+
+Press <kbd>Ctrl</kbd> + <kbd>B</kbd> for bold text.
+
+### Abbreviations
+
+<abbr title="Graphical User Interface">GUI</abbr>  
+<abbr title="Application Programming Interface">API</abbr>
+
+### Text Alignment
+
+<div style="text-align: center">
+Centered text for headings or important notices
+</div>
+
+<div style="text-align: right">
+Right-aligned text (for dates, signatures, etc.)
+</div>
+
+### **Lists**
+
+Create bullet points:
+* Item 1
+* Item 2
+  * Nested item
+    * Nested further
+
+### **Links and Images**
+
+Add a [link](https://git.4parv.in/parv.ashwani/markdownplusplus) to important resources.
+
+Embed an image:
+<img alt="Markdown Logo" src="https://markdownplusplus.pages.dev/assets/icon.jpg" width="120" height="120">
+
+### **Blockquotes**
+
+Quote someone famous:
+> "The best way to predict the future is to invent it." - Alan Kay
+
+---
+
+## 🛡️ Security Note
+
+This is a fully client-side application. Your content never leaves your browser and stays secure on your device.
+</script>
+
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
+    <script src="script.js"></script>
+    <!-- Screen reader dynamic accessibility announcer -->
+    <div id="app-accessibility-announcer" class="visually-hidden" aria-live="polite" aria-atomic="true"></div>
+</body>
+</html>

+ 42 - 0
manifest.json

@@ -0,0 +1,42 @@
+{
+  "name": "Markdown Viewer",
+  "short_name": "Markdown Viewer",
+  "description": "A premium client-side GitHub-style Markdown editor and live preview tool.",
+  "start_url": "./index.html?utm_source=pwa",
+  "display": "standalone",
+  "background_color": "#0d1117",
+  "theme_color": "#0d1117",
+  "orientation": "any",
+  "icons": [
+    {
+      "src": "assets/icon.jpg",
+      "sizes": "512x512",
+      "type": "image/jpeg",
+      "purpose": "any"
+    },
+    {
+      "src": "assets/icon.jpg",
+      "sizes": "192x192",
+      "type": "image/jpeg",
+      "purpose": "any"
+    },
+    {
+      "src": "assets/icon.jpg",
+      "sizes": "256x256",
+      "type": "image/jpeg",
+      "purpose": "any"
+    },
+    {
+      "src": "assets/icon.jpg",
+      "sizes": "384x384",
+      "type": "image/jpeg",
+      "purpose": "any"
+    },
+    {
+      "src": "assets/icon.jpg",
+      "sizes": "512x512",
+      "type": "image/jpeg",
+      "purpose": "maskable"
+    }
+  ]
+}

+ 497 - 0
preview-worker.js

@@ -0,0 +1,497 @@
+/* global importScripts, marked, hljs */
+
+let librariesLoaded = false;
+let markedConfigured = false;
+let mermaidIdCounter = 0;
+
+const markedOptions = {
+  gfm: true,
+  breaks: true,
+  pedantic: false,
+  sanitize: false,
+  smartypants: false,
+  xhtml: false,
+  headerIds: true,
+  mangle: false,
+};
+
+const BLOCK_MATH_MARKER_PATTERN = /^\$\$/m;
+const BLOCK_MATH_PATTERN = /^\$\$[ \t]*\n?([\s\S]*?)\n?\$\$[ \t]*(?:\n|$)/;
+const DEFINITION_LIST_ITEM_PATTERN = /^:[ \t]+(.*)$/;
+const SUPERSCRIPT_PATTERN = /^\^(?!\s)([^^\n]*?\S)\^(?!\^)/;
+const SUBSCRIPT_PATTERN = /^~(?!~)(?!\s)([^~\n]*?\S)~(?!~)/;
+const HIGHLIGHT_PATTERN = /^==(?=\S)([\s\S]*?\S)==/;
+const MARKDOWN_LIST_MARKER_PATTERN = /^(\s*)(?:[-*+]\s+|\d+\.\s+|>\s+)/;
+const EMPTY_LINE_PATTERN = /^\s*$/;
+
+let suppressFootnotePreprocess = false;
+const footnoteDefinitions = new Map();
+const footnoteOrder = [];
+const footnoteRefCounts = new Map();
+const footnoteFirstRefId = new Map();
+let anonymousFootnoteCounter = 0;
+
+function escapeHtml(str) {
+  return String(str)
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;");
+}
+
+function escapeHtmlAttribute(value) {
+  return String(value)
+    .replace(/&/g, "&amp;")
+    .replace(/"/g, "&quot;")
+    .replace(/'/g, "&#39;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;");
+}
+
+function resetExtendedMarkdownState() {
+  footnoteDefinitions.clear();
+  footnoteOrder.length = 0;
+  footnoteRefCounts.clear();
+  footnoteFirstRefId.clear();
+  anonymousFootnoteCounter = 0;
+}
+
+function normalizeFootnoteId(id) {
+  const normalized = String(id || "")
+    .trim()
+    .toLowerCase()
+    .replace(/[^a-z0-9_-]+/g, "-")
+    .replace(/^-+|-+$/g, "");
+  if (normalized) return normalized;
+  anonymousFootnoteCounter += 1;
+  return `footnote-${anonymousFootnoteCounter}`;
+}
+
+function parseInlineWithoutFootnotes(text) {
+  suppressFootnotePreprocess = true;
+  try {
+    return marked.parseInline(text);
+  } finally {
+    suppressFootnotePreprocess = false;
+  }
+}
+
+function renderDefinitionContent(content, options) {
+  const appendHtml = options && options.appendHtml ? options.appendHtml : "";
+  const paragraphs = String(content || "")
+    .split(/\n(?:[ \t]*\n)+/)
+    .map((paragraph) => paragraph.trim())
+    .filter(Boolean);
+
+  if (appendHtml) {
+    if (paragraphs.length === 0) {
+      paragraphs.push(appendHtml);
+    } else {
+      paragraphs[paragraphs.length - 1] = `${paragraphs[paragraphs.length - 1]} ${appendHtml}`;
+    }
+  }
+
+  return paragraphs
+    .map((paragraph) => `<p>${parseInlineWithoutFootnotes(paragraph)}</p>`)
+    .join("");
+}
+
+function extractFootnoteDefinitions(markdown) {
+  const lines = markdown.split("\n");
+  const preservedLines = [];
+  let index = 0;
+
+  while (index < lines.length) {
+    const match = /^([ \t]{0,3})\[\^([^\]\n]+)\]:[ \t]*(.*)$/.exec(lines[index]);
+    if (!match) {
+      preservedLines.push(lines[index]);
+      index += 1;
+      continue;
+    }
+
+    const baseIndent = match[1] || "";
+    const id = match[2].trim();
+    const definitionLines = [match[3] || ""];
+    index += 1;
+
+    while (index < lines.length) {
+      const line = lines[index];
+      if (!line.startsWith(baseIndent)) break;
+      const lineAfterBase = line.slice(baseIndent.length);
+      const indentedMatch = /^(?: {2,}|\t)(.*)$/.exec(lineAfterBase);
+      if (indentedMatch) {
+        definitionLines.push(indentedMatch[1]);
+        index += 1;
+        continue;
+      }
+      if (lineAfterBase.trim() === "") {
+        const nextLine = lines[index + 1] || "";
+        const nextAfterBase = nextLine.startsWith(baseIndent) ? nextLine.slice(baseIndent.length) : "";
+        if (/^(?: {2,}|\t)/.test(nextAfterBase)) {
+          definitionLines.push("");
+          index += 1;
+          continue;
+        }
+      }
+      break;
+    }
+
+    footnoteDefinitions.set(id, definitionLines.join("\n").trim());
+  }
+
+  return preservedLines.join("\n");
+}
+
+function applyFootnotes(markdown) {
+  const markdownWithReferences = markdown.replace(/\[\^([^\]\n]+)\]/g, function(match, idText) {
+    const id = idText.trim();
+    if (!id) return match;
+    if (!footnoteOrder.includes(id)) footnoteOrder.push(id);
+
+    const refCount = (footnoteRefCounts.get(id) || 0) + 1;
+    footnoteRefCounts.set(id, refCount);
+
+    const normalizedId = normalizeFootnoteId(id);
+    const refId = `fnref-${normalizedId}${refCount > 1 ? `-${refCount}` : ""}`;
+    if (!footnoteFirstRefId.has(id)) footnoteFirstRefId.set(id, refId);
+
+    const noteNumber = footnoteOrder.indexOf(id) + 1;
+    return `<sup id="${escapeHtmlAttribute(refId)}" class="footnote-ref"><a href="#fn-${escapeHtmlAttribute(normalizedId)}" aria-label="Footnote ${noteNumber}">[${noteNumber}]</a></sup>`;
+  });
+
+  const footnotesHtml = footnoteOrder
+    .filter((id) => footnoteDefinitions.has(id))
+    .map((id) => {
+      const normalizedId = normalizeFootnoteId(id);
+      const backRefId = footnoteFirstRefId.get(id) || `fnref-${normalizedId}`;
+      const backRefHtml = `<a href="#${escapeHtmlAttribute(backRefId)}" class="footnote-backref" aria-label="Back to content">&#8592;</a>`;
+      const noteHtml = renderDefinitionContent(footnoteDefinitions.get(id) || "", { appendHtml: backRefHtml });
+      return `<li id="fn-${escapeHtmlAttribute(normalizedId)}">${noteHtml}</li>`;
+    })
+    .join("");
+
+  if (!footnotesHtml) return markdownWithReferences;
+  return `${markdownWithReferences}\n\n<section class="footnotes"><hr><ol>${footnotesHtml}</ol></section>`;
+}
+
+function configureMarked() {
+  if (markedConfigured) return;
+
+  const renderer = new marked.Renderer();
+  const blockMathExtension = {
+    name: "blockMath",
+    level: "block",
+    start(src) {
+      const match = src.match(BLOCK_MATH_MARKER_PATTERN);
+      return match ? match.index : undefined;
+    },
+    tokenizer(src) {
+      const match = BLOCK_MATH_PATTERN.exec(src);
+      if (!match) return undefined;
+      return { type: "blockMath", raw: match[0], text: match[1] };
+    },
+    renderer(token) {
+      return `<div class="math-block">$$\n${token.text}\n$$</div>\n`;
+    },
+  };
+
+  const definitionListExtension = {
+    name: "definitionList",
+    level: "block",
+    start(src) {
+      const match = src.match(/\n:[ \t]+/);
+      return match ? match.index + 1 : undefined;
+    },
+    tokenizer(src) {
+      const lines = src.split("\n");
+      if (lines.length < 2) return undefined;
+
+      const term = lines[0];
+      if (EMPTY_LINE_PATTERN.test(term) || MARKDOWN_LIST_MARKER_PATTERN.test(term)) return undefined;
+      if (!DEFINITION_LIST_ITEM_PATTERN.test(lines[1])) return undefined;
+
+      const definitions = [];
+      const rawLines = [term];
+      let index = 1;
+      while (index < lines.length) {
+        const itemMatch = DEFINITION_LIST_ITEM_PATTERN.exec(lines[index]);
+        if (!itemMatch) break;
+
+        rawLines.push(lines[index]);
+        const definitionLines = [itemMatch[1]];
+        index += 1;
+
+        while (index < lines.length) {
+          const line = lines[index];
+          if (DEFINITION_LIST_ITEM_PATTERN.test(line)) break;
+          if (EMPTY_LINE_PATTERN.test(line)) {
+            const nextLine = lines[index + 1] || "";
+            if (/^(?: {2,}|\t)/.test(nextLine)) {
+              rawLines.push(line);
+              definitionLines.push("");
+              index += 1;
+              continue;
+            }
+            break;
+          }
+          const continuationMatch = /^(?: {2,}|\t)(.*)$/.exec(line);
+          if (!continuationMatch) break;
+          rawLines.push(line);
+          definitionLines.push(continuationMatch[1]);
+          index += 1;
+        }
+
+        definitions.push(definitionLines.join("\n").trim());
+      }
+
+      if (definitions.length === 0) return undefined;
+      let raw = rawLines.join("\n");
+      if (src.startsWith(raw + "\n")) raw += "\n";
+      return { type: "definitionList", raw, term: term.trim(), definitions };
+    },
+    renderer(token) {
+      const termHtml = parseInlineWithoutFootnotes(token.term);
+      const definitionHtml = token.definitions
+        .map((definition) => `<dd>${renderDefinitionContent(definition)}</dd>`)
+        .join("");
+      return `<dl><dt>${termHtml}</dt>${definitionHtml}</dl>\n`;
+    },
+  };
+
+  const superscriptExtension = {
+    name: "superscript",
+    level: "inline",
+    start(src) {
+      const index = src.indexOf("^");
+      return index >= 0 ? index : undefined;
+    },
+    tokenizer(src) {
+      const match = SUPERSCRIPT_PATTERN.exec(src);
+      return match ? { type: "superscript", raw: match[0], text: match[1] } : undefined;
+    },
+    renderer(token) {
+      return `<sup>${marked.parseInline(token.text)}</sup>`;
+    },
+  };
+
+  const subscriptExtension = {
+    name: "subscript",
+    level: "inline",
+    start(src) {
+      const index = src.indexOf("~");
+      return index >= 0 ? index : undefined;
+    },
+    tokenizer(src) {
+      const match = SUBSCRIPT_PATTERN.exec(src);
+      return match ? { type: "subscript", raw: match[0], text: match[1] } : undefined;
+    },
+    renderer(token) {
+      return `<sub>${marked.parseInline(token.text)}</sub>`;
+    },
+  };
+
+  const highlightExtension = {
+    name: "highlight",
+    level: "inline",
+    start(src) {
+      const index = src.indexOf("==");
+      return index >= 0 ? index : undefined;
+    },
+    tokenizer(src) {
+      const match = HIGHLIGHT_PATTERN.exec(src);
+      return match ? { type: "highlight", raw: match[0], text: match[1] } : undefined;
+    },
+    renderer(token) {
+      return `<mark>${marked.parseInline(token.text)}</mark>`;
+    },
+  };
+
+  renderer.code = function(code, language) {
+    if (language === "mermaid") {
+      const uniqueId = `mermaid-diagram-worker-${mermaidIdCounter++}`;
+      return `<div class="mermaid-container is-loading"><div class="mermaid" id="${uniqueId}" data-original-code="${encodeURIComponent(code)}">${escapeHtml(code)}</div></div>`;
+    }
+
+    const validLanguage = hljs && hljs.getLanguage(language) ? language : "plaintext";
+    const highlightedCode = hljs
+      ? hljs.highlight(code, { language: validLanguage }).value
+      : escapeHtml(code);
+    return `<pre><code class="hljs ${escapeHtmlAttribute(validLanguage)}">${highlightedCode}</code></pre>`;
+  };
+
+  renderer.heading = function(text, level, raw) {
+    let id = raw
+      .toLowerCase()
+      .trim()
+      .replace(/<[^>]*>/g, '')
+      .replace(/\s+/g, '-')
+      .replace(/[^\w-]/g, '')
+      .replace(/-+/g, '-');
+    if (!id) {
+      id = `heading-worker-${Math.random().toString(36).substr(2, 9)}`;
+    }
+    return `<h${level} id="${id}">${text}</h${level}>`;
+  };
+
+  marked.use({
+    extensions: [
+      blockMathExtension,
+      definitionListExtension,
+      superscriptExtension,
+      subscriptExtension,
+      highlightExtension,
+    ],
+    hooks: {
+      preprocess(markdown) {
+        if (suppressFootnotePreprocess) return markdown;
+        resetExtendedMarkdownState();
+        const protectedMarkdown = markdown.replace(/\\\$/g, "&#36;");
+        return applyFootnotes(extractFootnoteDefinitions(protectedMarkdown));
+      },
+    },
+  });
+
+  marked.setOptions(Object.assign({}, markedOptions, { renderer }));
+  markedConfigured = true;
+}
+
+function ensureLibraries(urls) {
+  if (!librariesLoaded) {
+    importScripts(urls.marked, urls.highlight);
+    librariesLoaded = true;
+  }
+  configureMarked();
+}
+
+function isSegmentedPreviewSafe(markdown) {
+  if (/^\s*---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/.test(markdown)) return false;
+  if (/^\[[^\]\n]+\]:\s+\S+/m.test(markdown)) return false;
+  if (/\[\^[^\]\n]+\]/.test(markdown)) return false;
+  if (/\n:[ \t]+/.test(markdown)) return false;
+  if (/^\s{0,3}<\/?[a-zA-Z][\w:-]*(?:\s|>|\/>)/m.test(markdown)) return false;
+  return true;
+}
+
+function hashString(value) {
+  let hash = 2166136261;
+  for (let i = 0; i < value.length; i += 1) {
+    hash ^= value.charCodeAt(i);
+    hash = Math.imul(hash, 16777619);
+  }
+  return (hash >>> 0).toString(36);
+}
+
+function splitMarkdownBlocks(markdown) {
+  const normalized = String(markdown || "").replace(/\r\n/g, "\n");
+  const lines = normalized.split("\n");
+  const blocks = [];
+  let buffer = [];
+  let startLine = 1;
+  let inFence = false;
+  let fenceChar = "";
+  let fenceLength = 0;
+  let inMathBlock = false;
+
+  function flush(endLine) {
+    const source = buffer.join("\n").trimEnd();
+    if (source.trim()) {
+      blocks.push({
+        source,
+        startLine,
+        endLine,
+      });
+    }
+    buffer = [];
+  }
+
+  for (let index = 0; index < lines.length; index += 1) {
+    const line = lines[index];
+    const lineNumber = index + 1;
+    const fenceMatch = /^ {0,3}(`{3,}|~{3,})/.exec(line);
+    const trimmed = line.trim();
+
+    if (fenceMatch) {
+      const marker = fenceMatch[1];
+      if (!inFence) {
+        inFence = true;
+        fenceChar = marker[0];
+        fenceLength = marker.length;
+      } else if (marker[0] === fenceChar && marker.length >= fenceLength) {
+        inFence = false;
+      }
+    }
+
+    if (!inFence && trimmed === "$$") {
+      inMathBlock = !inMathBlock;
+    }
+
+    if (!inFence && !inMathBlock && trimmed === "") {
+      flush(lineNumber);
+      startLine = lineNumber + 1;
+      continue;
+    }
+
+    if (buffer.length === 0) startLine = lineNumber;
+    buffer.push(line);
+  }
+
+  flush(lines.length);
+  return blocks;
+}
+
+function renderSegmentedMarkdown(markdown, options) {
+  if (!isSegmentedPreviewSafe(markdown)) {
+    return { mode: "full-required", reason: "unsafe-markdown" };
+  }
+
+  const blocks = splitMarkdownBlocks(markdown);
+  if (blocks.length < (options.minimumBlocks || 1)) {
+    return { mode: "full-required", reason: "too-few-blocks" };
+  }
+
+  const seenHashes = new Map();
+  const renderedBlocks = blocks.map((block) => {
+    const hash = hashString(block.source);
+    const seenCount = seenHashes.get(hash) || 0;
+    seenHashes.set(hash, seenCount + 1);
+    const html = marked.parse(block.source);
+    return {
+      id: `preview-block-${hash}-${seenCount}`,
+      hash,
+      html,
+      htmlLength: html.length,
+      sourceLength: block.source.length,
+      startLine: block.startLine,
+      endLine: block.endLine,
+    };
+  });
+
+  return {
+    mode: "segmented",
+    blocks: renderedBlocks,
+    blockCount: renderedBlocks.length,
+  };
+}
+
+self.onmessage = function(event) {
+  const data = event.data || {};
+  if (data.type !== "render") return;
+
+  try {
+    const options = data.options || {};
+    ensureLibraries(options.libraryUrls || {});
+    mermaidIdCounter = 0;
+    const result = renderSegmentedMarkdown(data.markdown || "", options);
+    self.postMessage({
+      type: "render-result",
+      requestId: data.requestId,
+      result,
+    });
+  } catch (error) {
+    self.postMessage({
+      type: "render-error",
+      requestId: data.requestId,
+      error: error && error.message ? error.message : "Preview worker render failed.",
+    });
+  }
+};

+ 5 - 0
robots.txt

@@ -0,0 +1,5 @@
+User-agent: *
+Allow: /
+Disallow: /wiki/Contributing
+
+Sitemap: https://markdownplusplus.pages.dev/sitemap.xml

+ 167 - 0
sample.md

@@ -0,0 +1,167 @@
+---
+title: Welcome to markdown++
+description: A GitHub-style Markdown renderer with live preview, math, diagrams, and export support.
+author: ThisIs-Developer
+tags: ["markdown", "preview", "mermaid", "latex", "open-source"]
+---
+
+# Welcome to markdown++
+
+## ✨ Key Features
+- **Live Preview** with GitHub styling
+- **Smart Import/Export** (MD, HTML, PDF)
+- **Mermaid Diagrams** for visual documentation
+- **LaTeX Math Support** for scientific notation
+- **Emoji Support** 😄 👍 🎉
+
+## 💻 Code with Syntax Highlighting
+```javascript
+  function renderMarkdown() {
+    const markdown = markdownEditor.value;
+    const html = marked.parse(markdown);
+    const sanitizedHtml = DOMPurify.sanitize(html);
+    markdownPreview.innerHTML = sanitizedHtml;
+    
+    // Syntax highlighting is handled automatically
+    // during the parsing phase by the marked renderer.
+    // Themes are applied instantly via CSS variables.
+  }
+```
+
+## 🧮 Mathematical Expressions
+Write complex formulas with LaTeX syntax:
+
+Inline equation: $$E = mc^2$$
+
+Display equations:
+$$\\frac{\\partial f}{\\partial x} = \\lim_{h \\to 0} \\frac{f(x+h) - f(x)}{h}$$
+
+$$\\sum_{i=1}^{n} i^2 = \\frac{n(n+1)(2n+1)}{6}$$
+
+## 📊 Mermaid Diagrams
+Create powerful visualizations directly in markdown:
+
+```mermaid
+flowchart LR
+    A[Start] --> B{Is it working?}
+    B -->|Yes| C[Great!]
+    B -->|No| D[Debug]
+    C --> E[Deploy]
+    D --> B
+```
+
+### Sequence Diagram Example
+```mermaid
+sequenceDiagram
+    User->>Editor: Type markdown
+    Editor->>Preview: Render content
+    User->>Editor: Make changes
+    Editor->>Preview: Update rendering
+    User->>Export: Save as PDF
+```
+
+## 📋 Task Management
+- [x] Create responsive layout
+- [x] Implement live preview with GitHub styling
+- [x] Add syntax highlighting for code blocks
+- [x] Support math expressions with LaTeX
+- [x] Enable mermaid diagrams
+
+## 🆚 Feature Comparison
+
+| Feature                  | markdown++ (Ours) | Other Markdown Editors  |
+|:-------------------------|:----------------------:|:-----------------------:|
+| Live Preview             | ✅ GitHub-Styled       | ✅                     |
+| Sync Scrolling           | ✅ Two-way             | 🔄 Partial/None        |
+| Mermaid Support          | ✅                     | ❌/Limited             |
+| LaTeX Math Rendering     | ✅                     | ❌/Limited             |
+
+### 📝 Multi-row Headers Support
+
+<table>
+  <thead>
+    <tr>
+      <th rowspan="2">Document Type</th>
+      <th colspan="2">Support</th>
+    </tr>
+    <tr>
+      <th>markdown++ (Ours)</th>
+      <th>Other Markdown Editors</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td>Technical Docs</td>
+      <td>Full + Diagrams</td>
+      <td>Limited/Basic</td>
+    </tr>
+    <tr>
+      <td>Research Notes</td>
+      <td>Full + Math</td>
+      <td>Partial</td>
+    </tr>
+    <tr>
+      <td>Developer Guides</td>
+      <td>Full + Export Options</td>
+      <td>Basic</td>
+    </tr>
+  </tbody>
+</table>
+
+## 📝 Text Formatting Examples
+
+### Text Formatting
+
+Text can be formatted in various ways for ~~strikethrough~~, **bold**, *italic*, or ***bold italic***.
+
+For highlighting important information, use <mark>highlighted text</mark> or add <u>underlines</u> where appropriate.
+
+### Superscript and Subscript
+
+Chemical formulas: H<sub>2</sub>O, CO<sub>2</sub>  
+Mathematical notation: x<sup>2</sup>, e<sup>iπ</sup>
+
+### Keyboard Keys
+
+Press <kbd>Ctrl</kbd> + <kbd>B</kbd> for bold text.
+
+### Abbreviations
+
+<abbr title="Graphical User Interface">GUI</abbr>  
+<abbr title="Application Programming Interface">API</abbr>
+
+### Text Alignment
+
+<div style="text-align: center">
+Centered text for headings or important notices
+</div>
+
+<div style="text-align: right">
+Right-aligned text (for dates, signatures, etc.)
+</div>
+
+### **Lists**
+
+Create bullet points:
+* Item 1
+* Item 2
+  * Nested item
+    * Nested further
+
+### **Links and Images**
+
+Add a [link](https://gitgog.alwaysdata.net/parv.ashwani/markdownplusplus) to important resources.
+
+Embed an image:
+![Markdown Logo](https://markdownplusplus.pages.dev/assets/icon.jpg)
+
+### **Blockquotes**
+
+Quote someone famous:
+> "The best way to predict the future is to invent it." - Alan Kay
+
+---
+
+## 🛡️ Security Note
+
+This is a fully client-side application. Your content never leaves your browser and stays secure on your device.

+ 9922 - 0
script.js

@@ -0,0 +1,9922 @@
+document.addEventListener("DOMContentLoaded", function () {
+  // PERF-002: Lazy script loader for optional heavy libraries
+  const _loadedScripts = new Set();
+  function loadScript(url) {
+    if (_loadedScripts.has(url)) return Promise.resolve();
+    return new Promise(function(resolve, reject) {
+      const script = document.createElement('script');
+      script.src = url;
+      script.onload = function() { _loadedScripts.add(url); resolve(); };
+      script.onerror = function() { reject(new Error('Failed to load: ' + url)); };
+      document.head.appendChild(script);
+    });
+  }
+
+  const _loadedStyles = new Set();
+  function loadStyle(url) {
+    if (_loadedStyles.has(url)) return Promise.resolve();
+    return new Promise(function(resolve, reject) {
+      const link = document.createElement('link');
+      link.rel = 'stylesheet';
+      link.href = url;
+      link.onload = function() { _loadedStyles.add(url); resolve(); };
+      link.onerror = function() { reject(new Error('Failed to load style: ' + url)); };
+      document.head.appendChild(link);
+    });
+  }
+
+  // CDN URLs for lazy-loaded libraries
+  const CDN = {
+    mermaid: 'https://cdn.jsdelivr.net/npm/mermaid@11.15.0/dist/mermaid.min.js',
+    mathjax: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.min.js',
+    jspdf: 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js',
+    html2canvas: 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js',
+    pako: 'https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js',
+    joypixels: 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/lib/js/joypixels.min.js',
+    joypixels_css: 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/extras/css/joypixels.min.css'
+  };
+
+  let markdownRenderTimeout = null;
+  let pendingPreviewRenderCancel = null;
+  let previewRenderGeneration = 0;
+  let previewHasCommittedRender = false;
+  let previewLastRenderedTabId = null;
+  // PERF-003: Track last rendered content to skip redundant renders
+  let _lastRenderedContent = null;
+  const LARGE_DOCUMENT_THRESHOLD = 15000;
+  const HUGE_DOCUMENT_THRESHOLD = 100000;
+  const PREVIEW_ENGINE_V2_ENABLED = true;
+  const PREVIEW_WORKER_THRESHOLD = 50000;
+  const PREVIEW_WORKER_TIMEOUT = 12000;
+  const PREVIEW_SEGMENT_MIN_BLOCKS = 8;
+  const PREVIEW_BLOCK_REUSE_LIMIT = 12000;
+  const PREVIEW_SANITIZE_OPTIONS = {
+    ADD_TAGS: ['mjx-container', 'input'],
+    ADD_ATTR: ['id', 'class', 'style', 'align', 'type', 'checked', 'disabled', 'data-original-code'],
+    ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel|blob):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
+  };
+  const RENDER_DELAY = 100;
+  const LARGE_RENDER_DELAY = 160;
+  const HUGE_RENDER_DELAY = 240;
+  let syncScrollingEnabled = true;
+  let isEditorScrolling = false;
+  let isPreviewScrolling = false;
+  let isProgrammaticScrolling = false;
+  let scrollSyncTimeout = null;
+  const SCROLL_SYNC_DELAY = 10;
+
+  // View Mode State - Story 1.1
+  let currentViewMode = 'split'; // 'editor', 'split', or 'preview'
+  const APP_VERSION = '3.7.4';
+  let activeModal = null;
+  let lastFocusedElement = null;
+  let isFindModalOpen = false;
+  let findMatches = [];
+  let activeFindIndex = -1;
+  let lastFindQuery = '';
+
+  // Custom Editor History State Manager variables
+  const tabHistories = {};
+  let currentHistoryTabId = null;
+  let lastPushedValue = '';
+  let typingTimeout = null;
+  let lastInputType = null; // 'insert', 'delete', 'programmatic', or null
+  let lastCursorStart = 0;
+  let lastCursorEnd = 0;
+  let pendingState = null;
+  let previewWorker = null;
+  let previewWorkerUnavailable = false;
+  let previewWorkerRequestCounter = 0;
+  let previewWorkerFailureCount = 0;
+  const previewWorkerRequests = new Map();
+  const previewSegmentHtmlCache = new Map();
+  let previewSegmentCacheTabId = null;
+
+  const markdownEditor = document.getElementById("markdown-editor");
+  const markdownPreview = document.getElementById("markdown-preview");
+  const markdownFormatToolbar = document.getElementById("markdown-format-toolbar");
+  const themeToggle = document.getElementById("theme-toggle");
+  const directionToggle = document.getElementById("direction-toggle");
+  const importFromFileButton = document.getElementById("import-from-file");
+  const importFromGithubButton = document.getElementById("import-from-github");
+  const fileInput = document.getElementById("file-input");
+  const exportMd = document.getElementById("export-md");
+  const exportHtml = document.getElementById("export-html");
+  const exportPdf = document.getElementById("export-pdf");
+  const copyMarkdownButton = document.getElementById("copy-markdown-button");
+  const dragOverlay = document.getElementById("drag-overlay");
+  const toggleSyncButton = document.getElementById("toggle-sync");
+  const editorPane = document.getElementById("markdown-editor");
+  const previewPane = document.querySelector(".preview-pane");
+  const readingTimeElement = document.getElementById("reading-time");
+  const wordCountElement = document.getElementById("word-count");
+  const charCountElement = document.getElementById("char-count");
+
+  // View Mode Elements - Story 1.1
+  const contentContainer = document.querySelector(".content-container");
+  const viewModeButtons = document.querySelectorAll(".view-toggle-btn");
+
+  // Mobile View Mode Elements - Story 1.4
+  const mobileViewModeButtons = document.querySelectorAll(".mobile-view-mode-btn");
+
+  // Resize Divider Elements - Story 1.3
+  const resizeDivider = document.querySelector(".resize-divider");
+  const editorPaneElement = document.querySelector(".editor-pane");
+  const previewPaneElement = document.querySelector(".preview-pane");
+  let isResizing = false;
+  let editorWidthPercent = 50; // Default 50%
+  const MIN_PANE_PERCENT = 20; // Minimum 20% width
+
+  const mobileMenuToggle    = document.getElementById("mobile-menu-toggle");
+  const mobileMenuPanel     = document.getElementById("mobile-menu-panel");
+  const mobileMenuOverlay   = document.getElementById("mobile-menu-overlay");
+  const mobileCloseMenu     = document.getElementById("close-mobile-menu");
+  const mobileReadingTime   = document.getElementById("mobile-reading-time");
+  const mobileWordCount     = document.getElementById("mobile-word-count");
+  const mobileCharCount     = document.getElementById("mobile-char-count");
+  const mobileToggleSync    = document.getElementById("mobile-toggle-sync");
+  const mobileImportBtn     = document.getElementById("mobile-import-button");
+  const mobileImportGithubBtn = document.getElementById("mobile-import-github-button");
+  const mobileExportMd      = document.getElementById("mobile-export-md");
+  const mobileExportHtml    = document.getElementById("mobile-export-html");
+  const mobileExportPdf     = document.getElementById("mobile-export-pdf");
+  const mobileCopyMarkdown  = document.getElementById("mobile-copy-markdown");
+  const mobileThemeToggle   = document.getElementById("mobile-theme-toggle");
+  const shareButton         = document.getElementById("share-button");
+  const mobileShareButton   = document.getElementById("mobile-share-button");
+  const githubImportModal = document.getElementById("github-import-modal");
+  const githubImportTitle = document.getElementById("github-import-title");
+  const githubImportUrlInput = document.getElementById("github-import-url");
+  const githubImportFileSelect = document.getElementById("github-import-file-select");
+  const githubImportSelectionToolbar = document.getElementById("github-import-selection-toolbar");
+  const githubImportSelectedCount = document.getElementById("github-import-selected-count");
+  const githubImportSelectAllBtn = document.getElementById("github-import-select-all");
+  const githubImportTree = document.getElementById("github-import-tree");
+  const githubImportError = document.getElementById("github-import-error");
+  const githubImportCancelBtn = document.getElementById("github-import-cancel");
+  const githubImportSubmitBtn = document.getElementById("github-import-submit");
+  const editorHighlightLayer = document.getElementById("editor-highlight-layer");
+  const lineNumbers = document.getElementById("line-numbers");
+  const clearFormattingModal = document.getElementById("clear-formatting-modal");
+  const clearFormattingConfirm = document.getElementById("clear-formatting-confirm");
+  const clearFormattingCancel = document.getElementById("clear-formatting-cancel");
+  const clearFormattingClose = document.getElementById("clear-formatting-close");
+  const findReplaceModal = document.getElementById("find-replace-modal");
+  const findReplaceInput = document.getElementById("find-replace-input");
+  const findReplaceWith = document.getElementById("find-replace-with");
+  const findReplaceCount = document.getElementById("find-replace-count");
+  const findReplacePrev = document.getElementById("find-prev");
+  const findReplaceNext = document.getElementById("find-next");
+  const findReplaceCurrent = document.getElementById("find-replace-current");
+  const findReplaceAll = document.getElementById("find-replace-all");
+  const findReplaceClose = document.getElementById("find-replace-close");
+  const findReplaceCloseIcon = document.getElementById("find-replace-close-icon");
+  const helpModal = document.getElementById("help-modal");
+  const helpModalClose = document.getElementById("help-modal-close");
+  const helpModalCloseIcon = document.getElementById("help-modal-close-icon");
+  const aboutModal = document.getElementById("about-modal");
+  const aboutModalClose = document.getElementById("about-modal-close");
+  const aboutModalCloseIcon = document.getElementById("about-modal-close-icon");
+  const aboutVersion = document.getElementById("about-version");
+  if (aboutVersion) {
+    aboutVersion.textContent = APP_VERSION;
+  }
+
+  // ========================================
+  // GLOBAL STATE (persisted across reloads)
+  // ========================================
+  const GLOBAL_STATE_KEY = 'markdownViewerGlobalState';
+  let referenceCounter = 1;
+  const imageObjectUrls = new Set();
+  const EMOJI_API_URL = 'https://api.github.com/emojis';
+  let emojiLoadPromise = null;
+  let emojiEntries = [];
+  let emojiUrlMap = new Map();
+  let emojiLookupLoaded = false;
+  let emojiRenderScheduled = false;
+  let emojiItems = [];
+  const emojiSelection = new Set();
+  let symbolItems = [];
+  const symbolSelection = new Set();
+  const SYMBOL_SECTIONS = [
+    {
+      title: 'Common symbols',
+      items: [
+        { symbol: '©', entity: '&copy;', name: 'copyright' },
+        { symbol: '®', entity: '&reg;', name: 'registered' },
+        { symbol: '™', entity: '&trade;', name: 'trademark' },
+        { symbol: '✓', entity: '&check;', name: 'check' },
+        { symbol: '★', entity: '&star;', name: 'star' },
+        { symbol: '•', entity: '&bull;', name: 'bullet' },
+        { symbol: '…', entity: '&hellip;', name: 'ellipsis' },
+        { symbol: '—', entity: '&mdash;', name: 'em dash' },
+        { symbol: '–', entity: '&ndash;', name: 'en dash' },
+        { symbol: '→', entity: '&rarr;', name: 'right arrow' },
+        { symbol: '←', entity: '&larr;', name: 'left arrow' },
+        { symbol: '↑', entity: '&uarr;', name: 'up arrow' },
+        { symbol: '↓', entity: '&darr;', name: 'down arrow' },
+      ],
+    },
+    {
+      title: 'HTML entities',
+      items: [
+        { symbol: '€', entity: '&euro;', name: 'euro' },
+        { symbol: '£', entity: '&pound;', name: 'pound' },
+        { symbol: '¥', entity: '&yen;', name: 'yen' },
+        { symbol: '§', entity: '&sect;', name: 'section' },
+        { symbol: '°', entity: '&deg;', name: 'degree' },
+        { symbol: '±', entity: '&plusmn;', name: 'plus minus' },
+        { symbol: '×', entity: '&times;', name: 'times' },
+        { symbol: '÷', entity: '&divide;', name: 'divide' },
+        { symbol: '≠', entity: '&ne;', name: 'not equal' },
+        { symbol: '≤', entity: '&le;', name: 'less equal' },
+        { symbol: '≥', entity: '&ge;', name: 'greater equal' },
+        { symbol: '∞', entity: '&infin;', name: 'infinity' },
+        { symbol: 'µ', entity: '&micro;', name: 'micro' },
+        { symbol: '¼', entity: '&frac14;', name: 'quarter' },
+        { symbol: '½', entity: '&frac12;', name: 'half' },
+        { symbol: '¾', entity: '&frac34;', name: 'three quarters' },
+        { symbol: '«', entity: '&laquo;', name: 'left quote' },
+        { symbol: '»', entity: '&raquo;', name: 'right quote' },
+      ],
+    },
+    {
+      title: 'Markdown-safe characters',
+      items: [
+        { symbol: '&', entity: '&amp;', name: 'ampersand' },
+        { symbol: '<', entity: '&lt;', name: 'less than' },
+        { symbol: '>', entity: '&gt;', name: 'greater than' },
+        { symbol: '"', entity: '&quot;', name: 'double quote' },
+        { symbol: "'", entity: '&#39;', name: 'apostrophe' },
+        { symbol: '|', entity: '&#124;', name: 'pipe' },
+        { symbol: '\\', entity: '&#92;', name: 'backslash' },
+        { symbol: '`', entity: '&#96;', name: 'backtick' },
+        { symbol: '*', entity: '&#42;', name: 'asterisk' },
+        { symbol: '_', entity: '&#95;', name: 'underscore' },
+        { symbol: '{', entity: '&#123;', name: 'left brace' },
+        { symbol: '}', entity: '&#125;', name: 'right brace' },
+        { symbol: '[', entity: '&#91;', name: 'left bracket' },
+        { symbol: ']', entity: '&#93;', name: 'right bracket' },
+        { symbol: '(', entity: '&#40;', name: 'left parenthesis' },
+        { symbol: ')', entity: '&#41;', name: 'right parenthesis' },
+      ],
+    },
+  ];
+
+  // In-memory cache for global state to avoid repeated JSON parse/stringify round-trips (PERF-008/024)
+  let _globalStateCache = null;
+  function loadGlobalState() {
+    if (_globalStateCache) return _globalStateCache;
+    try { _globalStateCache = JSON.parse(localStorage.getItem(GLOBAL_STATE_KEY)) || {}; }
+    catch { _globalStateCache = {}; }
+    return _globalStateCache;
+  }
+
+  function saveGlobalState(patch) {
+    _globalStateCache = { ...loadGlobalState(), ...patch };
+    localStorage.setItem(GLOBAL_STATE_KEY, JSON.stringify(_globalStateCache));
+  }
+
+  // Check dark mode preference first for proper initialization
+  const prefersDarkMode =
+    window.matchMedia &&
+    window.matchMedia("(prefers-color-scheme: dark)").matches;
+  const savedTheme = loadGlobalState().theme;
+  const initialTheme = savedTheme ?? (prefersDarkMode ? "dark" : "light");
+
+  document.documentElement.setAttribute("data-theme", initialTheme);
+
+  themeToggle.innerHTML = initialTheme === "dark"
+    ? '<i class="bi bi-sun"></i>'
+    : '<i class="bi bi-moon"></i>';
+
+  function updateDirectionToggleUI(direction) {
+    const isRtl = direction === "rtl";
+    const toggleLabel = isRtl ? "Switch to LTR" : "Switch to RTL";
+    if (directionToggle) {
+      directionToggle.textContent = isRtl ? "R" : "L";
+      directionToggle.setAttribute("title", toggleLabel);
+      directionToggle.setAttribute("aria-label", toggleLabel);
+      directionToggle.setAttribute("aria-pressed", isRtl.toString());
+    }
+  }
+
+  const savedDirection = loadGlobalState().direction;
+  const initialDirection = savedDirection === "rtl" ? "rtl" : "ltr";
+  function applyDirectionToContent(direction) {
+    if (markdownEditor) markdownEditor.setAttribute("dir", direction);
+    if (markdownPreview) markdownPreview.setAttribute("dir", direction);
+  }
+  applyDirectionToContent(initialDirection);
+  updateDirectionToggleUI(initialDirection);
+
+  // Track last Mermaid theme to avoid redundant re-initialization (PERF-005)
+  let _lastMermaidTheme = null;
+  let _mermaidThemeReinitTimeout = null;
+  let _themeTransitionTimeout = null;
+  const initMermaid = (forceReinit) => {
+    if (typeof mermaid === 'undefined') return; // PERF-002: Not loaded yet
+    const currentTheme = document.documentElement.getAttribute("data-theme");
+    const mermaidTheme = currentTheme === "dark" ? "dark" : "default";
+    
+    // Skip re-initialization if theme hasn't changed (PERF-005)
+    if (!forceReinit && _lastMermaidTheme === mermaidTheme) return;
+    _lastMermaidTheme = mermaidTheme;
+    
+    mermaid.initialize({
+      startOnLoad: false,
+      theme: mermaidTheme,
+      securityLevel: 'strict',
+      flowchart: { useMaxWidth: true, htmlLabels: true },
+      fontSize: 16
+    });
+  };
+
+  // PERF-002: Removed eager initMermaid() — mermaid is now lazy-loaded on first use
+
+  const markedOptions = {
+    gfm: true,
+    breaks: true,
+    pedantic: false,
+    sanitize: false,
+    smartypants: false,
+    xhtml: false,
+    headerIds: true,
+    mangle: false,
+  };
+  const LINE_NUMBER_GUTTER_MIN_CH = 3;
+  const LINE_NUMBER_GUTTER_PADDING_CH = 1;
+  const LINE_NUMBER_EMPTY_PLACEHOLDER = '\u200b';
+  const LINE_CACHE_MAX_ENTRIES = 5000;
+  const LARGE_EDITOR_WORK_DELAY = 180;
+  const HUGE_EDITOR_WORK_DELAY = 320;
+  const FIND_REFRESH_DELAY = 120;
+  const LARGE_FIND_REFRESH_DELAY = 320;
+  let lineNumberMeasure = null;
+  let lineNumberUpdateFrame = null;
+  let lineNumberUpdateTimeout = null;
+  let editorOverlayScrollFrame = null;
+  let findRefreshTimeout = null;
+
+  const renderer = new marked.Renderer();
+  const BLOCK_MATH_MARKER_PATTERN = /^\$\$/m;
+  const BLOCK_MATH_PATTERN = /^\$\$[ \t]*\n?([\s\S]*?)\n?\$\$[ \t]*(?:\n|$)/;
+  const DEFINITION_LIST_ITEM_PATTERN = /^:[ \t]+(.*)$/;
+  const SUPERSCRIPT_PATTERN = /^\^(?!\s)([^^\n]*?\S)\^(?!\^)/;
+  const SUBSCRIPT_PATTERN = /^~(?!~)(?!\s)([^~\n]*?\S)~(?!~)/;
+  const HIGHLIGHT_PATTERN = /^==(?=\S)([\s\S]*?\S)==/;
+  const MARKDOWN_LIST_MARKER_PATTERN = /^(\s*)(?:[-*+]\s+|\d+\.\s+|>\s+)/;
+  const EMPTY_LINE_PATTERN = /^\s*$/;
+  const footnoteDefinitions = new Map();
+  const footnoteOrder = [];
+  const footnoteRefCounts = new Map();
+  const footnoteFirstRefId = new Map();
+  let anonymousFootnoteCounter = 0;
+  let suppressFootnotePreprocess = false;
+
+  function resetExtendedMarkdownState() {
+    footnoteDefinitions.clear();
+    footnoteOrder.length = 0;
+    footnoteRefCounts.clear();
+    footnoteFirstRefId.clear();
+    anonymousFootnoteCounter = 0;
+  }
+
+  function normalizeFootnoteId(id) {
+    const normalized = String(id || "")
+      .trim()
+      .toLowerCase()
+      .replace(/[^a-z0-9_-]+/g, "-")
+      .replace(/^-+|-+$/g, "");
+    if (normalized) {
+      return normalized;
+    }
+
+    anonymousFootnoteCounter += 1;
+    return `footnote-${anonymousFootnoteCounter}`;
+  }
+
+  function escapeHtmlAttribute(value) {
+    return String(value)
+      .replace(/&/g, "&amp;")
+      .replace(/"/g, "&quot;")
+      .replace(/'/g, "&#39;")
+      .replace(/</g, "&lt;")
+      .replace(/>/g, "&gt;");
+  }
+
+  function sanitizePreviewHtml(html) {
+    if (typeof DOMPurify === "undefined") {
+      throw new ReferenceError("DOMPurify is not defined. Secure rendering aborted.");
+    }
+    return DOMPurify.sanitize(html, PREVIEW_SANITIZE_OPTIONS);
+  }
+
+  function getLoadedScriptUrl(needle, fallbackUrl) {
+    const scripts = document.getElementsByTagName("script");
+    for (let i = 0; i < scripts.length; i += 1) {
+      const src = scripts[i].getAttribute("src") || "";
+      if (src.includes(needle)) {
+        try {
+          return new URL(src, window.location.href).toString();
+        } catch (e) {
+          return src;
+        }
+      }
+    }
+    return fallbackUrl;
+  }
+
+  function getPreviewWorkerUrl() {
+    const scripts = document.getElementsByTagName("script");
+    let scriptUrl = "";
+    for (let i = scripts.length - 1; i >= 0; i -= 1) {
+      const src = scripts[i].getAttribute("src") || "";
+      if (src.includes("script.js")) {
+        scriptUrl = src;
+        break;
+      }
+    }
+
+    try {
+      return new URL("preview-worker.js", scriptUrl ? new URL(scriptUrl, window.location.href) : window.location.href).toString();
+    } catch (e) {
+      return "preview-worker.js";
+    }
+  }
+
+  function getPreviewWorkerLibraryUrls() {
+    return {
+      marked: getLoadedScriptUrl("marked", "https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"),
+      highlight: getLoadedScriptUrl("highlight", "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"),
+    };
+  }
+
+  function isSegmentedPreviewSafe(markdown) {
+    if (!markdown || markdown.length < PREVIEW_WORKER_THRESHOLD) return false;
+    if (/^\s*---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/.test(markdown)) return false;
+    if (/^\[[^\]\n]+\]:\s+\S+/m.test(markdown)) return false;
+    if (/\[\^[^\]\n]+\]/.test(markdown)) return false;
+    if (/\n:[ \t]+/.test(markdown)) return false;
+    if (/^\s{0,3}<\/?[a-zA-Z][\w:-]*(?:\s|>|\/>)/m.test(markdown)) return false;
+    return true;
+  }
+
+  function shouldUsePreviewWorker(rawVal, context) {
+    if (!PREVIEW_ENGINE_V2_ENABLED || previewWorkerUnavailable || context.disableWorker) return false;
+    if (typeof Worker === "undefined" || typeof URL === "undefined") return false;
+    return isSegmentedPreviewSafe(rawVal);
+  }
+
+  function resetPreviewSegmentCache(previewDocumentId) {
+    if (previewSegmentCacheTabId !== previewDocumentId) {
+      previewSegmentHtmlCache.clear();
+      previewSegmentCacheTabId = previewDocumentId;
+    }
+  }
+
+  function trimPreviewSegmentCache() {
+    while (previewSegmentHtmlCache.size > PREVIEW_BLOCK_REUSE_LIMIT) {
+      const firstKey = previewSegmentHtmlCache.keys().next().value;
+      previewSegmentHtmlCache.delete(firstKey);
+    }
+  }
+
+  function buildSegmentedPreviewHtml(blocks, previewDocumentId) {
+    resetPreviewSegmentCache(previewDocumentId);
+    const htmlParts = [];
+
+    blocks.forEach(function(block, index) {
+      const hash = String(block.hash || "");
+      const cacheKey = `${hash}:${block.sourceLength || 0}:${block.htmlLength || (block.html ? block.html.length : 0)}`;
+      let sanitizedBlock = previewSegmentHtmlCache.get(cacheKey);
+      if (sanitizedBlock === undefined) {
+        sanitizedBlock = sanitizePreviewHtml(block.html || "");
+        previewSegmentHtmlCache.set(cacheKey, sanitizedBlock);
+      }
+      const blockId = block.id || `preview-block-${index}`;
+      htmlParts.push(
+        `<section class="preview-render-block" style="content-visibility: auto; contain-intrinsic-size: auto 220px;" data-preview-block-id="${escapeHtmlAttribute(blockId)}" data-preview-block-hash="${escapeHtmlAttribute(cacheKey)}">${sanitizedBlock}</section>`
+      );
+    });
+
+    trimPreviewSegmentCache();
+    return htmlParts.join("");
+  }
+
+  function markPreviewWorkerFailure(error) {
+    previewWorkerFailureCount += 1;
+    if (previewWorkerFailureCount >= 2) {
+      previewWorkerUnavailable = true;
+    }
+    if (previewWorker) {
+      try {
+        previewWorker.terminate();
+      } catch (e) {
+        // Ignore worker shutdown failures; fallback rendering will continue on main.
+      }
+      previewWorker = null;
+    }
+    previewWorkerRequests.forEach(function(pending) {
+      clearTimeout(pending.timeoutId);
+      pending.reject(error || new Error("Preview worker unavailable."));
+    });
+    previewWorkerRequests.clear();
+  }
+
+  function recordPreviewWorkerRenderFailure() {
+    previewWorkerFailureCount += 1;
+    if (previewWorkerFailureCount < 2) return;
+    previewWorkerUnavailable = true;
+    if (previewWorker) {
+      try {
+        previewWorker.terminate();
+      } catch (e) {
+        // Ignore worker shutdown failures; fallback rendering will continue on main.
+      }
+      previewWorker = null;
+    }
+  }
+
+  function getPreviewWorker() {
+    if (previewWorkerUnavailable) return null;
+    if (previewWorker) return previewWorker;
+    try {
+      previewWorker = new Worker(getPreviewWorkerUrl());
+      previewWorker.onmessage = function(event) {
+        const data = event.data || {};
+        const pending = previewWorkerRequests.get(data.requestId);
+        if (!pending) return;
+        clearTimeout(pending.timeoutId);
+        previewWorkerRequests.delete(data.requestId);
+        if (data.type === "render-result") {
+          previewWorkerFailureCount = 0;
+          pending.resolve(data.result);
+        } else {
+          recordPreviewWorkerRenderFailure();
+          pending.reject(new Error(data.error || "Preview worker render failed."));
+        }
+      };
+      previewWorker.onerror = function(event) {
+        markPreviewWorkerFailure(event && event.message ? new Error(event.message) : new Error("Preview worker failed."));
+      };
+    } catch (e) {
+      markPreviewWorkerFailure(e);
+      return null;
+    }
+    return previewWorker;
+  }
+
+  function requestPreviewWorkerRender(rawVal, context) {
+    const worker = getPreviewWorker();
+    if (!worker) {
+      return Promise.reject(new Error("Preview worker unavailable."));
+    }
+
+    const requestId = ++previewWorkerRequestCounter;
+    return new Promise(function(resolve, reject) {
+      const timeoutId = setTimeout(function() {
+        previewWorkerRequests.delete(requestId);
+        recordPreviewWorkerRenderFailure();
+        reject(new Error("Preview worker timed out."));
+      }, PREVIEW_WORKER_TIMEOUT);
+
+      previewWorkerRequests.set(requestId, { resolve, reject, timeoutId });
+      worker.postMessage({
+        type: "render",
+        requestId,
+        markdown: rawVal,
+        options: {
+          minimumBlocks: PREVIEW_SEGMENT_MIN_BLOCKS,
+          libraryUrls: getPreviewWorkerLibraryUrls(),
+          renderId: context.renderId,
+        },
+      });
+    });
+  }
+
+  function parseInlineWithoutFootnotes(text) {
+    suppressFootnotePreprocess = true;
+    try {
+      return marked.parseInline(text);
+    } finally {
+      suppressFootnotePreprocess = false;
+    }
+  }
+
+  function renderDefinitionContent(content, options = {}) {
+    const { appendHtml = "" } = options;
+    const paragraphs = String(content || "")
+      .split(/\n(?:[ \t]*\n)+/)
+      .map((paragraph) => paragraph.trim())
+      .filter(Boolean);
+
+    if (appendHtml) {
+      if (paragraphs.length === 0) {
+        paragraphs.push(appendHtml);
+      } else {
+        paragraphs[paragraphs.length - 1] = `${paragraphs[paragraphs.length - 1]} ${appendHtml}`;
+      }
+    }
+
+    return paragraphs
+      .map((paragraph) => {
+        const renderedParagraph = parseInlineWithoutFootnotes(paragraph);
+        if (typeof DOMPurify === "undefined") {
+          throw new ReferenceError("DOMPurify is not defined. Secure rendering aborted.");
+        }
+        const safeParagraph = DOMPurify.sanitize(renderedParagraph);
+        return `<p>${safeParagraph}</p>`;
+      })
+      .join("");
+  }
+
+  function extractFootnoteDefinitions(markdown) {
+    const lines = markdown.split("\n");
+    const preservedLines = [];
+    let index = 0;
+
+    while (index < lines.length) {
+      const match = /^([ \t]{0,3})\[\^([^\]\n]+)\]:[ \t]*(.*)$/.exec(lines[index]);
+      if (!match) {
+        preservedLines.push(lines[index]);
+        index += 1;
+        continue;
+      }
+
+      const baseIndent = match[1] || "";
+      const id = match[2].trim();
+      const definitionLines = [match[3] || ""];
+      index += 1;
+
+      while (index < lines.length) {
+        const line = lines[index];
+        if (!line.startsWith(baseIndent)) {
+          break;
+        }
+
+        const lineAfterBase = line.slice(baseIndent.length);
+        const indentedMatch = /^(?: {2,}|\t)(.*)$/.exec(lineAfterBase);
+        if (indentedMatch) {
+          definitionLines.push(indentedMatch[1]);
+          index += 1;
+          continue;
+        }
+
+        if (lineAfterBase.trim() === "") {
+          const nextLine = lines[index + 1] || "";
+          const nextAfterBase = nextLine.startsWith(baseIndent)
+            ? nextLine.slice(baseIndent.length)
+            : "";
+          if (/^(?: {2,}|\t)/.test(nextAfterBase)) {
+            definitionLines.push("");
+            index += 1;
+            continue;
+          }
+        }
+
+        break;
+      }
+
+      footnoteDefinitions.set(id, definitionLines.join("\n").trim());
+    }
+
+    return preservedLines.join("\n");
+  }
+
+  function applyFootnotes(markdown) {
+    const markdownWithReferences = markdown.replace(/\[\^([^\]\n]+)\]/g, function(match, idText) {
+      const id = idText.trim();
+      if (!id) {
+        return match;
+      }
+
+      if (!footnoteOrder.includes(id)) {
+        footnoteOrder.push(id);
+      }
+
+      const refCount = (footnoteRefCounts.get(id) || 0) + 1;
+      footnoteRefCounts.set(id, refCount);
+
+      const normalizedId = normalizeFootnoteId(id);
+      const refId = `fnref-${normalizedId}${refCount > 1 ? `-${refCount}` : ""}`;
+      if (!footnoteFirstRefId.has(id)) {
+        footnoteFirstRefId.set(id, refId);
+      }
+
+      const noteNumber = footnoteOrder.indexOf(id) + 1;
+      const safeRefId = escapeHtmlAttribute(refId);
+      const safeNormalizedId = escapeHtmlAttribute(normalizedId);
+      return `<sup id="${safeRefId}" class="footnote-ref"><a href="#fn-${safeNormalizedId}" aria-label="Footnote ${noteNumber}">[${noteNumber}]</a></sup>`;
+    });
+
+    const footnotesHtml = footnoteOrder
+      .filter((id) => footnoteDefinitions.has(id))
+      .map((id) => {
+        const normalizedId = normalizeFootnoteId(id);
+        const backRefId = footnoteFirstRefId.get(id) || `fnref-${normalizedId}`;
+        const safeNormalizedId = escapeHtmlAttribute(normalizedId);
+        const safeBackRefId = escapeHtmlAttribute(backRefId);
+        const backRefHtml = `<a href="#${safeBackRefId}" class="footnote-backref" aria-label="Back to content">←</a>`;
+        const noteHtml = renderDefinitionContent(
+          footnoteDefinitions.get(id) || "",
+          { appendHtml: backRefHtml }
+        );
+        return `<li id="fn-${safeNormalizedId}">${noteHtml}</li>`;
+      })
+      .join("");
+
+    if (!footnotesHtml) {
+      return markdownWithReferences;
+    }
+
+    return `${markdownWithReferences}\n\n<section class="footnotes"><hr><ol>${footnotesHtml}</ol></section>`;
+  }
+
+  const blockMathExtension = {
+    name: 'blockMath',
+    level: 'block',
+    start(src) {
+      const match = src.match(BLOCK_MATH_MARKER_PATTERN);
+      if (!match) {
+        return undefined;
+      }
+      return match.index;
+    },
+    tokenizer(src) {
+      const match = BLOCK_MATH_PATTERN.exec(src);
+      if (!match) {
+        return undefined;
+      }
+      return {
+        type: 'blockMath',
+        raw: match[0],
+        text: match[1],
+      };
+    },
+    renderer(token) {
+      return `<div class="math-block">$$\n${token.text}\n$$</div>\n`;
+    }
+  };
+  const definitionListExtension = {
+    name: "definitionList",
+    level: "block",
+    start(src) {
+      const match = src.match(/\n:[ \t]+/);
+      if (!match) {
+        return undefined;
+      }
+      return match.index + 1;
+    },
+    tokenizer(src) {
+      const lines = src.split("\n");
+      if (lines.length < 2) {
+        return undefined;
+      }
+
+      const term = lines[0];
+      if (EMPTY_LINE_PATTERN.test(term) || MARKDOWN_LIST_MARKER_PATTERN.test(term)) {
+        return undefined;
+      }
+
+      if (!DEFINITION_LIST_ITEM_PATTERN.test(lines[1])) {
+        return undefined;
+      }
+
+      const definitions = [];
+      const rawLines = [term];
+      let index = 1;
+      while (index < lines.length) {
+        const itemMatch = DEFINITION_LIST_ITEM_PATTERN.exec(lines[index]);
+        if (!itemMatch) {
+          break;
+        }
+
+        rawLines.push(lines[index]);
+        const definitionLines = [itemMatch[1]];
+        index += 1;
+
+        while (index < lines.length) {
+          const line = lines[index];
+          if (DEFINITION_LIST_ITEM_PATTERN.test(line)) {
+            break;
+          }
+          if (EMPTY_LINE_PATTERN.test(line)) {
+            const nextLine = lines[index + 1] || "";
+            if (/^(?: {2,}|\t)/.test(nextLine)) {
+              rawLines.push(line);
+              definitionLines.push("");
+              index += 1;
+              continue;
+            }
+            break;
+          }
+          const continuationMatch = /^(?: {2,}|\t)(.*)$/.exec(line);
+          if (!continuationMatch) {
+            break;
+          }
+
+          rawLines.push(line);
+          definitionLines.push(continuationMatch[1]);
+          index += 1;
+        }
+
+        definitions.push(definitionLines.join("\n").trim());
+      }
+
+      if (definitions.length === 0) {
+        return undefined;
+      }
+
+      let raw = rawLines.join("\n");
+      if (src.startsWith(raw + "\n")) {
+        raw += "\n";
+      }
+
+      return {
+        type: "definitionList",
+        raw: raw,
+        term: term.trim(),
+        definitions: definitions,
+      };
+    },
+    renderer(token) {
+      const termHtml = parseInlineWithoutFootnotes(token.term);
+      const definitionHtml = token.definitions
+        .map((definition) => `<dd>${renderDefinitionContent(definition)}</dd>`)
+        .join("");
+      return `<dl><dt>${termHtml}</dt>${definitionHtml}</dl>\n`;
+    },
+  };
+  const superscriptExtension = {
+    name: "superscript",
+    level: "inline",
+    start(src) {
+      const index = src.indexOf("^");
+      return index >= 0 ? index : undefined;
+    },
+    tokenizer(src) {
+      const match = SUPERSCRIPT_PATTERN.exec(src);
+      if (!match) {
+        return undefined;
+      }
+      return {
+        type: "superscript",
+        raw: match[0],
+        text: match[1],
+      };
+    },
+    renderer(token) {
+      return `<sup>${marked.parseInline(token.text)}</sup>`;
+    },
+  };
+  const subscriptExtension = {
+    name: "subscript",
+    level: "inline",
+    start(src) {
+      const index = src.indexOf("~");
+      return index >= 0 ? index : undefined;
+    },
+    tokenizer(src) {
+      const match = SUBSCRIPT_PATTERN.exec(src);
+      if (!match) {
+        return undefined;
+      }
+      return {
+        type: "subscript",
+        raw: match[0],
+        text: match[1],
+      };
+    },
+    renderer(token) {
+      return `<sub>${marked.parseInline(token.text)}</sub>`;
+    },
+  };
+  const highlightExtension = {
+    name: "highlight",
+    level: "inline",
+    start(src) {
+      const index = src.indexOf("==");
+      return index >= 0 ? index : undefined;
+    },
+    tokenizer(src) {
+      const match = HIGHLIGHT_PATTERN.exec(src);
+      if (!match) {
+        return undefined;
+      }
+      return {
+        type: "highlight",
+        raw: match[0],
+        text: match[1],
+      };
+    },
+    renderer(token) {
+      return `<mark>${marked.parseInline(token.text)}</mark>`;
+    },
+  };
+
+  renderer.code = function (code, language) {
+    if (language === 'mermaid') {
+      const uniqueId = 'mermaid-diagram-' + Math.random().toString(36).substr(2, 9);
+      const escapedCode = code
+        .replace(/&/g, "&amp;")
+        .replace(/</g, "&lt;")
+        .replace(/>/g, "&gt;");
+      return `<div class="mermaid-container is-loading"><div class="mermaid" id="${uniqueId}" data-original-code="${encodeURIComponent(code)}">${escapedCode}</div></div>`;
+    }
+    
+    const validLanguage = hljs.getLanguage(language) ? language : "plaintext";
+    const highlightedCode = hljs.highlight(code, {
+      language: validLanguage,
+    }).value;
+    return `<pre><code class="hljs ${validLanguage}">${highlightedCode}</code></pre>`;
+  };
+
+  renderer.heading = function (text, level, raw) {
+    let id = raw
+      .toLowerCase()
+      .trim()
+      .replace(/<[^>]*>/g, '')
+      .replace(/\s+/g, '-')
+      .replace(/[^\w-]/g, '')
+      .replace(/-+/g, '-');
+    if (!id) {
+      id = 'heading-' + Math.random().toString(36).substr(2, 9);
+    }
+    return `<h${level} id="${id}">${text}</h${level}>`;
+  };
+
+  marked.use({
+    extensions: [
+      blockMathExtension,
+      definitionListExtension,
+      superscriptExtension,
+      subscriptExtension,
+      highlightExtension,
+    ],
+    hooks: {
+      preprocess(markdown) {
+        if (suppressFootnotePreprocess) {
+          return markdown;
+        }
+        resetExtendedMarkdownState();
+        // ✅ Replace escaped dollar signs before marked.js strips the backslash.
+        // This prevents MathJax from treating lone $ as a math delimiter.
+        const protectedMarkdown = markdown.replace(/\\\$/g, '&#36;');
+        return applyFootnotes(extractFootnoteDefinitions(protectedMarkdown));
+      },
+    },
+  });
+
+  marked.setOptions({
+    ...markedOptions,
+    renderer: renderer,
+  });
+
+  const GITHUB_ALERT_META = {
+    note: {
+      label: "Note",
+      viewBox: "0 0 512 512",
+      path: "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336l24 0 0-64-24 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l48 0c13.3 0 24 10.7 24 24l0 88 8 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z",
+    },
+    tip: {
+      label: "Tip",
+      viewBox: "0 0 384 512",
+      path: "M297.2 248.9C311.6 228.3 320 203.2 320 176c0-70.7-57.3-128-128-128S64 105.3 64 176c0 27.2 8.4 52.3 22.8 72.9c3.7 5.3 8.1 11.3 12.8 17.7c0 0 0 0 0 0c12.9 17.7 28.3 38.9 39.8 59.8c10.4 19 15.7 38.8 18.3 57.5L109 384c-2.2-12-5.9-23.7-11.8-34.5c-9.9-18-22.2-34.9-34.5-51.8c0 0 0 0 0 0s0 0 0 0c-5.2-7.1-10.4-14.2-15.4-21.4C27.6 247.9 16 213.3 16 176C16 78.8 94.8 0 192 0s176 78.8 176 176c0 37.3-11.6 71.9-31.4 100.3c-5 7.2-10.2 14.3-15.4 21.4c0 0 0 0 0 0s0 0 0 0c-12.3 16.8-24.6 33.7-34.5 51.8c-5.9 10.8-9.6 22.5-11.8 34.5l-48.6 0c2.6-18.7 7.9-38.6 18.3-57.5c11.5-20.9 26.9-42.1 39.8-59.8c0 0 0 0 0 0s0 0 0 0s0 0 0 0c4.7-6.4 9-12.4 12.7-17.7zM192 128c-26.5 0-48 21.5-48 48c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-44.2 35.8-80 80-80c8.8 0 16 7.2 16 16s-7.2 16-16 16zm0 384c-44.2 0-80-35.8-80-80l0-16 160 0 0 16c0 44.2-35.8 80-80 80z",
+    },
+    important: {
+      label: "Important",
+      viewBox: "0 0 512 512",
+      path: "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z",
+    },
+    warning: {
+      label: "Warning",
+      viewBox: "0 0 512 512",
+      path: "M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z",
+    },
+    caution: {
+      label: "Caution",
+      viewBox: "0 0 512 512",
+      path: "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z",
+    },
+  };
+  const GITHUB_ALERT_MARKER_REGEX = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?:(?:\s|&nbsp;|<br\s*\/?>)+|$)/i;
+
+  function enhanceGitHubAlerts(container) {
+    if (!container) return;
+
+    const blockquotes = container.querySelectorAll("blockquote");
+    blockquotes.forEach((blockquote) => {
+      let firstParagraph = null;
+      for (const child of blockquote.children) {
+        if (child.tagName === "P") {
+          firstParagraph = child;
+          break;
+        }
+      }
+      if (!firstParagraph) return;
+
+    const firstParagraphHtml = firstParagraph.innerHTML.trim();
+    const markerMatch = firstParagraphHtml.match(GITHUB_ALERT_MARKER_REGEX);
+      if (!markerMatch) return;
+
+      const alertType = markerMatch[1].toLowerCase();
+      blockquote.classList.add("markdown-alert", `markdown-alert-${alertType}`);
+
+      const title = document.createElement("p");
+      title.className = "markdown-alert-title";
+      const alertMeta = GITHUB_ALERT_META[alertType] || { label: markerMatch[1], path: "" };
+      const icon = document.createElement("span");
+      icon.className = "markdown-alert-icon";
+      icon.setAttribute("aria-hidden", "true");
+
+      if (alertMeta.path) {
+        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+        svg.setAttribute("viewBox", alertMeta.viewBox || "0 0 512 512");
+        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
+        path.setAttribute("d", alertMeta.path);
+        svg.appendChild(path);
+        icon.appendChild(svg);
+      }
+
+      const label = document.createElement("span");
+      label.textContent = alertMeta.label;
+      title.appendChild(icon);
+      title.appendChild(label);
+
+      blockquote.insertBefore(title, blockquote.firstChild);
+
+    const remainingHtml = firstParagraphHtml
+      .replace(GITHUB_ALERT_MARKER_REGEX, "")
+      .trim();
+      if (remainingHtml) {
+        firstParagraph.innerHTML = remainingHtml;
+      } else {
+        firstParagraph.remove();
+      }
+    });
+  }
+
+  function parseFrontmatter(markdown) {
+    const match = markdown.match(/^\s*---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
+    if (!match) return { frontmatter: null, body: markdown };
+    try {
+      const data = jsyaml.load(match[1]) || {};
+      return { frontmatter: data, body: markdown.slice(match[0].length) };
+    } catch (e) {
+      console.warn('Frontmatter YAML parse error:', e);
+      return { frontmatter: null, body: markdown };
+    }
+  }
+
+  function renderFrontmatterValue(value) {
+    if (value === null || value === undefined) return '';
+    if (value instanceof Date) {
+      const y = value.getUTCFullYear();
+      const m = String(value.getUTCMonth() + 1).padStart(2, '0');
+      const d = String(value.getUTCDate()).padStart(2, '0');
+      return `${y}-${m}-${d}`;
+    }
+    if (Array.isArray(value)) {
+      const allPrimitive = value.every(v => v === null || typeof v !== 'object');
+      if (allPrimitive) {
+        return value
+          .map(v => `<span class="fm-tag">${escapeHtml(String(v ?? ''))}</span>`)
+          .join('');
+      }
+      return `<pre class="fm-complex">${escapeHtml(jsyaml.dump(value).trimEnd())}</pre>`;
+    }
+    if (typeof value === 'object') {
+      return `<pre class="fm-complex">${escapeHtml(jsyaml.dump(value).trimEnd())}</pre>`;
+    }
+    return escapeHtml(String(value));
+  }
+
+  function renderFrontmatterTable(data) {
+    const rows = Object.entries(data).map(([key, value]) =>
+      `<tr><th>${escapeHtml(key)}</th><td>${renderFrontmatterValue(value)}</td></tr>`
+    );
+    return `<table class="frontmatter-table"><tbody>${rows.join('')}</tbody></table>`;
+  }
+
+  function escapeHtml(str) {
+    return str
+      .replace(/&/g, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/"/g, '&quot;');
+  }
+
+  // PERF-012: Inlined default template to eliminate network request, FOUC, and layout shifts
+  const defaultMarkdownTemplate = document.getElementById('default-markdown');
+  let templateText = '';
+  if (defaultMarkdownTemplate) {
+    if (defaultMarkdownTemplate.content && typeof defaultMarkdownTemplate.content.textContent === 'string') {
+      templateText = defaultMarkdownTemplate.content.textContent.trim();
+    } else {
+      templateText = defaultMarkdownTemplate.textContent ? defaultMarkdownTemplate.textContent.trim() : '';
+    }
+  }
+  const sampleMarkdown = templateText || '# Welcome to Markdown Viewer\n\nStart typing your markdown here...';
+
+  if (!markdownEditor.value) {
+    markdownEditor.value = sampleMarkdown;
+  }
+
+  // ========================================
+  // DOCUMENT TABS & SESSION MANAGEMENT
+  // ========================================
+
+  const STORAGE_KEY = 'markdownViewerTabs';
+  const ACTIVE_TAB_KEY = 'markdownViewerActiveTab';
+  const UNTITLED_COUNTER_KEY = 'markdownViewerUntitledCounter';
+  let tabs = [];
+  let activeTabId = null;
+  let draggedTabId = null;
+  let saveTabStateTimeout = null;
+  let untitledCounter = 0;
+
+  function loadTabsFromStorage() {
+    try {
+      return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
+    } catch (e) {
+      return [];
+    }
+  }
+
+  function saveTabsToStorage(tabsArr) {
+    // PERF-008: Debounce tab saves to reduce main thread blocking from JSON.stringify
+    // on large document arrays. Immediate flush happens on visibilitychange/beforeunload.
+    clearTimeout(saveTabStateTimeout);
+    saveTabStateTimeout = setTimeout(function() {
+      _flushTabsToStorage(tabsArr);
+    }, 500);
+  }
+
+  function _flushTabsToStorage(tabsArr) {
+    clearTimeout(saveTabStateTimeout);
+    try {
+      localStorage.setItem(STORAGE_KEY, JSON.stringify(tabsArr || tabs));
+    } catch (e) {
+      console.warn('Failed to save tabs to localStorage:', e);
+    }
+  }
+
+  // Ensure tabs are persisted before page close (PERF-008)
+  window.addEventListener('beforeunload', function() { _flushTabsToStorage(tabs); });
+  document.addEventListener('visibilitychange', function() {
+    if (document.visibilityState === 'hidden') _flushTabsToStorage(tabs);
+  });
+
+  function loadActiveTabId() {
+    return localStorage.getItem(ACTIVE_TAB_KEY);
+  }
+
+  function saveActiveTabId(id) {
+    localStorage.setItem(ACTIVE_TAB_KEY, id);
+  }
+
+  function loadUntitledCounter() {
+    return parseInt(localStorage.getItem(UNTITLED_COUNTER_KEY) || '0', 10);
+  }
+
+  function saveUntitledCounter(val) {
+    localStorage.setItem(UNTITLED_COUNTER_KEY, String(val));
+  }
+
+  function nextUntitledTitle() {
+    untitledCounter += 1;
+    saveUntitledCounter(untitledCounter);
+    return 'Untitled ' + untitledCounter;
+  }
+
+  function createTab(content, title, viewMode) {
+    if (content === undefined) content = '';
+    if (title === undefined) title = null;
+    if (viewMode === undefined) viewMode = 'split';
+    return {
+      id: 'tab_' + Date.now() + '_' + Math.random().toString(36).substring(2, 8),
+      title: title || 'Untitled',
+      content: content,
+      scrollPos: 0,
+      viewMode: viewMode,
+      createdAt: Date.now()
+    };
+  }
+
+  function closeTabMenus() {
+    document.querySelectorAll('.tab-menu-btn.open').forEach(function(btn) {
+      btn.classList.remove('open');
+      btn.setAttribute('aria-expanded', 'false');
+    });
+    document.querySelectorAll('.tab-menu-dropdown.open').forEach(function(dropdown) {
+      dropdown.classList.remove('open');
+    });
+  }
+
+  function removeTabMenuDropdowns() {
+    document.querySelectorAll('.tab-menu-dropdown[data-tab-menu-dropdown="true"]').forEach(function(dropdown) {
+      dropdown.remove();
+    });
+  }
+
+  function positionTabMenu(menuBtn, dropdown) {
+    const rect = menuBtn.getBoundingClientRect();
+    const margin = 8;
+    const dropdownWidth = dropdown.offsetWidth || 130;
+    const dropdownHeight = dropdown.offsetHeight || 110;
+    let left = rect.right - dropdownWidth;
+    let top = rect.bottom + 4;
+
+    left = Math.max(margin, Math.min(left, window.innerWidth - dropdownWidth - margin));
+    if (top + dropdownHeight > window.innerHeight - margin) {
+      top = Math.max(margin, rect.top - dropdownHeight - 4);
+    }
+
+    dropdown.style.top = top + 'px';
+    dropdown.style.left = left + 'px';
+    dropdown.style.right = 'auto';
+  }
+
+  function runTabMenuAction(tabId, action, isMobileMenu) {
+    if (action === 'rename') {
+      if (isMobileMenu) closeMobileMenu();
+      renameTab(tabId);
+    } else if (action === 'duplicate') {
+      duplicateTab(tabId);
+      if (isMobileMenu) closeMobileMenu();
+    } else if (action === 'delete') {
+      deleteTab(tabId);
+    }
+  }
+
+  function createTabActionMenu(tab, options) {
+    const isMobileMenu = options && options.isMobileMenu;
+    const menuIdPrefix = options && options.menuIdPrefix ? options.menuIdPrefix : 'tab-menu';
+    const menuId = menuIdPrefix + '-' + tab.id;
+
+    const menuBtn = document.createElement('button');
+    menuBtn.type = 'button';
+    menuBtn.className = 'tab-menu-btn';
+    menuBtn.setAttribute('aria-label', 'File options for ' + (tab.title || 'Untitled'));
+    menuBtn.setAttribute('aria-haspopup', 'menu');
+    menuBtn.setAttribute('aria-expanded', 'false');
+    menuBtn.setAttribute('aria-controls', menuId);
+    menuBtn.setAttribute('draggable', 'false');
+    menuBtn.title = 'File options';
+    // PERF-007: Replace HTML entity with plain unicode character set via textContent
+    menuBtn.textContent = '⋯';
+
+    const dropdown = document.createElement('div');
+    dropdown.id = menuId;
+    dropdown.className = 'tab-menu-dropdown';
+    dropdown.setAttribute('data-tab-menu-dropdown', 'true');
+    dropdown.setAttribute('role', 'menu');
+    dropdown.innerHTML =
+      '<button type="button" class="tab-menu-item" role="menuitem" data-action="rename"><i class="bi bi-pencil"></i> Rename</button>' +
+      '<button type="button" class="tab-menu-item" role="menuitem" data-action="duplicate"><i class="bi bi-files"></i> Duplicate</button>' +
+      '<button type="button" class="tab-menu-item tab-menu-item-danger" role="menuitem" data-action="delete"><i class="bi bi-trash"></i> Delete</button>';
+
+    menuBtn.addEventListener('click', function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+      const shouldOpen = !menuBtn.classList.contains('open');
+      closeTabMenus();
+      if (shouldOpen) {
+        menuBtn.classList.add('open');
+        menuBtn.setAttribute('aria-expanded', 'true');
+        dropdown.classList.add('open');
+        positionTabMenu(menuBtn, dropdown);
+      }
+    });
+
+    menuBtn.addEventListener('mousedown', function(e) {
+      e.stopPropagation();
+    });
+
+    menuBtn.addEventListener('dragstart', function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+    });
+
+    dropdown.addEventListener('click', function(e) {
+      e.stopPropagation();
+    });
+
+    dropdown.querySelectorAll('.tab-menu-item').forEach(function(actionBtn) {
+      actionBtn.addEventListener('click', function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        const action = actionBtn.getAttribute('data-action');
+        closeTabMenus();
+        runTabMenuAction(tab.id, action, isMobileMenu);
+      });
+    });
+
+    document.body.appendChild(dropdown);
+
+    return { button: menuBtn, dropdown: dropdown };
+  }
+
+  function renderTabBar(tabsArr, currentActiveTabId) {
+    const tabList = document.getElementById('tab-list');
+    if (!tabList) return;
+    closeTabMenus();
+    removeTabMenuDropdowns();
+    // PERF-007: Use textContent instead of innerHTML to clear elements faster
+    tabList.textContent = '';
+    tabsArr.forEach(function(tab) {
+      const item = document.createElement('div');
+      item.className = 'tab-item' + (tab.id === currentActiveTabId ? ' active' : '');
+      item.setAttribute('data-tab-id', tab.id);
+      item.setAttribute('role', 'tab');
+      item.setAttribute('aria-selected', tab.id === currentActiveTabId ? 'true' : 'false');
+      item.setAttribute('draggable', 'true');
+      item.setAttribute('tabindex', tab.id === currentActiveTabId ? '0' : '-1');
+
+      const titleSpan = document.createElement('span');
+      titleSpan.className = 'tab-title';
+      titleSpan.textContent = tab.title || 'Untitled';
+      titleSpan.title = tab.title || 'Untitled';
+
+      const tabMenu = createTabActionMenu(tab, { menuIdPrefix: 'desktop-tab-menu' });
+
+      item.appendChild(titleSpan);
+      item.appendChild(tabMenu.button);
+
+      item.addEventListener('dragstart', function() {
+        draggedTabId = tab.id;
+        setTimeout(function() { item.classList.add('dragging'); }, 0);
+      });
+
+      item.addEventListener('dragend', function() {
+        item.classList.remove('dragging');
+        draggedTabId = null;
+      });
+
+      item.addEventListener('dragover', function(e) {
+        e.preventDefault();
+        item.classList.add('drag-over');
+      });
+
+      item.addEventListener('dragleave', function() {
+        item.classList.remove('drag-over');
+      });
+
+      item.addEventListener('drop', function(e) {
+        e.preventDefault();
+        item.classList.remove('drag-over');
+        if (!draggedTabId || draggedTabId === tab.id) return;
+        const fromIdx = tabs.findIndex(function(t) { return t.id === draggedTabId; });
+        const toIdx = tabs.findIndex(function(t) { return t.id === tab.id; });
+        if (fromIdx === -1 || toIdx === -1) return;
+        const moved = tabs.splice(fromIdx, 1)[0];
+        tabs.splice(toIdx, 0, moved);
+        saveTabsToStorage(tabs);
+        renderTabBar(tabs, activeTabId);
+      });
+
+      tabList.appendChild(item);
+    });
+
+    // PERF-006: Event delegation — single click handler for all tabs
+    tabList.onclick = function(e) {
+      const tabItem = e.target.closest('.tab-item');
+      if (!tabItem) return;
+      // Don't switch tab if clicking the menu button
+      if (e.target.closest('.tab-menu-btn')) return;
+      const tabId = tabItem.getAttribute('data-tab-id');
+      if (tabId) switchTab(tabId);
+    };
+
+
+    // Auto-scroll active tab into view (paint-aligned to prevent forced reflows)
+    const activeItem = tabList.querySelector('.tab-item.active');
+    if (activeItem) {
+      requestAnimationFrame(function() {
+        activeItem.scrollIntoView({ block: 'nearest', inline: 'nearest' });
+      });
+    }
+
+    // Arrow-key keyboard navigation inside tabList (WAI-ARIA compliance with manual selection)
+    tabList.onkeydown = function(e) {
+      const items = Array.from(tabList.querySelectorAll('.tab-item'));
+      if (items.length === 0) return;
+      
+      const focusedItem = document.activeElement.closest('.tab-item');
+      if (!focusedItem) return;
+      
+      const activeIdx = items.indexOf(focusedItem);
+      if (activeIdx === -1) return;
+      
+      let targetIdx = -1;
+      if (e.key === 'ArrowRight') {
+        targetIdx = (activeIdx + 1) % items.length;
+      } else if (e.key === 'ArrowLeft') {
+        targetIdx = (activeIdx - 1 + items.length) % items.length;
+      } else if (e.key === 'Home') {
+        targetIdx = 0;
+      } else if (e.key === 'End') {
+        targetIdx = items.length - 1;
+      } else if (e.key === 'Enter' || e.key === ' ') {
+        e.preventDefault();
+        const tabId = focusedItem.getAttribute('data-tab-id');
+        switchTab(tabId);
+        // After switch tab, focus the active item in the newly rendered tab bar
+        requestAnimationFrame(function() {
+          const newActive = tabList.querySelector(`.tab-item[data-tab-id="${tabId}"]`);
+          if (newActive) newActive.focus();
+        });
+        return;
+      }
+      
+      if (targetIdx !== -1) {
+        e.preventDefault();
+        // Update roving tabindex focus without triggering heavy re-renders
+        items.forEach(function(item, idx) {
+          if (idx === targetIdx) {
+            item.setAttribute('tabindex', '0');
+            item.focus();
+          } else {
+            item.setAttribute('tabindex', '-1');
+          }
+        });
+      }
+    };
+
+    renderMobileTabList(tabsArr, currentActiveTabId);
+    if (typeof tabList.dispatchEvent === 'function') {
+      tabList.dispatchEvent(new Event('scroll'));
+    }
+  }
+
+  // ========================================
+  // TAB OVERFLOW — Scroll Buttons, Wheel, Indicators
+  // ========================================
+
+  var _tabOverflowInitialized = false;
+
+  function setupTabOverflow() {
+    if (_tabOverflowInitialized) return;
+    _tabOverflowInitialized = true;
+
+    var tabBar = document.getElementById('tab-bar');
+    var tabList = document.getElementById('tab-list');
+    if (!tabBar || !tabList) return;
+
+    // --- Create scroll arrow buttons ---
+    var scrollLeftBtn = document.createElement('button');
+    scrollLeftBtn.className = 'tab-scroll-btn tab-scroll-left';
+    scrollLeftBtn.setAttribute('aria-label', 'Scroll tabs left');
+    scrollLeftBtn.title = 'Scroll left';
+    scrollLeftBtn.innerHTML = '<i class="bi bi-chevron-left"></i>';
+    scrollLeftBtn.addEventListener('click', function() {
+      tabList.scrollBy({ left: -200, behavior: 'smooth' });
+    });
+
+    var scrollRightBtn = document.createElement('button');
+    scrollRightBtn.className = 'tab-scroll-btn tab-scroll-right';
+    scrollRightBtn.setAttribute('aria-label', 'Scroll tabs right');
+    scrollRightBtn.title = 'Scroll right';
+    scrollRightBtn.innerHTML = '<i class="bi bi-chevron-right"></i>';
+    scrollRightBtn.addEventListener('click', function() {
+      tabList.scrollBy({ left: 200, behavior: 'smooth' });
+    });
+
+    // Insert scroll buttons flanking the tab-list
+    tabBar.insertBefore(scrollLeftBtn, tabList);
+    var newBtn = document.getElementById('tab-new-btn');
+    if (newBtn) {
+      tabBar.insertBefore(scrollRightBtn, newBtn);
+    } else {
+      var resetBtn = document.getElementById('tab-reset-btn');
+      if (resetBtn) {
+        tabBar.insertBefore(scrollRightBtn, resetBtn);
+      } else {
+        tabBar.appendChild(scrollRightBtn);
+      }
+    }
+
+    // --- Overflow detection ---
+    var _overflowRafId = null;
+    function updateOverflowState() {
+      if (_overflowRafId) return;
+      _overflowRafId = requestAnimationFrame(function() {
+        _overflowRafId = null;
+        var hasLeft = tabList.scrollLeft > 1;
+        var hasRight = tabList.scrollLeft < (tabList.scrollWidth - tabList.clientWidth - 1);
+        tabBar.classList.toggle('has-overflow-left', hasLeft);
+        tabBar.classList.toggle('has-overflow-right', hasRight);
+      });
+    }
+
+    tabList.addEventListener('scroll', updateOverflowState);
+
+    // Use ResizeObserver to detect when overflow state changes due to window resize
+    if (typeof ResizeObserver !== 'undefined') {
+      var resizeObs = new ResizeObserver(updateOverflowState);
+      resizeObs.observe(tabList);
+    }
+
+    // Initial check
+    updateOverflowState();
+
+    // --- Mouse wheel scroll: vertical wheel → horizontal scroll ---
+    tabList.addEventListener('wheel', function(e) {
+      // Only intercept vertical wheel (don't fight native horizontal wheel/trackpad)
+      if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
+        e.preventDefault();
+        tabList.scrollLeft += e.deltaY;
+        updateOverflowState();
+      }
+    }, { passive: false });
+  }
+
+  function renderMobileTabList(tabsArr, currentActiveTabId) {
+    const mobileTabList = document.getElementById('mobile-tab-list');
+    if (!mobileTabList) return;
+    // PERF-007: Clear element content using textContent instead of innerHTML
+    mobileTabList.textContent = '';
+    tabsArr.forEach(function(tab) {
+      const item = document.createElement('div');
+      item.className = 'mobile-tab-item' + (tab.id === currentActiveTabId ? ' active' : '');
+      item.setAttribute('role', 'tab');
+      item.setAttribute('aria-selected', tab.id === currentActiveTabId ? 'true' : 'false');
+      item.setAttribute('data-tab-id', tab.id);
+
+      const titleSpan = document.createElement('span');
+      titleSpan.className = 'mobile-tab-title';
+      titleSpan.textContent = tab.title || 'Untitled';
+      titleSpan.title = tab.title || 'Untitled';
+
+      const tabMenu = createTabActionMenu(tab, {
+        isMobileMenu: true,
+        menuIdPrefix: 'mobile-tab-menu'
+      });
+
+      item.appendChild(titleSpan);
+      item.appendChild(tabMenu.button);
+
+      item.addEventListener('click', function() {
+        switchTab(tab.id);
+        closeMobileMenu();
+      });
+
+      mobileTabList.appendChild(item);
+    });
+  }
+
+  // Close any open tab dropdown when clicking elsewhere in the document
+  document.addEventListener('click', function() {
+    closeTabMenus();
+  });
+
+  function saveCurrentTabState() {
+    const tab = tabs.find(function(t) { return t.id === activeTabId; });
+    if (!tab) return;
+    tab.content = markdownEditor.value;
+    tab.scrollPos = markdownEditor.scrollTop;
+    tab.viewMode = currentViewMode || 'split';
+    saveTabsToStorage(tabs);
+  }
+
+  function restoreViewMode(mode) {
+    currentViewMode = null;
+    setViewMode(mode || 'split');
+  }
+
+  function switchTab(tabId) {
+    if (tabId === activeTabId) return;
+    saveCurrentTabState();
+    
+    // Clear typing timeout and reset tracking for the new tab
+    if (typingTimeout) {
+      clearTimeout(typingTimeout);
+      typingTimeout = null;
+    }
+    lastInputType = null;
+    pendingState = null;
+    
+    activeTabId = tabId;
+    saveActiveTabId(activeTabId);
+    const tab = tabs.find(function(t) { return t.id === tabId; });
+    if (!tab) return;
+    markdownEditor.value = tab.content;
+    
+    initTabHistory(tabId, tab.content);
+    lastPushedValue = tab.content;
+    currentHistoryTabId = tabId;
+    updateUndoRedoButtons();
+    
+    restoreViewMode(tab.viewMode);
+    renderMarkdown();
+    requestAnimationFrame(function() {
+      markdownEditor.scrollTop = tab.scrollPos || 0;
+    });
+    renderTabBar(tabs, activeTabId);
+  }
+
+  function newTab(content, title) {
+    if (content === undefined) content = '';
+    if (tabs.length >= 20) {
+      alert('Maximum of 20 tabs reached. Please close an existing tab to open a new one.');
+      return;
+    }
+    if (!title) title = nextUntitledTitle();
+    const tab = createTab(content, title);
+    tabs.push(tab);
+    switchTab(tab.id);
+    markdownEditor.focus();
+  }
+
+  function closeTab(tabId) {
+    const idx = tabs.findIndex(function(t) { return t.id === tabId; });
+    if (idx === -1) return;
+    
+    // Clean up history of the closed tab
+    if (tabHistories[tabId]) {
+      delete tabHistories[tabId];
+    }
+    
+    tabs.splice(idx, 1);
+    if (tabs.length === 0) {
+      // Auto-create new "Untitled" when last tab is deleted
+      const newT = createTab('', nextUntitledTitle());
+      tabs.push(newT);
+      activeTabId = newT.id;
+      saveActiveTabId(activeTabId);
+      markdownEditor.value = '';
+      restoreViewMode('split');
+      renderMarkdown();
+    } else if (activeTabId === tabId) {
+      const newIdx = Math.max(0, idx - 1);
+      activeTabId = tabs[newIdx].id;
+      saveActiveTabId(activeTabId);
+      const newActiveTab = tabs[newIdx];
+      markdownEditor.value = newActiveTab.content;
+      restoreViewMode(newActiveTab.viewMode);
+      renderMarkdown();
+      requestAnimationFrame(function() {
+        markdownEditor.scrollTop = newActiveTab.scrollPos || 0;
+      });
+    }
+    saveTabsToStorage(tabs);
+    renderTabBar(tabs, activeTabId);
+  }
+
+  function deleteTab(tabId) {
+    closeTab(tabId);
+  }
+
+  function renameTab(tabId) {
+    const tab = tabs.find(function(t) { return t.id === tabId; });
+    if (!tab) return;
+    const modal = document.getElementById('rename-modal');
+    const input = document.getElementById('rename-modal-input');
+    const confirmBtn = document.getElementById('rename-modal-confirm');
+    const cancelBtn = document.getElementById('rename-modal-cancel');
+    if (!modal || !input) return;
+    input.value = tab.title;
+
+    function doRename() {
+      const newName = input.value.trim();
+      if (newName) {
+        tab.title = newName;
+        saveTabsToStorage(tabs);
+        renderTabBar(tabs, activeTabId);
+      }
+      closeAppModal(modal);
+      cleanup();
+    }
+
+    function doCancel() {
+      closeAppModal(modal);
+      cleanup();
+    }
+
+    function onKey(e) {
+      if (e.key === 'Enter') doRename();
+    }
+
+    function cleanup() {
+      confirmBtn.removeEventListener('click', doRename);
+      cancelBtn.removeEventListener('click', doCancel);
+      input.removeEventListener('keydown', onKey);
+    }
+
+    confirmBtn.addEventListener('click', doRename);
+    cancelBtn.addEventListener('click', doCancel);
+    input.addEventListener('keydown', onKey);
+
+    openAppModal(modal, {
+      focusTarget: input,
+      onClose: doCancel
+    });
+    input.select();
+  }
+
+  function duplicateTab(tabId) {
+    const tab = tabs.find(function(t) { return t.id === tabId; });
+    if (!tab) return;
+    if (tabs.length >= 20) {
+      alert('Maximum of 20 tabs reached. Please close an existing tab to open a new one.');
+      return;
+    }
+    const shouldSwitchToDuplicate = tabId === activeTabId;
+    saveCurrentTabState();
+    const dupTitle = tab.title + ' (copy)';
+    const dup = createTab(tab.content, dupTitle, tab.viewMode);
+    const idx = tabs.findIndex(function(t) { return t.id === tabId; });
+    tabs.splice(idx + 1, 0, dup);
+    if (shouldSwitchToDuplicate) {
+      switchTab(dup.id);
+    } else {
+      saveTabsToStorage(tabs);
+      renderTabBar(tabs, activeTabId);
+    }
+  }
+
+  function resetAllTabs() {
+    const modal = document.getElementById('reset-confirm-modal');
+    const confirmBtn = document.getElementById('reset-modal-confirm');
+    const cancelBtn = document.getElementById('reset-modal-cancel');
+    if (!modal) return;
+
+    function doReset() {
+      closeAppModal(modal);
+      cleanup();
+      tabs = [];
+      untitledCounter = 0;
+      saveUntitledCounter(0);
+      const welcome = createTab(sampleMarkdown, 'Welcome to Markdown');
+      tabs.push(welcome);
+      activeTabId = welcome.id;
+      saveActiveTabId(activeTabId);
+      saveTabsToStorage(tabs);
+      markdownEditor.value = sampleMarkdown;
+      restoreViewMode('split');
+      renderMarkdown();
+      renderTabBar(tabs, activeTabId);
+    }
+
+    function doCancel() {
+      closeAppModal(modal);
+      cleanup();
+    }
+
+    function cleanup() {
+      confirmBtn.removeEventListener('click', doReset);
+      cancelBtn.removeEventListener('click', doCancel);
+    }
+
+    confirmBtn.addEventListener('click', doReset);
+    cancelBtn.addEventListener('click', doCancel);
+
+    openAppModal(modal, {
+      focusTarget: confirmBtn,
+      onClose: doCancel
+    });
+  }
+
+  function initTabs() {
+    untitledCounter = loadUntitledCounter();
+    tabs = loadTabsFromStorage();
+    activeTabId = loadActiveTabId();
+
+    // Check if Neutralino passed an initial file via command line (early load)
+    if (window.NL_INITIAL_FILE_CONTENT) {
+      const initialFile = window.NL_INITIAL_FILE_CONTENT;
+      const tab = createTab(initialFile.content, initialFile.name);
+      tabs.push(tab);
+      activeTabId = tab.id;
+      saveTabsToStorage(tabs);
+      saveActiveTabId(activeTabId);
+      delete window.NL_INITIAL_FILE_CONTENT;
+    } else if (tabs.length === 0) {
+      const tab = createTab(sampleMarkdown, 'Welcome to Markdown');
+      tabs.push(tab);
+      activeTabId = tab.id;
+      saveTabsToStorage(tabs);
+      saveActiveTabId(activeTabId);
+    } else if (!tabs.find(function(t) { return t.id === activeTabId; })) {
+      activeTabId = tabs[0].id;
+      saveActiveTabId(activeTabId);
+    }
+    const activeTab = tabs.find(function(t) { return t.id === activeTabId; });
+    markdownEditor.value = activeTab.content;
+    initTabHistory(activeTabId, activeTab.content);
+    updateUndoRedoButtons();
+    restoreViewMode(activeTab.viewMode);
+    renderMarkdown();
+    const editorPane = document.querySelector('.editor-pane');
+    if (editorPane) {
+      editorPane.classList.remove('is-loading');
+    }
+    requestAnimationFrame(function() {
+      markdownEditor.scrollTop = activeTab.scrollPos || 0;
+    });
+    renderTabBar(tabs, activeTabId);
+    setupTabOverflow();
+
+    const staticNewBtn = document.getElementById('tab-new-btn');
+    if (staticNewBtn) {
+      staticNewBtn.onclick = function() {
+        newTab();
+      };
+    }
+  }
+
+  // Late-load callback hook for Neutralino command-line files
+  window.NL_IMPORT_EXTERNAL_FILE = function(content, name) {
+    if (typeof tabs === 'undefined') return;
+    const existing = tabs.find(function(t) { return t.title === name && t.content === content; });
+    if (existing) {
+      switchTab(existing.id);
+      return;
+    }
+    newTab(content, name);
+  };
+
+  function showPreviewSkeleton() {
+    if (markdownPreview && !markdownPreview.querySelector('#markdown-preview-skeleton')) {
+      markdownPreview.setAttribute('aria-busy', 'true');
+      markdownPreview.dataset.renderState = 'loading';
+      markdownPreview.innerHTML = `
+        <div class="skeleton-preview-container" id="markdown-preview-skeleton" aria-hidden="true">
+            <div class="skeleton-placeholder skeleton-title"></div>
+            <div class="skeleton-placeholder skeleton-line skeleton-w90"></div>
+            <div class="skeleton-placeholder skeleton-line skeleton-w85"></div>
+            <div class="skeleton-placeholder skeleton-line skeleton-w60"></div>
+            
+            <div class="skeleton-placeholder skeleton-subtitle"></div>
+            <div class="skeleton-placeholder skeleton-line skeleton-w88"></div>
+            <div class="skeleton-placeholder skeleton-line skeleton-w92"></div>
+            <div class="skeleton-placeholder skeleton-line skeleton-w45"></div>
+        </div>
+      `;
+    }
+  }
+
+  function previewContainsSkeleton() {
+    return Boolean(markdownPreview && markdownPreview.querySelector('#markdown-preview-skeleton'));
+  }
+
+  function getActivePreviewDocumentId() {
+    return activeTabId || '__single-document__';
+  }
+
+  function clearPendingPreviewWork() {
+    if (pendingPreviewRenderCancel) {
+      pendingPreviewRenderCancel();
+      pendingPreviewRenderCancel = null;
+    }
+  }
+
+  function getPreviewRenderDelay(markdown) {
+    const length = markdown.length;
+    if (length >= HUGE_DOCUMENT_THRESHOLD) return HUGE_RENDER_DELAY;
+    if (length >= LARGE_DOCUMENT_THRESHOLD) return LARGE_RENDER_DELAY;
+    return RENDER_DELAY;
+  }
+
+  function getEditorWorkDelay(markdown) {
+    const length = markdown.length;
+    if (length >= HUGE_DOCUMENT_THRESHOLD) return HUGE_EDITOR_WORK_DELAY;
+    if (length >= LARGE_DOCUMENT_THRESHOLD) return LARGE_EDITOR_WORK_DELAY;
+    return 0;
+  }
+
+  function isEditorVisible() {
+    return currentViewMode === 'editor' || currentViewMode === 'split';
+  }
+
+  function deferPreviewWork(callback, rawLength) {
+    let cancelled = false;
+    let rafId = null;
+    let idleId = null;
+    let timeoutId = null;
+
+    rafId = requestAnimationFrame(function() {
+      rafId = null;
+      if (cancelled) return;
+
+      if ('requestIdleCallback' in window) {
+        idleId = window.requestIdleCallback(function() {
+          idleId = null;
+          if (!cancelled) callback();
+        }, { timeout: rawLength >= HUGE_DOCUMENT_THRESHOLD ? 700 : 350 });
+      } else {
+        timeoutId = setTimeout(function() {
+          timeoutId = null;
+          if (!cancelled) callback();
+        }, 0);
+      }
+    });
+
+    return function cancelDeferredPreviewWork() {
+      cancelled = true;
+      if (rafId !== null) cancelAnimationFrame(rafId);
+      if (idleId !== null && 'cancelIdleCallback' in window) window.cancelIdleCallback(idleId);
+      if (timeoutId !== null) clearTimeout(timeoutId);
+    };
+  }
+
+  function capturePreviewScroll() {
+    if (!previewPane) return null;
+    return {
+      top: previewPane.scrollTop,
+      left: previewPane.scrollLeft,
+    };
+  }
+
+  function restorePreviewScroll(snapshot) {
+    if (!snapshot || !previewPane) return;
+    requestAnimationFrame(function() {
+      const maxTop = Math.max(0, previewPane.scrollHeight - previewPane.clientHeight);
+      previewPane.scrollTop = Math.min(maxTop, snapshot.top);
+      previewPane.scrollLeft = snapshot.left;
+    });
+  }
+
+  function canReusePreviewNode(currentNode, nextNode, options) {
+    if (!currentNode || !nextNode || currentNode.nodeType !== nextNode.nodeType) return false;
+
+    if (currentNode.nodeType === Node.TEXT_NODE) {
+      if (currentNode.nodeValue !== nextNode.nodeValue) {
+        currentNode.nodeValue = nextNode.nodeValue;
+      }
+      return true;
+    }
+
+    if (currentNode.nodeType !== Node.ELEMENT_NODE) {
+      return currentNode.nodeValue === nextNode.nodeValue;
+    }
+
+    if (currentNode.nodeName !== nextNode.nodeName) return false;
+
+    const currentEl = currentNode;
+    const nextEl = nextNode;
+    if ((currentEl.id || nextEl.id) && currentEl.id !== nextEl.id) return false;
+
+    if (
+      options &&
+      options.reusePreviewBlocks &&
+      currentEl.dataset &&
+      nextEl.dataset &&
+      currentEl.dataset.previewBlockHash &&
+      currentEl.dataset.previewBlockHash === nextEl.dataset.previewBlockHash
+    ) {
+      return true;
+    }
+
+    if (currentEl.outerHTML === nextEl.outerHTML) return true;
+
+    if (currentEl.tagName === 'DETAILS' && nextEl.tagName === 'DETAILS' && currentEl.hasAttribute('open')) {
+      nextEl.setAttribute('open', '');
+    }
+
+    return false;
+  }
+
+  function patchPreviewDom(container, html, options) {
+    const result = {
+      fullReplace: false,
+      updatedNodes: [],
+    };
+
+    if (!previewHasCommittedRender || previewContainsSkeleton()) {
+      container.innerHTML = html;
+      result.fullReplace = true;
+      result.updatedNodes = [container];
+      return result;
+    }
+
+    const template = document.createElement('template');
+    template.innerHTML = html;
+    const nextNodes = Array.from(template.content.childNodes);
+    const currentNodeCount = container.childNodes.length;
+
+    if (nextNodes.length > 6000 || currentNodeCount > 6000) {
+      container.replaceChildren(...nextNodes);
+      result.fullReplace = true;
+      result.updatedNodes = [container];
+      return result;
+    }
+
+    let index = 0;
+    while (index < nextNodes.length || index < container.childNodes.length) {
+      const currentNode = container.childNodes[index];
+      const nextNode = nextNodes[index];
+
+      if (!nextNode) {
+        currentNode.remove();
+        continue;
+      }
+
+      if (!currentNode) {
+        container.appendChild(nextNode);
+        result.updatedNodes.push(nextNode);
+        index += 1;
+        continue;
+      }
+
+      if (canReusePreviewNode(currentNode, nextNode, options)) {
+        index += 1;
+        continue;
+      }
+
+      result.updatedNodes.push(nextNode);
+      currentNode.replaceWith(nextNode);
+      index += 1;
+    }
+
+    return result;
+  }
+
+  function commitPreviewHtml(sanitizedHtml, referenceData, rawVal, context) {
+    const shouldRestoreScroll = previewHasCommittedRender && !previewContainsSkeleton();
+    const scrollSnapshot = shouldRestoreScroll ? capturePreviewScroll() : null;
+
+    const patchResult = patchPreviewDom(markdownPreview, sanitizedHtml, {
+      reusePreviewBlocks: context.previewEngineMode === 'segmented' && !context.force,
+    });
+    applyReferencePreviewLinks(markdownPreview, referenceData.definitions);
+    enhanceGitHubAlerts(markdownPreview);
+
+    _lastRenderedContent = rawVal;
+    previewHasCommittedRender = true;
+    previewLastRenderedTabId = context.previewDocumentId;
+    markdownPreview.removeAttribute('aria-busy');
+    markdownPreview.dataset.renderState = 'ready';
+
+    restorePreviewScroll(scrollSnapshot);
+    return patchResult;
+  }
+
+  function getPreviewPostProcessRoots(patchResult) {
+    if (!patchResult || patchResult.fullReplace || !patchResult.updatedNodes || patchResult.updatedNodes.length === 0) {
+      return [markdownPreview];
+    }
+
+    const roots = [];
+    const seen = new Set();
+    patchResult.updatedNodes.forEach(function(node) {
+      const root = node && node.nodeType === Node.ELEMENT_NODE ? node : (node && node.parentElement);
+      if (root && !seen.has(root)) {
+        seen.add(root);
+        roots.push(root);
+      }
+    });
+
+    return roots.length ? roots : [markdownPreview];
+  }
+
+  function queryPreviewRoots(roots, selector) {
+    const matches = [];
+    const seen = new Set();
+    roots.forEach(function(root) {
+      if (!root || root.nodeType !== Node.ELEMENT_NODE) return;
+      if (root.matches && root.matches(selector) && !seen.has(root)) {
+        seen.add(root);
+        matches.push(root);
+      }
+      root.querySelectorAll(selector).forEach(function(node) {
+        if (!seen.has(node)) {
+          seen.add(node);
+          matches.push(node);
+        }
+      });
+    });
+    return matches;
+  }
+
+  function markPreviewRootsReady(roots) {
+    queryPreviewRoots(roots, '.mermaid-container.is-loading').forEach(function(container) {
+      container.classList.remove('is-loading');
+    });
+  }
+
+  function postProcessPreview(rawVal, context, patchResult) {
+    const roots = getPreviewPostProcessRoots(patchResult);
+
+    roots.forEach(function(root) {
+      processEmojis(root);
+    });
+
+    queryPreviewRoots(roots, 'input[type="checkbox"]').forEach(function(input) {
+      if (!input.hasAttribute('aria-label')) {
+        const parentText = input.parentElement ? input.parentElement.textContent.trim() : '';
+        input.setAttribute('aria-label', parentText || 'Task item');
+      }
+    });
+
+    try {
+      const mermaidNodes = queryPreviewRoots(roots, '.mermaid');
+      if (mermaidNodes.length > 0) {
+        const renderMermaidNodes = function() {
+          if (context.renderId !== previewRenderGeneration) return;
+          initMermaid(false);
+          Promise.resolve(mermaid.init(undefined, mermaidNodes))
+            .then(() => {
+              if (context.renderId !== previewRenderGeneration) return;
+              markPreviewRootsReady(roots);
+              addMermaidToolbars();
+            })
+            .catch((e) => {
+              if (context.renderId !== previewRenderGeneration) return;
+              console.warn("Mermaid rendering failed:", e);
+              markPreviewRootsReady(roots);
+              addMermaidToolbars();
+            });
+        };
+        if (typeof mermaid === 'undefined') {
+          loadScript(CDN.mermaid).then(function() {
+            if (context.renderId !== previewRenderGeneration) return;
+            initMermaid(true);
+            renderMermaidNodes();
+          }).catch(function(e) { console.warn('Failed to load mermaid:', e); });
+        } else {
+          renderMermaidNodes();
+        }
+      }
+    } catch (e) {
+      console.warn("Mermaid rendering failed:", e);
+    }
+
+    const hasMath = /\$\$|\$[^$]|\\\(|\\\[/.test(rawVal || '');
+    if (hasMath) {
+      const typesetTargets = roots.filter(function(root) {
+        return root && root.nodeType === Node.ELEMENT_NODE && /\$\$|\$[^$]|\\\(|\\\[/.test(root.textContent || '');
+      });
+      const mathTargets = typesetTargets.length ? typesetTargets : roots;
+      if (window.MathJax) {
+        try {
+          MathJax.typesetPromise(mathTargets).then(function() {
+            if (context.renderId !== previewRenderGeneration) return;
+            queryPreviewRoots(mathTargets, 'mjx-container[tabindex="0"]').forEach(function(mjx) {
+              mjx.removeAttribute('tabindex');
+            });
+          }).catch(function(err) {
+            console.warn('MathJax typesetting failed:', err);
+          });
+        } catch (e) {
+          console.warn("MathJax rendering failed:", e);
+        }
+      } else {
+        window.MathJax = {
+          loader: { load: ['[tex]/ams', '[tex]/boldsymbol'] },
+          options: {
+            a11y: { inTabOrder: false }
+          },
+          tex: {
+            inlineMath: [['$', '$'], ['\\(', '\\)']],
+            displayMath: [['$$', '$$'], ['\\[', '\\]']],
+            processEscapes: true,
+            packages: { '[+]': ['ams', 'boldsymbol'] }
+          }
+        };
+        loadScript(CDN.mathjax).then(function() {
+          if (context.renderId !== previewRenderGeneration) return;
+          try {
+            MathJax.typesetPromise(mathTargets).then(function() {
+              if (context.renderId !== previewRenderGeneration) return;
+              queryPreviewRoots(mathTargets, 'mjx-container[tabindex="0"]').forEach(function(mjx) {
+                mjx.removeAttribute('tabindex');
+              });
+            }).catch(function(err) {
+              console.warn('MathJax typesetting failed:', err);
+            });
+          } catch (e) {
+            console.warn('MathJax rendering failed:', e);
+          }
+        }).catch(function(e) { console.warn('Failed to load MathJax:', e); });
+      }
+    }
+
+    updateDocumentStats();
+    updateFindHighlights();
+    cleanupImageObjectUrls();
+    scheduleLineNumberUpdate();
+  }
+
+  function executeMainThreadRender(rawVal, context) {
+    const { frontmatter, body } = parseFrontmatter(rawVal);
+    const tableHtml = frontmatter ? renderFrontmatterTable(frontmatter) : '';
+    const referenceData = extractReferenceDefinitions(body);
+    const html = tableHtml + marked.parse(referenceData.cleanedMarkdown);
+    const sanitizedHtml = sanitizePreviewHtml(html);
+
+    if (context.renderId !== previewRenderGeneration || markdownEditor.value !== rawVal) return;
+
+    previewSegmentHtmlCache.clear();
+    const patchResult = commitPreviewHtml(sanitizedHtml, referenceData, rawVal, context);
+    postProcessPreview(rawVal, context, patchResult);
+  }
+
+  function executeWorkerRender(rawVal, context) {
+    requestPreviewWorkerRender(rawVal, context)
+      .then(function(result) {
+        if (context.renderId !== previewRenderGeneration || markdownEditor.value !== rawVal) return;
+        if (!result || result.mode !== 'segmented' || !Array.isArray(result.blocks) || result.blocks.length < PREVIEW_SEGMENT_MIN_BLOCKS) {
+          executeMainThreadRender(rawVal, Object.assign({}, context, { disableWorker: true }));
+          return;
+        }
+
+        const segmentedHtml = buildSegmentedPreviewHtml(result.blocks, context.previewDocumentId);
+        if (context.renderId !== previewRenderGeneration || markdownEditor.value !== rawVal) return;
+
+        const patchResult = commitPreviewHtml(segmentedHtml, { definitions: new Map() }, rawVal, Object.assign({}, context, {
+          previewEngineMode: 'segmented',
+        }));
+        postProcessPreview(rawVal, context, patchResult);
+      })
+      .catch(function(error) {
+        if (context.renderId !== previewRenderGeneration || markdownEditor.value !== rawVal) return;
+        console.warn('Preview worker unavailable; falling back to main-thread renderer:', error);
+        executeMainThreadRender(rawVal, Object.assign({}, context, { disableWorker: true }));
+      });
+  }
+
+  function renderMarkdown(options) {
+    options = options || {};
+    const rawVal = markdownEditor.value;
+    const force = options.force === true;
+    const previewDocumentId = getActivePreviewDocumentId();
+    const hasCurrentPreview =
+      previewHasCommittedRender &&
+      previewLastRenderedTabId === previewDocumentId &&
+      _lastRenderedContent === rawVal &&
+      !previewContainsSkeleton();
+
+    if (hasCurrentPreview && !force) return;
+
+    clearPendingPreviewWork();
+    const renderId = ++previewRenderGeneration;
+    const isLargeDocument = rawVal.length >= LARGE_DOCUMENT_THRESHOLD;
+    const isDocumentSwap = previewHasCommittedRender && previewLastRenderedTabId !== previewDocumentId;
+    const needsInitialPreview = !previewHasCommittedRender || previewContainsSkeleton();
+    const shouldShowSkeleton =
+      isLargeDocument &&
+      (options.showSkeleton === true || needsInitialPreview || isDocumentSwap);
+
+    if (shouldShowSkeleton) {
+      showPreviewSkeleton();
+    } else if (markdownPreview) {
+      markdownPreview.setAttribute('aria-busy', 'true');
+      markdownPreview.dataset.renderState = 'refreshing';
+    }
+
+    const runRender = function() {
+      pendingPreviewRenderCancel = null;
+      if (renderId !== previewRenderGeneration || markdownEditor.value !== rawVal) return;
+      executeRender(rawVal, {
+        force,
+        renderId,
+        previewDocumentId,
+        reason: options.reason || 'direct',
+      });
+    };
+
+    if (isLargeDocument) {
+      pendingPreviewRenderCancel = deferPreviewWork(runRender, rawVal.length);
+    } else {
+      runRender();
+    }
+  }
+
+  function executeRender(rawVal, context) {
+    context = context || {};
+    // PERF-003: Skip render if content hasn't changed
+    if (
+      !context.force &&
+      rawVal === _lastRenderedContent &&
+      previewLastRenderedTabId === context.previewDocumentId &&
+      previewHasCommittedRender &&
+      !previewContainsSkeleton()
+    ) {
+      markdownPreview.removeAttribute('aria-busy');
+      markdownPreview.dataset.renderState = 'ready';
+      return;
+    }
+
+    try {
+      if (shouldUsePreviewWorker(rawVal, context)) {
+        executeWorkerRender(rawVal, context);
+      } else {
+        executeMainThreadRender(rawVal, context);
+      }
+    } catch (e) {
+      console.error("Markdown rendering failed:", e);
+      const safeMessage = escapeHtml(e && e.message ? e.message : 'Unknown error');
+      const safeMarkdown = escapeHtml(rawVal);
+      markdownPreview.removeAttribute('aria-busy');
+      markdownPreview.dataset.renderState = 'error';
+      if (!previewHasCommittedRender || previewContainsSkeleton()) {
+        markdownPreview.innerHTML = `<div class="alert alert-danger">
+              <strong>Error rendering markdown:</strong> ${safeMessage}
+          </div>
+          <pre>${safeMarkdown}</pre>`;
+      }
+    }
+  }
+
+  function importMarkdownFile(file) {
+    if (file.size > 10 * 1024 * 1024) {
+      alert('File is too large (maximum 10MB supported).');
+      return;
+    }
+
+    const reader = new FileReader();
+    reader.onload = function(e) {
+      const text = e.target.result || '';
+      
+      // Simple binary check: look for null bytes in the first 8KB
+      const checkLength = Math.min(text.length, 8000);
+      for (let i = 0; i < checkLength; i++) {
+        if (text.charCodeAt(i) === 0) {
+          alert('Cannot import: The selected file appears to be a binary file.');
+          return;
+        }
+      }
+
+      newTab(text, file.name.replace(/\.md$/i, ''));
+    };
+    reader.onerror = function() {
+      alert('Failed to read the file. Please check permissions and try again.');
+    };
+    reader.readAsText(file);
+  }
+
+  function isMarkdownPath(path) {
+    return /\.(md|markdown)$/i.test(path || "");
+  }
+  const MAX_GITHUB_FILES_SHOWN = 30;
+  const GITHUB_IMPORT_MIN_REQUEST_INTERVAL_MS = 800;
+  let lastGitHubImportRequestAt = 0;
+  const selectedGitHubImportPaths = new Set();
+  let availableGitHubImportPaths = [];
+
+  function getFileName(path) {
+    return (path || "").split("/").pop() || "document.md";
+  }
+
+  function buildRawGitHubUrl(owner, repo, ref, filePath) {
+    const encodedPath = filePath
+      .split("/")
+      .map((part) => encodeURIComponent(part))
+      .join("/");
+    return `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(ref)}/${encodedPath}`;
+  }
+
+  async function fetchGitHubJson(url) {
+    const now = Date.now();
+    const waitTime = GITHUB_IMPORT_MIN_REQUEST_INTERVAL_MS - (now - lastGitHubImportRequestAt);
+    if (waitTime > 0) {
+      await new Promise((resolve) => setTimeout(resolve, waitTime));
+    }
+    lastGitHubImportRequestAt = Date.now();
+    const response = await fetch(url, {
+      headers: {
+        Accept: "application/vnd.github+json"
+      }
+    });
+    if (!response.ok) {
+      throw new Error(`GitHub API request failed (${response.status})`);
+    }
+    return response.json();
+  }
+
+  async function fetchTextContent(url) {
+    const response = await fetch(url);
+    if (!response.ok) {
+      throw new Error(`Failed to fetch file (${response.status})`);
+    }
+    return response.text();
+  }
+
+  function parseGitHubImportUrl(input) {
+    let parsedUrl;
+    try {
+      parsedUrl = new URL((input || "").trim());
+    } catch (_) {
+      return null;
+    }
+
+    const host = parsedUrl.hostname.replace(/^www\./, "");
+    const segments = parsedUrl.pathname.split("/").filter(Boolean);
+
+    if (host === "raw.githubusercontent.com") {
+      if (segments.length < 4) return null;
+      const [owner, repo, ref, ...rest] = segments;
+      const filePath = rest.join("/");
+      return { owner, repo, ref, type: "file", filePath };
+    }
+
+    if (host !== "github.com" || segments.length < 2) return null;
+
+    const owner = segments[0];
+    const repo = segments[1].replace(/\.git$/i, "");
+    if (segments.length === 2) {
+      return { owner, repo, type: "repo" };
+    }
+
+    const mode = segments[2];
+    if (mode === "blob" && segments.length >= 5) {
+      return {
+        owner,
+        repo,
+        type: "file",
+        ref: segments[3],
+        filePath: segments.slice(4).join("/")
+      };
+    }
+
+    if (mode === "tree" && segments.length >= 4) {
+      return {
+        owner,
+        repo,
+        type: "tree",
+        ref: segments[3],
+        basePath: segments.slice(4).join("/")
+      };
+    }
+
+    return { owner, repo, type: "repo" };
+  }
+
+  async function getDefaultBranch(owner, repo) {
+    const repoInfo = await fetchGitHubJson(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`);
+    return repoInfo.default_branch;
+  }
+
+  async function listMarkdownFiles(owner, repo, ref, basePath) {
+    const treeResponse = await fetchGitHubJson(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(ref)}?recursive=1`);
+    const normalizedBasePath = (basePath || "").replace(/^\/+|\/+$/g, "");
+
+    return (treeResponse.tree || [])
+      .filter((entry) => entry.type === "blob" && isMarkdownPath(entry.path))
+      .filter((entry) => !normalizedBasePath || entry.path === normalizedBasePath || entry.path.startsWith(normalizedBasePath + "/"))
+      .map((entry) => entry.path)
+      .sort((a, b) => a.localeCompare(b));
+  }
+
+  function buildMarkdownFileTree(paths) {
+    const root = { folders: {}, files: [] };
+    (paths || []).forEach((path) => {
+      const segments = (path || "").split("/").filter(Boolean);
+      if (!segments.length) return;
+      const fileName = segments.pop();
+      let node = root;
+      segments.forEach((segment) => {
+        if (!node.folders[segment]) {
+          node.folders[segment] = { folders: {}, files: [] };
+        }
+        node = node.folders[segment];
+      });
+      node.files.push({ name: fileName, path });
+    });
+    return root;
+  }
+
+  function updateGitHubImportSelectedCount() {
+    if (!githubImportSelectedCount) return;
+    const count = selectedGitHubImportPaths.size;
+    githubImportSelectedCount.textContent = `${count} selected`;
+  }
+
+  function updateGitHubSelectAllButtonLabel() {
+    if (!githubImportSelectAllBtn) return;
+    const total = availableGitHubImportPaths.length;
+    const allSelected = total > 0 && selectedGitHubImportPaths.size === total;
+    githubImportSelectAllBtn.textContent = allSelected ? "Clear All" : "Select All";
+  }
+
+  function syncGitHubSelectionToButtons() {
+    if (!githubImportTree) return;
+    Array.from(githubImportTree.querySelectorAll(".github-tree-file-btn")).forEach((btn) => {
+      const isSelected = selectedGitHubImportPaths.has(btn.dataset.path);
+      btn.classList.toggle("is-selected", isSelected);
+      btn.setAttribute("aria-pressed", isSelected ? "true" : "false");
+    });
+  }
+
+  function setGitHubSelectedPaths(paths) {
+    selectedGitHubImportPaths.clear();
+    (paths || []).forEach((path) => selectedGitHubImportPaths.add(path));
+    updateGitHubImportSelectedCount();
+    syncGitHubSelectionToButtons();
+    updateGitHubSelectAllButtonLabel();
+  }
+
+  function toggleGitHubSelectedPath(path) {
+    if (!path) return;
+    if (selectedGitHubImportPaths.has(path)) {
+      selectedGitHubImportPaths.delete(path);
+    } else {
+      selectedGitHubImportPaths.add(path);
+    }
+    updateGitHubImportSelectedCount();
+    syncGitHubSelectionToButtons();
+    updateGitHubSelectAllButtonLabel();
+  }
+
+  function renderGitHubImportTree(paths) {
+    if (!githubImportTree || !githubImportFileSelect) return;
+    githubImportTree.innerHTML = "";
+    const tree = buildMarkdownFileTree(paths);
+
+    const createTreeBranch = function(node, parentPath) {
+      const list = document.createElement("ul");
+      const folderNames = Object.keys(node.folders).sort((a, b) => a.localeCompare(b));
+      folderNames.forEach((folderName) => {
+        const folderPath = parentPath ? `${parentPath}/${folderName}` : folderName;
+        const item = document.createElement("li");
+        const folderLabel = document.createElement("span");
+        folderLabel.className = "github-tree-folder-label";
+        folderLabel.textContent = `📁 ${folderName}`;
+        item.appendChild(folderLabel);
+        item.appendChild(createTreeBranch(node.folders[folderName], folderPath));
+        list.appendChild(item);
+      });
+
+      node.files
+        .sort((a, b) => a.path.localeCompare(b.path))
+        .forEach((file) => {
+          const fileItem = document.createElement("li");
+          const fileButton = document.createElement("button");
+          fileButton.type = "button";
+          fileButton.className = "github-tree-file-btn";
+          fileButton.dataset.path = file.path;
+          fileButton.setAttribute("aria-pressed", "false");
+          fileButton.textContent = `📄 ${file.name}`;
+          fileButton.addEventListener("click", function() {
+            toggleGitHubSelectedPath(file.path);
+          });
+          fileItem.appendChild(fileButton);
+          list.appendChild(fileItem);
+        });
+
+      return list;
+    };
+
+    githubImportTree.appendChild(createTreeBranch(tree, ""));
+    syncGitHubSelectionToButtons();
+  }
+
+  function setGitHubImportLoading(isLoading) {
+    if (!githubImportSubmitBtn) return;
+    if (isLoading) {
+      githubImportSubmitBtn.dataset.loadingText = githubImportSubmitBtn.textContent;
+      githubImportSubmitBtn.textContent = "Importing...";
+    } else if (githubImportSubmitBtn.dataset.loadingText) {
+      githubImportSubmitBtn.textContent = githubImportSubmitBtn.dataset.loadingText;
+      delete githubImportSubmitBtn.dataset.loadingText;
+    }
+  }
+
+  function setGitHubImportMessage(message, options = {}) {
+    if (!githubImportError) return;
+    const { isError = true } = options;
+    githubImportError.classList.toggle("is-info", !isError);
+    if (!message) {
+      githubImportError.textContent = "";
+      githubImportError.style.display = "none";
+      return;
+    }
+    githubImportError.textContent = message;
+    githubImportError.style.display = "block";
+  }
+
+  function resetGitHubImportModal() {
+    if (!githubImportUrlInput || !githubImportFileSelect || !githubImportSubmitBtn) return;
+    if (githubImportTitle) {
+      githubImportTitle.textContent = "Import Markdown from GitHub";
+    }
+    githubImportUrlInput.value = "";
+    githubImportUrlInput.style.display = "block";
+    githubImportUrlInput.disabled = false;
+    githubImportFileSelect.innerHTML = "";
+    githubImportFileSelect.style.display = "none";
+    githubImportFileSelect.disabled = false;
+    if (githubImportSelectionToolbar) {
+      githubImportSelectionToolbar.style.display = "none";
+    }
+    availableGitHubImportPaths = [];
+    setGitHubSelectedPaths([]);
+    if (githubImportTree) {
+      githubImportTree.innerHTML = "";
+      githubImportTree.style.display = "none";
+    }
+    githubImportSubmitBtn.dataset.step = "url";
+    delete githubImportSubmitBtn.dataset.owner;
+    delete githubImportSubmitBtn.dataset.repo;
+    delete githubImportSubmitBtn.dataset.ref;
+    githubImportSubmitBtn.textContent = "Import";
+    setGitHubImportMessage("");
+  }
+
+  function openGitHubImportModal() {
+    if (!githubImportModal || !githubImportUrlInput || !githubImportSubmitBtn) return;
+    resetGitHubImportModal();
+    openAppModal(githubImportModal, {
+      focusTarget: githubImportUrlInput,
+      onClose: closeGitHubImportModal
+    });
+  }
+
+  function closeGitHubImportModal() {
+    if (!githubImportModal) return;
+    closeAppModal(githubImportModal);
+    resetGitHubImportModal();
+  }
+
+  async function handleGitHubImportSubmit() {
+    if (!githubImportSubmitBtn || !githubImportUrlInput || !githubImportFileSelect) return;
+    const setGitHubImportDialogDisabled = (disabled) => {
+      githubImportSubmitBtn.disabled = disabled;
+      if (githubImportCancelBtn) {
+        githubImportCancelBtn.disabled = disabled;
+      }
+      if (githubImportSelectAllBtn) {
+        githubImportSelectAllBtn.disabled = disabled;
+      }
+    };
+    const step = githubImportSubmitBtn.dataset.step || "url";
+    if (step === "select") {
+      const selectedPaths = Array.from(selectedGitHubImportPaths);
+      const owner = githubImportSubmitBtn.dataset.owner;
+      const repo = githubImportSubmitBtn.dataset.repo;
+      const ref = githubImportSubmitBtn.dataset.ref;
+      if (!owner || !repo || !ref || !selectedPaths.length) {
+        setGitHubImportMessage("Please select at least one file to import.");
+        return;
+      }
+      setGitHubImportLoading(true);
+      setGitHubImportDialogDisabled(true);
+      announceToScreenReader("Importing selected files from GitHub...");
+      try {
+        for (const selectedPath of selectedPaths) {
+          const markdown = await fetchTextContent(buildRawGitHubUrl(owner, repo, ref, selectedPath));
+          newTab(markdown, getFileName(selectedPath).replace(/\.(md|markdown)$/i, ""));
+        }
+        closeGitHubImportModal();
+        announceToScreenReader("Files imported successfully.");
+      } catch (error) {
+        console.error("GitHub import failed:", error);
+        setGitHubImportMessage("GitHub import failed: " + error.message);
+        announceToScreenReader("GitHub import failed.");
+      } finally {
+        setGitHubImportDialogDisabled(false);
+        setGitHubImportLoading(false);
+      }
+      return;
+    }
+
+    const urlInput = githubImportUrlInput.value.trim();
+    if (!urlInput) {
+      setGitHubImportMessage("Please enter a GitHub URL.");
+      return;
+    }
+
+    const parsed = parseGitHubImportUrl(urlInput);
+    if (!parsed || !parsed.owner || !parsed.repo) {
+      setGitHubImportMessage("Please enter a valid GitHub URL.");
+      return;
+    }
+
+    setGitHubImportMessage("");
+    setGitHubImportLoading(true);
+    setGitHubImportDialogDisabled(true);
+    try {
+      if (parsed.type === "file") {
+        if (!isMarkdownPath(parsed.filePath)) {
+          throw new Error("The provided URL does not point to a Markdown file.");
+        }
+        announceToScreenReader("Fetching file from GitHub...");
+        const markdown = await fetchTextContent(buildRawGitHubUrl(parsed.owner, parsed.repo, parsed.ref, parsed.filePath));
+        newTab(markdown, getFileName(parsed.filePath).replace(/\.(md|markdown)$/i, ""));
+        closeGitHubImportModal();
+        announceToScreenReader("File imported successfully.");
+        return;
+      }
+
+      // Accessibility dynamic live announcer
+      const fetchingText = I18N_DICTS[activeLang].loadingFiles || "Fetching file tree...";
+      announceToScreenReader(fetchingText);
+
+      // Render hierarchical visual skeleton tree while the list is loading
+      if (githubImportTree) {
+        renderGitHubImportTreeSkeleton();
+        githubImportTree.style.display = "block";
+      }
+
+      const ref = parsed.ref || await getDefaultBranch(parsed.owner, parsed.repo);
+      const files = await listMarkdownFiles(parsed.owner, parsed.repo, ref, parsed.basePath || "");
+
+      if (!files.length) {
+        if (githubImportTree) {
+          githubImportTree.innerHTML = "";
+          githubImportTree.style.display = "none";
+        }
+        setGitHubImportMessage("No Markdown files were found at that GitHub location.");
+        announceToScreenReader("Failed to locate Markdown files.");
+        return;
+      }
+
+      const shownFiles = files.slice(0, MAX_GITHUB_FILES_SHOWN);
+      if (files.length === 1) {
+        const targetPath = files[0];
+        announceToScreenReader("Fetching file content...");
+        const markdown = await fetchTextContent(buildRawGitHubUrl(parsed.owner, parsed.repo, ref, targetPath));
+        newTab(markdown, getFileName(targetPath).replace(/\.(md|markdown)$/i, ""));
+        closeGitHubImportModal();
+        announceToScreenReader("File imported successfully.");
+        return;
+      }
+
+      githubImportFileSelect.innerHTML = "";
+      githubImportUrlInput.style.display = "none";
+      githubImportFileSelect.style.display = "none";
+      if (githubImportSelectionToolbar) {
+        githubImportSelectionToolbar.style.display = "flex";
+      }
+      if (githubImportTree) {
+        githubImportTree.style.display = "block";
+      }
+      shownFiles.forEach((filePath) => {
+        const option = document.createElement("option");
+        option.value = filePath;
+        option.textContent = filePath;
+        githubImportFileSelect.appendChild(option);
+      });
+      availableGitHubImportPaths = shownFiles.slice();
+      setGitHubSelectedPaths(shownFiles[0] ? [shownFiles[0]] : []);
+      renderGitHubImportTree(shownFiles);
+
+      // Announce load complete
+      announceToScreenReader("GitHub files loaded. " + files.length + " files available in the tree.");
+
+      if (files.length > MAX_GITHUB_FILES_SHOWN) {
+        setGitHubImportMessage(`Showing first ${MAX_GITHUB_FILES_SHOWN} of ${files.length} Markdown files.`, { isError: false });
+      } else {
+        setGitHubImportMessage("");
+      }
+      if (githubImportTitle) {
+        githubImportTitle.textContent = "Select Markdown file(s) to import";
+      }
+      githubImportSubmitBtn.dataset.step = "select";
+      githubImportSubmitBtn.dataset.owner = parsed.owner;
+      githubImportSubmitBtn.dataset.repo = parsed.repo;
+      githubImportSubmitBtn.dataset.ref = ref;
+      githubImportSubmitBtn.textContent = "Import Selected";
+    } catch (error) {
+      console.error("GitHub import failed:", error);
+      setGitHubImportMessage("GitHub import failed: " + error.message);
+      announceToScreenReader("GitHub import failed.");
+      if (githubImportTree) {
+        githubImportTree.innerHTML = "";
+        githubImportTree.style.display = "none";
+      }
+    } finally {
+      setGitHubImportDialogDisabled(false);
+      setGitHubImportLoading(false);
+    }
+  }
+
+  function scheduleEmojiLookupRefresh() {
+    if (emojiLookupLoaded || emojiRenderScheduled) return;
+    emojiRenderScheduled = true;
+    loadEmojiEntries()
+      .then(() => {
+        if (emojiUrlMap.size) {
+          renderMarkdown({ force: true, reason: 'emoji-refresh' });
+        }
+      })
+      .finally(() => {
+        emojiRenderScheduled = false;
+      });
+  }
+
+  function processEmojis(element) {
+    // Early exit if the raw text content has no colon characters (PERF-013)
+    // This avoids the expensive TreeWalker DOM walk for documents without emoji shortcodes
+    if (!element.textContent || !element.textContent.includes(':')) return;
+
+    // PERF-002: Lazy-load JoyPixels on first use
+    if (typeof joypixels === 'undefined') {
+      Promise.all([
+        loadScript(CDN.joypixels),
+        loadStyle(CDN.joypixels_css)
+      ]).then(function() { processEmojis(element); });
+      return;
+    }
+    
+    const walker = document.createTreeWalker(
+      element,
+      NodeFilter.SHOW_TEXT,
+      null,
+      false
+    );
+    
+    const textNodes = [];
+    let node;
+    while ((node = walker.nextNode())) {
+      let parent = node.parentNode;
+      let isInCode = false;
+      while (parent && parent !== element) {
+        if (parent.tagName === 'PRE' || parent.tagName === 'CODE') {
+          isInCode = true;
+          break;
+        }
+        parent = parent.parentNode;
+      }
+      
+      if (!isInCode && node.nodeValue.includes(':')) {
+        textNodes.push(node);
+      }
+    }
+    
+    let needsEmojiLookup = false;
+    textNodes.forEach(textNode => {
+      const text = textNode.nodeValue;
+      const emojiRegex = /:([\w+-]+):/g;
+      
+      let match;
+      let lastIndex = 0;
+      let hasEmoji = false;
+      const fragment = document.createDocumentFragment();
+      
+      while ((match = emojiRegex.exec(text)) !== null) {
+        const shortcode = match[1];
+        const emoji = joypixels.shortnameToUnicode(`:${shortcode}:`);
+        
+        if (emoji !== `:${shortcode}:`) { // If conversion was successful
+          hasEmoji = true;
+          if (match.index > lastIndex) {
+            fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
+          }
+          fragment.appendChild(document.createTextNode(emoji));
+          lastIndex = emojiRegex.lastIndex;
+        } else {
+          const emojiUrl = emojiUrlMap.get(shortcode);
+          if (emojiUrl) {
+            hasEmoji = true;
+            if (match.index > lastIndex) {
+              fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
+            }
+            const image = document.createElement('img');
+            image.className = 'emoji-inline';
+            image.src = emojiUrl;
+            image.alt = `:${shortcode}:`;
+            image.loading = 'lazy';
+            image.setAttribute('aria-label', `:${shortcode}:`);
+            fragment.appendChild(image);
+            lastIndex = emojiRegex.lastIndex;
+          } else if (!emojiLookupLoaded) {
+            needsEmojiLookup = true;
+          }
+        }
+      }
+      
+      if (hasEmoji) {
+        if (lastIndex < text.length) {
+          fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
+        }
+        textNode.parentNode.replaceChild(fragment, textNode);
+      }
+    });
+
+    if (needsEmojiLookup) {
+      scheduleEmojiLookupRefresh();
+    }
+  }
+
+  function debouncedRender() {
+    clearTimeout(markdownRenderTimeout);
+    const delay = getPreviewRenderDelay(markdownEditor.value);
+    markdownRenderTimeout = setTimeout(function() {
+      renderMarkdown({ reason: 'edit' });
+    }, delay);
+  }
+
+  function countWordsFast(text) {
+    let count = 0;
+    let inWord = false;
+    for (let i = 0; i < text.length; i += 1) {
+      const code = text.charCodeAt(i);
+      if (
+        code === 32 ||
+        code === 9 ||
+        code === 10 ||
+        code === 13 ||
+        code === 12 ||
+        code === 11 ||
+        code === 160
+      ) {
+        inWord = false;
+      } else if (!inWord) {
+        count += 1;
+        inWord = true;
+      }
+    }
+    return count;
+  }
+
+  function updateDocumentStats() {
+    const text = markdownEditor.value;
+
+    const charCount = text.length;
+    charCountElement.textContent = charCount.toLocaleString();
+
+    const wordCount = countWordsFast(text);
+    wordCountElement.textContent = wordCount.toLocaleString();
+
+    const readingTimeMinutes = Math.ceil(wordCount / 200);
+    readingTimeElement.textContent = readingTimeMinutes;
+  }
+
+  function syncEditorToPreview() {
+    if (!syncScrollingEnabled || isPreviewScrolling || isProgrammaticScrolling) return;
+    isEditorScrolling = true;
+
+    if (scrollSyncTimeout) cancelAnimationFrame(scrollSyncTimeout);
+    scrollSyncTimeout = requestAnimationFrame(function() {
+      const editorScrollRange = markdownEditor.scrollHeight - markdownEditor.clientHeight;
+      const editorScrollRatio =
+        editorScrollRange > 0 ? markdownEditor.scrollTop / editorScrollRange : 0;
+      const previewScrollPosition =
+        (previewPane.scrollHeight - previewPane.clientHeight) *
+        editorScrollRatio;
+
+      if (!isNaN(previewScrollPosition) && isFinite(previewScrollPosition)) {
+        previewPane.scrollTop = previewScrollPosition;
+      }
+
+      setTimeout(function() {
+        isEditorScrolling = false;
+      }, 50);
+    });
+  }
+
+  function syncPreviewToEditor() {
+    if (!syncScrollingEnabled || isEditorScrolling || isProgrammaticScrolling) return;
+    isPreviewScrolling = true;
+
+    if (scrollSyncTimeout) cancelAnimationFrame(scrollSyncTimeout);
+    scrollSyncTimeout = requestAnimationFrame(function() {
+      const previewScrollRange = previewPane.scrollHeight - previewPane.clientHeight;
+      const previewScrollRatio =
+        previewScrollRange > 0 ? previewPane.scrollTop / previewScrollRange : 0;
+      const editorScrollPosition =
+        (markdownEditor.scrollHeight - markdownEditor.clientHeight) *
+        previewScrollRatio;
+
+      if (!isNaN(editorScrollPosition) && isFinite(editorScrollPosition)) {
+        markdownEditor.scrollTop = editorScrollPosition;
+        syncEditorScrollOverlays();
+      }
+
+      setTimeout(function() {
+        isPreviewScrolling = false;
+      }, 50);
+    });
+  }
+
+  function toggleSyncScrolling() {
+    syncScrollingEnabled = !syncScrollingEnabled;
+    if (syncScrollingEnabled) {
+      toggleSyncButton.innerHTML = '<i class="bi bi-link-45deg"></i> <span class="btn-text">Sync Off</span>';
+      toggleSyncButton.classList.add("sync-disabled");
+      toggleSyncButton.classList.remove("sync-enabled");
+      toggleSyncButton.classList.add("sync-active");
+    } else {
+      toggleSyncButton.innerHTML = '<i class="bi bi-link"></i> <span class="btn-text">Sync On</span>';
+      toggleSyncButton.classList.add("sync-enabled");
+      toggleSyncButton.classList.remove("sync-disabled");
+      toggleSyncButton.classList.remove("sync-active");
+    }
+    saveGlobalState({ syncScrollingEnabled });
+  }
+
+  // View Mode Functions - Story 1.1 & 1.2
+  function setViewMode(mode) {
+    if (mode === currentViewMode) return;
+
+    const previousMode = currentViewMode;
+    currentViewMode = mode;
+
+    // Update content container class
+    contentContainer.classList.remove('view-editor-only', 'view-preview-only', 'view-split');
+    contentContainer.classList.add('view-' + (mode === 'editor' ? 'editor-only' : mode === 'preview' ? 'preview-only' : 'split'));
+
+    // Update button active states (desktop)
+    viewModeButtons.forEach(btn => {
+      const btnMode = btn.getAttribute('data-view-mode');
+      if (btnMode === mode) {
+        btn.classList.add('is-active');
+        btn.setAttribute('aria-pressed', 'true');
+      } else {
+        btn.classList.remove('is-active');
+        btn.setAttribute('aria-pressed', 'false');
+      }
+    });
+
+    // Story 1.4: Update mobile button active states
+    mobileViewModeButtons.forEach(btn => {
+      const btnMode = btn.getAttribute('data-mode');
+      if (btnMode === mode) {
+        btn.classList.add('active');
+        btn.setAttribute('aria-pressed', 'true');
+      } else {
+        btn.classList.remove('active');
+        btn.setAttribute('aria-pressed', 'false');
+      }
+    });
+
+    // Story 1.2: Show/hide sync toggle based on view mode
+    updateSyncToggleVisibility(mode);
+
+    // Story 1.3: Handle pane widths when switching modes
+    if (mode === 'split') {
+      // Restore preserved pane widths when entering split mode
+      applyPaneWidths();
+    } else {
+      // Reset inline pane widths when not in split mode
+      resetPaneWidths();
+    }
+
+    // Re-render markdown when switching to a view that includes preview
+    if (mode === 'split' || mode === 'preview') {
+      renderMarkdown({ reason: 'view-switch' });
+    }
+
+    if (mode === 'split' || mode === 'editor') {
+      refreshEditorWidth();
+      scheduleLineNumberUpdate({ force: true });
+      updateFindHighlights();
+      scheduleEditorOverlayScrollSync();
+    }
+  }
+
+  function resolveViewToggleMode(mode) {
+    if ((mode === 'editor' || mode === 'preview') && currentViewMode === mode) {
+      return 'split';
+    }
+    return mode;
+  }
+
+  // Story 1.2: Update sync toggle visibility
+  function updateSyncToggleVisibility(mode) {
+    const isSplitView = mode === 'split';
+
+    // Desktop sync toggle
+    if (toggleSyncButton) {
+      toggleSyncButton.style.display = '';
+      toggleSyncButton.disabled = !isSplitView;
+      toggleSyncButton.setAttribute('aria-disabled', String(!isSplitView));
+      toggleSyncButton.removeAttribute('aria-hidden');
+    }
+
+    // Mobile sync toggle
+    if (mobileToggleSync) {
+      mobileToggleSync.style.display = '';
+      mobileToggleSync.disabled = !isSplitView;
+      mobileToggleSync.setAttribute('aria-disabled', String(!isSplitView));
+      mobileToggleSync.removeAttribute('aria-hidden');
+    }
+  }
+
+  function replaceEditorRange(start, end, replacement, selectStart, selectEnd) {
+    pushProgrammaticHistoryState();
+    markdownEditor.focus();
+    markdownEditor.setRangeText(replacement, start, end, 'end');
+    const nextStart = typeof selectStart === 'number' ? selectStart : start + replacement.length;
+    const nextEnd = typeof selectEnd === 'number' ? selectEnd : nextStart;
+    markdownEditor.setSelectionRange(nextStart, nextEnd);
+    markdownEditor.dispatchEvent(new Event('input', { bubbles: true }));
+    lastPushedValue = markdownEditor.value;
+    lastInputType = 'programmatic';
+  }
+
+  function wrapEditorSelection(prefix, suffix, placeholder) {
+    const start = markdownEditor.selectionStart;
+    const end = markdownEditor.selectionEnd;
+    const selected = markdownEditor.value.slice(start, end) || placeholder;
+    const replacement = prefix + selected + suffix;
+    const selectionStart = start + prefix.length;
+    const selectionEnd = selectionStart + selected.length;
+    replaceEditorRange(start, end, replacement, selectionStart, selectionEnd);
+  }
+
+  function getCurrentLineRange() {
+    const value = markdownEditor.value;
+    const start = markdownEditor.selectionStart;
+    const lineStart = value.lastIndexOf('\n', Math.max(0, start - 1)) + 1;
+    let lineEnd = value.indexOf('\n', start);
+    if (lineEnd === -1) lineEnd = value.length;
+    return { start: lineStart, end: lineEnd, text: value.slice(lineStart, lineEnd) };
+  }
+
+  function getSelectedLineRange() {
+    const value = markdownEditor.value;
+    const start = markdownEditor.selectionStart;
+    const end = markdownEditor.selectionEnd;
+    const lineStart = value.lastIndexOf('\n', Math.max(0, start - 1)) + 1;
+    let lineEnd = value.indexOf('\n', end);
+    if (lineEnd === -1) lineEnd = value.length;
+    return { start: lineStart, end: lineEnd, text: value.slice(lineStart, lineEnd) };
+  }
+
+  function transformEditorLines(transformer) {
+    const range = getSelectedLineRange();
+    const replacement = range.text.split('\n').map(transformer).join('\n');
+    replaceEditorRange(range.start, range.end, replacement, range.start, range.start + replacement.length);
+  }
+
+  function getListLineRange() {
+    const value = markdownEditor.value;
+    const start = markdownEditor.selectionStart;
+    const end = markdownEditor.selectionEnd;
+    const effectiveEnd = end > start && value[end - 1] === '\n' ? end - 1 : end;
+    const lineStart = value.lastIndexOf('\n', Math.max(0, start - 1)) + 1;
+    let lineEnd = value.indexOf('\n', effectiveEnd);
+    if (lineEnd === -1) lineEnd = value.length;
+    return { start: lineStart, end: lineEnd, text: value.slice(lineStart, lineEnd) };
+  }
+
+  function parseMarkdownListItem(line) {
+    const match = line.match(/^(\s*)((\d+)\.|[-*+])(?:\s+|$)(.*)$/);
+    if (!match) return null;
+    const isOrdered = typeof match[3] !== 'undefined';
+    return {
+      type: isOrdered ? 'ordered' : 'unordered',
+      indent: match[1],
+      marker: match[2],
+      number: isOrdered ? parseInt(match[3], 10) : null,
+      bullet: isOrdered ? null : match[2],
+      body: match[4] || '',
+      prefix: match[1] + match[2] + ' '
+    };
+  }
+
+  function stripListMarkerForApply(line) {
+    const parsed = parseMarkdownListItem(line);
+    if (parsed) {
+      return { indent: parsed.indent, body: parsed.body };
+    }
+    const match = line.match(/^(\s*)(.*)$/);
+    return { indent: match ? match[1] : '', body: match ? match[2] : line };
+  }
+
+  function getPreviousLineInfo(lineStart) {
+    if (lineStart <= 0) return null;
+    const value = markdownEditor.value;
+    const previousEnd = lineStart - 1;
+    const previousStart = previousEnd > 0 ? value.lastIndexOf('\n', previousEnd - 1) + 1 : 0;
+    return { start: previousStart, text: value.slice(previousStart, previousEnd) };
+  }
+
+  function getOrderedListStartNumber(lineStart) {
+    const previousLine = getPreviousLineInfo(lineStart);
+    if (!previousLine || !previousLine.text.trim()) return 1;
+    const parsed = parseMarkdownListItem(previousLine.text);
+    return parsed && parsed.type === 'ordered' ? parsed.number + 1 : 1;
+  }
+
+  function applyMarkdownList(type) {
+    const range = getListLineRange();
+    const hadSelection = markdownEditor.selectionStart !== markdownEditor.selectionEnd;
+    const lines = range.text.split('\n');
+    let nextNumber = type === 'ordered' ? getOrderedListStartNumber(range.start) : 1;
+    let firstPrefixLength = null;
+
+    const replacement = lines.map(function(line) {
+      const stripped = stripListMarkerForApply(line);
+      const prefix = type === 'ordered'
+        ? stripped.indent + (nextNumber++) + '. '
+        : stripped.indent + '- ';
+      if (firstPrefixLength === null) firstPrefixLength = prefix.length;
+      return prefix + stripped.body;
+    }).join('\n');
+
+    const isSingleLine = lines.length === 1;
+    const caret = (!hadSelection || isSingleLine)
+      ? range.start + (firstPrefixLength || 0)
+      : range.start + replacement.length;
+
+    replaceEditorRange(range.start, range.end, replacement, caret, caret);
+  }
+
+  function renumberOrderedListAfterPosition(position, nextNumber) {
+    let value = markdownEditor.value;
+    let lineStart = value.indexOf('\n', position);
+    if (lineStart === -1) return;
+    lineStart += 1;
+
+    let changed = false;
+    while (lineStart < value.length) {
+      let lineEnd = value.indexOf('\n', lineStart);
+      const hasNewline = lineEnd !== -1;
+      if (!hasNewline) lineEnd = value.length;
+
+      const line = value.slice(lineStart, lineEnd);
+      if (!line.trim()) break;
+
+      const parsed = parseMarkdownListItem(line);
+      if (!parsed || parsed.type !== 'ordered') break;
+
+      const replacement = parsed.indent + nextNumber + '. ' + parsed.body;
+      if (replacement !== line) {
+        value = value.slice(0, lineStart) + replacement + value.slice(lineEnd);
+        changed = true;
+      }
+
+      lineStart += replacement.length + (hasNewline ? 1 : 0);
+      nextNumber += 1;
+    }
+
+    if (changed) {
+      const selectionStart = markdownEditor.selectionStart;
+      const selectionEnd = markdownEditor.selectionEnd;
+      markdownEditor.value = value;
+      markdownEditor.setSelectionRange(selectionStart, selectionEnd);
+      markdownEditor.dispatchEvent(new Event('input', { bubbles: true }));
+    }
+  }
+
+  function handleListEnter(e) {
+    if (e.key !== 'Enter' || e.shiftKey || markdownEditor.selectionStart !== markdownEditor.selectionEnd) {
+      return false;
+    }
+
+    const range = getCurrentLineRange();
+    const parsed = parseMarkdownListItem(range.text);
+    if (!parsed) return false;
+
+    e.preventDefault();
+    if (!parsed.body.trim()) {
+      const caret = range.start + parsed.indent.length;
+      replaceEditorRange(range.start, range.end, parsed.indent, caret, caret);
+      return true;
+    }
+
+    const nextPrefix = parsed.type === 'ordered'
+      ? parsed.indent + (parsed.number + 1) + '. '
+      : parsed.indent + parsed.bullet + ' ';
+    const insertAt = markdownEditor.selectionStart;
+    const caret = insertAt + 1 + nextPrefix.length;
+    replaceEditorRange(insertAt, insertAt, '\n' + nextPrefix, caret, caret);
+
+    if (parsed.type === 'ordered') {
+      renumberOrderedListAfterPosition(caret, parsed.number + 2);
+    }
+
+    return true;
+  }
+
+  function transformSelectionOrCurrentLine(transformer) {
+    let start = markdownEditor.selectionStart;
+    let end = markdownEditor.selectionEnd;
+    if (start === end) {
+      const range = getCurrentLineRange();
+      start = range.start;
+      end = range.end;
+    }
+    const replacement = transformer(markdownEditor.value.slice(start, end));
+    replaceEditorRange(start, end, replacement, start, start + replacement.length);
+  }
+
+  function stripBasicMarkdown(text) {
+    return text
+      .replace(/^#{1,6}\s+/gm, '')
+      .replace(/^>\s?/gm, '')
+      .replace(/^(\s*)([-*+]|\d+\.)\s+/gm, '$1')
+      .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1')
+      .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
+      .replace(/(\*\*|__)(.*?)\1/g, '$2')
+      .replace(/(\*|_)(.*?)\1/g, '$2')
+      .replace(/~~(.*?)~~/g, '$1')
+      .replace(/`([^`]+)`/g, '$1');
+  }
+
+  function getOrCreateTabHistory(tabId) {
+    if (!tabId) return { undoStack: [], redoStack: [] };
+    if (!tabHistories[tabId]) {
+      tabHistories[tabId] = {
+        undoStack: [],
+        redoStack: []
+      };
+    }
+    return tabHistories[tabId];
+  }
+
+  function initTabHistory(tabId, initialValue) {
+    const hist = getOrCreateTabHistory(tabId);
+    if (hist.undoStack.length === 0) {
+      hist.undoStack.push({
+        value: initialValue || '',
+        selectionStart: 0,
+        selectionEnd: 0
+      });
+      lastPushedValue = initialValue || '';
+      currentHistoryTabId = tabId;
+      pendingState = null;
+    }
+  }
+
+  function pushProgrammaticHistoryState() {
+    if (typingTimeout) {
+      clearTimeout(typingTimeout);
+      typingTimeout = null;
+    }
+    
+    const tabId = activeTabId;
+    const hist = getOrCreateTabHistory(tabId);
+    const currentValue = markdownEditor.value;
+    
+    if (pendingState) {
+      hist.undoStack.push(pendingState);
+      if (hist.undoStack.length > 200) {
+        hist.undoStack.shift();
+      }
+      hist.redoStack.length = 0;
+      pendingState = null;
+      lastPushedValue = currentValue;
+    } else if (currentValue !== lastPushedValue) {
+      hist.undoStack.push({
+        value: currentValue,
+        selectionStart: markdownEditor.selectionStart,
+        selectionEnd: markdownEditor.selectionEnd
+      });
+      if (hist.undoStack.length > 200) {
+        hist.undoStack.shift();
+      }
+      hist.redoStack.length = 0;
+      lastPushedValue = currentValue;
+    }
+    updateUndoRedoButtons();
+  }
+
+  function commitPendingState() {
+    if (typingTimeout) {
+      clearTimeout(typingTimeout);
+      typingTimeout = null;
+    }
+    if (!pendingState) return;
+    
+    const tabId = activeTabId;
+    const hist = getOrCreateTabHistory(tabId);
+    
+    hist.undoStack.push(pendingState);
+    if (hist.undoStack.length > 200) {
+      hist.undoStack.shift();
+    }
+    
+    hist.redoStack.length = 0;
+    lastPushedValue = markdownEditor.value;
+    pendingState = null;
+    updateUndoRedoButtons();
+  }
+
+  function handleKeystrokeHistory(e) {
+    const currentValue = markdownEditor.value;
+    if (currentValue === lastPushedValue) return;
+    
+    const inputType = e && typeof e.inputType === 'string' ? e.inputType : '';
+    
+    if (!pendingState) {
+      pendingState = {
+        value: lastPushedValue,
+        selectionStart: lastCursorStart,
+        selectionEnd: lastCursorEnd
+      };
+    }
+    
+    let shouldCommit = false;
+    
+    if (inputType === 'insertLineBreak' || inputType === 'insertParagraph' || inputType === 'insertFromPaste' || lastInputType === 'programmatic') {
+      shouldCommit = true;
+    } else if (e && e.data === ' ') {
+      shouldCommit = true;
+    } else {
+      const isDelete = inputType.startsWith('delete');
+      const wasDelete = lastInputType === 'delete';
+      const isInsert = inputType.startsWith('insert');
+      const wasInsert = lastInputType === 'insert';
+      
+      if ((isDelete && wasInsert) || (isInsert && wasDelete)) {
+        shouldCommit = true;
+      }
+    }
+    
+    if (shouldCommit) {
+      commitPendingState();
+    }
+    
+    if (typingTimeout) {
+      clearTimeout(typingTimeout);
+    }
+    typingTimeout = setTimeout(function() {
+      commitPendingState();
+    }, 1000);
+    
+    if (inputType.startsWith('delete')) {
+      lastInputType = 'delete';
+    } else if (inputType.startsWith('insert')) {
+      lastInputType = 'insert';
+    } else {
+      lastInputType = 'other';
+    }
+  }
+
+  function updateLastCursor() {
+    if (markdownEditor) {
+      lastCursorStart = markdownEditor.selectionStart;
+      lastCursorEnd = markdownEditor.selectionEnd;
+    }
+  }
+
+  function updateUndoRedoButtons() {
+    const undoBtn = document.querySelector('[data-md-action="undo"]');
+    const redoBtn = document.querySelector('[data-md-action="redo"]');
+    if (!undoBtn || !redoBtn) return;
+    
+    const tabId = activeTabId;
+    const hist = getOrCreateTabHistory(tabId);
+    
+    const canUndo = hist.undoStack.length > 0 || pendingState !== null;
+    const canRedo = hist.redoStack.length > 0;
+    
+    undoBtn.disabled = !canUndo;
+    undoBtn.classList.toggle('disabled', !canUndo);
+    
+    redoBtn.disabled = !canRedo;
+    redoBtn.classList.toggle('disabled', !canRedo);
+  }
+
+  function executeUndo() {
+    if (typingTimeout) {
+      clearTimeout(typingTimeout);
+      typingTimeout = null;
+    }
+    
+    const tabId = activeTabId;
+    const hist = getOrCreateTabHistory(tabId);
+    const currentValue = markdownEditor.value;
+    
+    let stateToRestore = null;
+    
+    if (pendingState) {
+      stateToRestore = pendingState;
+      pendingState = null;
+      
+      hist.redoStack.push({
+        value: currentValue,
+        selectionStart: markdownEditor.selectionStart,
+        selectionEnd: markdownEditor.selectionEnd
+      });
+      if (hist.redoStack.length > 200) {
+        hist.redoStack.shift();
+      }
+    } else if (hist.undoStack.length > 0) {
+      const topState = hist.undoStack.pop();
+      if (topState) {
+        stateToRestore = topState;
+        
+        hist.redoStack.push({
+          value: currentValue,
+          selectionStart: markdownEditor.selectionStart,
+          selectionEnd: markdownEditor.selectionEnd
+        });
+        if (hist.redoStack.length > 200) {
+          hist.redoStack.shift();
+        }
+      }
+    }
+    
+    if (stateToRestore) {
+      markdownEditor.value = stateToRestore.value;
+      markdownEditor.setSelectionRange(stateToRestore.selectionStart, stateToRestore.selectionEnd);
+      lastPushedValue = stateToRestore.value;
+      lastInputType = null;
+      
+      markdownEditor.dispatchEvent(new Event('input', { bubbles: true }));
+      saveCurrentTabState();
+    }
+    
+    updateUndoRedoButtons();
+  }
+
+  function executeRedo() {
+    if (typingTimeout) {
+      clearTimeout(typingTimeout);
+      typingTimeout = null;
+    }
+    
+    const tabId = activeTabId;
+    const hist = getOrCreateTabHistory(tabId);
+    const currentValue = markdownEditor.value;
+    
+    if (hist.redoStack.length > 0) {
+      const stateToRestore = hist.redoStack.pop();
+      
+      hist.undoStack.push({
+        value: currentValue,
+        selectionStart: markdownEditor.selectionStart,
+        selectionEnd: markdownEditor.selectionEnd
+      });
+      if (hist.undoStack.length > 200) {
+        hist.undoStack.shift();
+      }
+      
+      markdownEditor.value = stateToRestore.value;
+      markdownEditor.setSelectionRange(stateToRestore.selectionStart, stateToRestore.selectionEnd);
+      lastPushedValue = stateToRestore.value;
+      lastInputType = null;
+      pendingState = null;
+      
+      markdownEditor.dispatchEvent(new Event('input', { bubbles: true }));
+      saveCurrentTabState();
+    }
+    
+    updateUndoRedoButtons();
+  }
+
+  function stripMarkdownFormatting(text) {
+    if (!text) return '';
+    return text
+      // Remove fenced code block syntax
+      .replace(/^```[a-zA-Z0-9-]*\r?\n?/gm, '')
+      .replace(/```\r?$/gm, '')
+      // Remove reference link definitions (e.g., [id]: url "title")
+      .replace(/^\[[^\]]+\]:\s*\S+(?:\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\s*$/gm, '')
+      // Strip basic markdown constructs (headers, blockquotes, lists, bold, italic, strikethrough, code)
+      .replace(/^#{1,6}\s+/gm, '')
+      .replace(/^>\s?/gm, '')
+      .replace(/^(\s*)([-*+]|\d+\.)\s+/gm, '$1')
+      .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
+      .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
+      // HTML alignment tags or custom tags (strip the tags, keep inner text)
+      .replace(/<[^>]+>/g, '')
+      // Bold, Italic, Strikethrough, Inline code
+      .replace(/(\*\*|__)(.*?)\1/g, '$2')
+      .replace(/(\*|_)(.*?)\1/g, '$2')
+      .replace(/~~(.*?)~~/g, '$1')
+      .replace(/`([^`]+)`/g, '$1')
+      // Remove horizontal rules
+      .replace(/^\s*[-*_]{3,}\s*$/gm, '');
+  }
+
+  function applyClearFormatting() {
+    const fullText = markdownEditor.value;
+    pushProgrammaticHistoryState();
+    replaceEditorRange(0, fullText.length, '', 0, 0);
+    
+    // Force immediate visual rendering and gutter update
+    renderMarkdown();
+    updateLineNumbers();
+    updateFindHighlights();
+    saveCurrentTabState();
+  }
+
+  function toTitleCase(text) {
+    return text.toLowerCase().replace(/\b\w/g, function(letter) {
+      return letter.toUpperCase();
+    });
+  }
+
+  function toSlug(text) {
+    const slug = text.toLowerCase().trim()
+      .replace(/[^a-z0-9\s-]/g, '')
+      .replace(/\s+/g, '-')
+      .replace(/-+/g, '-');
+    return slug || 'section';
+  }
+
+  function getUsedReferenceNumbers(text) {
+    const used = new Set();
+    const regex = /^\[(\d+)\]:/gm;
+    let match = regex.exec(text);
+    while (match) {
+      const num = parseInt(match[1], 10);
+      if (!Number.isNaN(num)) used.add(num);
+      match = regex.exec(text);
+    }
+    return used;
+  }
+
+  function extractReferenceDefinitions(markdown) {
+    const definitions = new Map();
+    // Matches reference definitions: [1]: <url> "title", [1]: url 'title', or [1]: url (title)
+    const definitionRegex = /^\[(\d+)\]:\s*(?:<([^>\s]+)>|(\S+))(?:\s+(?:"([^"]*)"|'([^']*)'|\(([^)]+)\)))?\s*$/gm;
+    const cleanedMarkdown = markdown.replace(
+      definitionRegex,
+      function(match, numberText, angleUrl, plainUrl, titleDouble, titleSingle, titleParen) {
+        const number = parseInt(numberText, 10);
+        if (Number.isNaN(number)) return match;
+        const url = (angleUrl || plainUrl || '').trim();
+        if (!url) return match;
+        const title = titleDouble || titleSingle || titleParen || '';
+        definitions.set(number, { url: url, title: title });
+        return '';
+      }
+    );
+    return { definitions, cleanedMarkdown };
+  }
+
+  function getNextAvailableReferenceNumber(used, startNumber) {
+    let next = Math.max(1, startNumber || 1);
+    while (used.has(next)) next += 1;
+    return next;
+  }
+
+  function sanitizeMarkdownTitle(title) {
+    return title
+      .replace(/\\/g, '\\\\')
+      .replace(/"/g, '\\"');
+  }
+
+  function isSafeReferenceUrl(url) {
+    if (!url) return false;
+    try {
+      const parsed = new URL(url, window.location.href);
+      return ['http:', 'https:', 'mailto:', 'tel:', 'blob:'].includes(parsed.protocol);
+    } catch (e) {
+      return false;
+    }
+  }
+
+  function applyReferencePreviewLinks(container, referenceDefinitions) {
+    if (!container || !referenceDefinitions || referenceDefinitions.size === 0) return;
+
+    function applyReferenceStyle(link, number) {
+      const definition = referenceDefinitions.get(number);
+      if (definition && definition.url && isSafeReferenceUrl(definition.url)) {
+        link.setAttribute('href', definition.url);
+        if (definition.title) {
+          link.setAttribute('title', definition.title);
+        } else {
+          link.removeAttribute('title');
+        }
+      } else {
+        link.removeAttribute('href');
+      }
+      link.textContent = '[' + number + ']';
+      link.classList.add('reference-link');
+    }
+
+    const links = container.querySelectorAll('a');
+    links.forEach(function(link) {
+      const text = link.textContent.trim();
+      let number = null;
+      if (/^\d+$/.test(text)) {
+        number = parseInt(text, 10);
+      } else {
+        const match = text.match(/^\[(\d+)\]$/);
+        if (match) number = parseInt(match[1], 10);
+      }
+      if (number && referenceDefinitions.has(number)) {
+        applyReferenceStyle(link, number);
+      }
+    });
+
+    const referenceRegex = /\[(\d+)\](?!\s*:)/g;
+    const nodesToProcess = [];
+    const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
+    while (walker.nextNode()) {
+      const node = walker.currentNode;
+      const parent = node.parentElement;
+      if (!parent || !node.nodeValue) continue;
+      if (parent.closest('a, code, pre, script, style, mjx-container')) continue;
+      referenceRegex.lastIndex = 0;
+      if (referenceRegex.test(node.nodeValue)) {
+        nodesToProcess.push(node);
+      }
+    }
+    nodesToProcess.forEach(function(node) {
+      const text = node.nodeValue;
+      referenceRegex.lastIndex = 0;
+      let match;
+      let lastIndex = 0;
+      const fragment = document.createDocumentFragment();
+      while ((match = referenceRegex.exec(text)) !== null) {
+        const before = text.slice(lastIndex, match.index);
+        if (before) fragment.appendChild(document.createTextNode(before));
+        const number = parseInt(match[1], 10);
+        const definition = referenceDefinitions.get(number);
+        if (definition && definition.url && isSafeReferenceUrl(definition.url)) {
+          const link = document.createElement('a');
+          link.href = definition.url;
+          if (definition.title) link.title = definition.title;
+          link.textContent = '[' + number + ']';
+          link.classList.add('reference-link');
+          fragment.appendChild(link);
+        } else {
+          fragment.appendChild(document.createTextNode(match[0]));
+        }
+        lastIndex = match.index + match[0].length;
+      }
+      const after = text.slice(lastIndex);
+      if (after) fragment.appendChild(document.createTextNode(after));
+      node.parentNode.replaceChild(fragment, node);
+    });
+  }
+
+  function cleanupImageObjectUrls() {
+    if (imageObjectUrls.size === 0) return;
+    const contents = [markdownEditor.value];
+    if (Array.isArray(tabs)) {
+      tabs.forEach(function(tab) {
+        if (tab && typeof tab.content === 'string' && tab.content) {
+          contents.push(tab.content);
+        }
+      });
+    }
+    const snapshot = contents.join('\n');
+    Array.from(imageObjectUrls).forEach(function(url) {
+      if (!snapshot.includes(url)) {
+        URL.revokeObjectURL(url);
+        imageObjectUrls.delete(url);
+      }
+    });
+  }
+
+  function insertAlignmentBlock(align) {
+    const allowedAlignments = new Set(['left', 'center', 'right']);
+    const isAllowed = allowedAlignments.has(align);
+    if (!isAllowed) {
+      console.warn('Unsupported alignment:', align);
+      return;
+    }
+    const safeAlign = align;
+    const value = markdownEditor.value;
+    const start = markdownEditor.selectionStart;
+    const end = markdownEditor.selectionEnd;
+    const selected = value.slice(start, end);
+    const hasSelection = start !== end;
+    const blockStart = `<div align="${safeAlign}">\n`;
+    const blockEnd = `\n</div>`;
+    const block = `${blockStart}${hasSelection ? selected : ''}${blockEnd}`;
+    const needsLeadingBreak = start > 0 && value[start - 1] !== '\n';
+    const needsTrailingBreak = end < value.length && value[end] !== '\n';
+    const replacement = (needsLeadingBreak ? '\n' : '') + block + (needsTrailingBreak ? '\n' : '');
+    const contentStart = start + (needsLeadingBreak ? 1 : 0) + blockStart.length;
+    const contentEnd = contentStart + (hasSelection ? selected.length : 0);
+    replaceEditorRange(start, end, replacement, contentStart, hasSelection ? contentEnd : contentStart);
+  }
+
+  function insertMarkdownBlock(block, startOverride, endOverride) {
+    const value = markdownEditor.value;
+    const start = typeof startOverride === 'number' ? startOverride : markdownEditor.selectionStart;
+    const end = typeof endOverride === 'number' ? endOverride : markdownEditor.selectionEnd;
+    const needsLeadingBreak = start > 0 && value[start - 1] !== '\n';
+    const needsTrailingBreak = end < value.length && value[end] !== '\n';
+    const replacement = (needsLeadingBreak ? '\n' : '') + block + (needsTrailingBreak ? '\n' : '');
+    const caret = start + replacement.length;
+    replaceEditorRange(start, end, replacement, caret, caret);
+  }
+
+  function clampNumber(value, min, max, fallback) {
+    const parsed = parseInt(value, 10);
+    if (Number.isNaN(parsed)) return fallback;
+    return Math.max(min, Math.min(max, parsed));
+  }
+
+  function buildMarkdownTable(columns, rows) {
+    const header = Array.from({ length: columns }, (_, index) => `Column ${index + 1}`).join(' | ');
+    const divider = Array.from({ length: columns }, () => '---').join(' | ');
+    const bodyRows = Array.from({ length: rows }, () => `| ${Array.from({ length: columns }, () => 'Value').join(' | ')} |`);
+    return `| ${header} |\n| ${divider} |\n${bodyRows.join('\n')}\n`;
+  }
+
+  function loadEmojiEntries() {
+    if (emojiLoadPromise) return emojiLoadPromise;
+    emojiLoadPromise = fetch(EMOJI_API_URL)
+      .then((response) => {
+        if (!response.ok) throw new Error(`Emoji request failed (${response.status})`);
+        return response.json();
+      })
+      .then((data) => {
+        emojiEntries = Object.keys(data)
+          .sort((a, b) => a.localeCompare(b))
+          .map((name) => ({
+            name,
+            url: data[name],
+            shortcode: `:${name}:`,
+            search: `${name} :${name}:`.toLowerCase(),
+          }));
+        emojiUrlMap = new Map(emojiEntries.map((entry) => [entry.name, entry.url]));
+        emojiLookupLoaded = true;
+        return emojiEntries;
+      })
+      .catch((error) => {
+        console.error('Failed to load GitHub emojis:', error);
+        emojiEntries = [];
+        emojiUrlMap = new Map();
+        emojiLookupLoaded = true;
+        return emojiEntries;
+      });
+    return emojiLoadPromise;
+  }
+
+  function createAlertPreview(type, meta) {
+    const wrapper = document.createElement('div');
+    wrapper.className = `markdown-alert markdown-alert-${type}`;
+    const title = document.createElement('p');
+    title.className = 'markdown-alert-title';
+    const icon = document.createElement('span');
+    icon.className = 'markdown-alert-icon';
+    icon.setAttribute('aria-hidden', 'true');
+    if (meta.path) {
+      const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+      svg.setAttribute('viewBox', meta.viewBox || '0 0 512 512');
+      const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+      path.setAttribute('d', meta.path);
+      svg.appendChild(path);
+      icon.appendChild(svg);
+    }
+    const label = document.createElement('span');
+    label.textContent = meta.label;
+    title.appendChild(icon);
+    title.appendChild(label);
+    const body = document.createElement('p');
+    body.textContent = `${meta.label} details go here.`;
+    wrapper.appendChild(title);
+    wrapper.appendChild(body);
+    return wrapper;
+  }
+
+  function flashCopyButton(button) {
+    const icon = button.querySelector('i');
+    if (!icon) return;
+    icon.className = 'bi bi-check-lg';
+    button.classList.add('is-copied');
+    clearTimeout(button.copyTimeout);
+    button.copyTimeout = setTimeout(() => {
+      icon.className = 'bi bi-clipboard';
+      button.classList.remove('is-copied');
+    }, 1200);
+  }
+
+  function openTableModal() {
+    const modal = document.getElementById('table-modal');
+    const columnInput = document.getElementById('table-modal-columns');
+    const rowInput = document.getElementById('table-modal-rows');
+    const confirmBtn = document.getElementById('table-modal-insert');
+    const cancelBtn = document.getElementById('table-modal-cancel');
+    if (!modal || !columnInput || !rowInput || !confirmBtn || !cancelBtn) return;
+    const start = markdownEditor.selectionStart;
+    const end = markdownEditor.selectionEnd;
+    columnInput.value = '3';
+    rowInput.value = '1';
+    modal.style.display = 'flex';
+
+    function insertTable() {
+      const columns = clampNumber(columnInput.value, 1, 20, 3);
+      const rows = clampNumber(rowInput.value, 1, 20, 1);
+      const table = buildMarkdownTable(columns, rows);
+      modal.style.display = 'none';
+      cleanup();
+      insertMarkdownBlock(table, start, end);
+    }
+
+    function closeModal() {
+      modal.style.display = 'none';
+      cleanup();
+    }
+
+    function onKey(e) {
+      if (e.key === 'Enter') {
+        e.preventDefault();
+        insertTable();
+      } else if (e.key === 'Escape') {
+        e.preventDefault();
+        closeModal();
+      }
+    }
+
+    function cleanup() {
+      confirmBtn.removeEventListener('click', insertTable);
+      cancelBtn.removeEventListener('click', closeModal);
+      columnInput.removeEventListener('keydown', onKey);
+      rowInput.removeEventListener('keydown', onKey);
+    }
+
+    confirmBtn.addEventListener('click', insertTable);
+    cancelBtn.addEventListener('click', closeModal);
+    columnInput.addEventListener('keydown', onKey);
+    rowInput.addEventListener('keydown', onKey);
+
+    requestAnimationFrame(() => {
+      columnInput.focus();
+      columnInput.select();
+    });
+  }
+
+  function openEmojiModal() {
+    const modal = document.getElementById('emoji-modal');
+    const grid = document.getElementById('emoji-modal-grid');
+    const emptyMessage = document.getElementById('emoji-modal-empty');
+    const searchInput = document.getElementById('emoji-modal-search');
+    const confirmBtn = document.getElementById('emoji-modal-insert');
+    const cancelBtn = document.getElementById('emoji-modal-cancel');
+    if (!modal || !grid || !emptyMessage || !searchInput || !confirmBtn || !cancelBtn) return;
+    const start = markdownEditor.selectionStart;
+    const end = markdownEditor.selectionEnd;
+    modal.style.display = 'flex';
+    confirmBtn.disabled = true;
+
+    // Localized and accessible announcements for loading
+    const loadingText = I18N_DICTS[activeLang].loadingEmojis || "Loading emojis...";
+    emptyMessage.textContent = loadingText;
+    emptyMessage.style.display = 'block';
+    announceToScreenReader(loadingText);
+
+    searchInput.value = '';
+    emojiSelection.clear();
+    // PERF-007: Clear elements using textContent
+    grid.textContent = '';
+    grid.scrollTop = 0;
+    emojiItems = [];
+
+    // Render visual placeholders during active requests
+    if (!emojiLookupLoaded) {
+      renderEmojiSkeletons();
+    }
+
+    let currentFilteredEntries = [];
+    let renderedCount = 0;
+    const CHUNK_SIZE = 120;
+
+    function updateInsertState() {
+      confirmBtn.disabled = emojiSelection.size === 0;
+    }
+
+    function toggleSelection(shortcode, element) {
+      if (emojiSelection.has(shortcode)) {
+        emojiSelection.delete(shortcode);
+        element.classList.remove('is-selected');
+      } else {
+        emojiSelection.add(shortcode);
+        element.classList.add('is-selected');
+      }
+      element.setAttribute('aria-pressed', emojiSelection.has(shortcode).toString());
+      updateInsertState();
+    }
+
+    function renderEmojiChunk(clear = false) {
+      if (clear) {
+        // PERF-007: Clear elements using textContent
+        grid.textContent = '';
+        emojiItems = [];
+        renderedCount = 0;
+      }
+
+      const nextBatch = currentFilteredEntries.slice(renderedCount, renderedCount + CHUNK_SIZE);
+      if (nextBatch.length === 0) {
+        emptyMessage.style.display = emojiItems.length ? 'none' : 'block';
+        return;
+      }
+
+      const fragment = document.createDocumentFragment();
+      const newItems = nextBatch.map((entry) => {
+        const item = document.createElement('button');
+        item.type = 'button';
+        item.className = 'emoji-item';
+
+        const isSelected = emojiSelection.has(entry.shortcode);
+        if (isSelected) {
+          item.classList.add('is-selected');
+        }
+        item.setAttribute('aria-pressed', isSelected ? 'true' : 'false');
+        item.dataset.search = entry.search;
+        item.dataset.shortcode = entry.shortcode;
+
+        const preview = document.createElement('span');
+        preview.className = 'emoji-preview';
+        const image = document.createElement('img');
+        image.src = entry.url;
+        image.alt = entry.shortcode;
+        image.loading = 'lazy';
+        preview.appendChild(image);
+
+        const shortcodeRow = document.createElement('div');
+        shortcodeRow.className = 'emoji-shortcode';
+        const code = document.createElement('span');
+        code.textContent = entry.shortcode;
+        const copyBtn = document.createElement('button');
+        copyBtn.type = 'button';
+        copyBtn.className = 'emoji-copy-btn';
+        copyBtn.setAttribute('aria-label', `Copy ${entry.shortcode}`);
+        copyBtn.innerHTML = '<i class="bi bi-clipboard"></i>';
+        copyBtn.addEventListener('click', (event) => {
+          event.stopPropagation();
+          copyTextToClipboard(entry.shortcode)
+            .then(() => flashCopyButton(copyBtn))
+            .catch((error) => console.error('Copy failed:', error));
+        });
+        shortcodeRow.appendChild(code);
+        shortcodeRow.appendChild(copyBtn);
+
+        item.appendChild(preview);
+        item.appendChild(shortcodeRow);
+        item.addEventListener('click', () => toggleSelection(entry.shortcode, item));
+        fragment.appendChild(item);
+        return { element: item, search: entry.search, shortcode: entry.shortcode };
+      });
+
+      emojiItems = emojiItems.concat(newItems);
+      grid.appendChild(fragment);
+      renderedCount += nextBatch.length;
+      emptyMessage.style.display = emojiItems.length ? 'none' : 'block';
+    }
+
+    function applyFilter() {
+      const query = searchInput.value.trim().toLowerCase();
+      currentFilteredEntries = emojiEntries.filter(entry => !query || entry.search.includes(query));
+      renderEmojiChunk(true);
+    }
+
+    function handleScroll() {
+      if (grid.scrollTop + grid.clientHeight >= grid.scrollHeight - 60) {
+        renderEmojiChunk(false);
+      }
+    }
+
+    function insertEmojis() {
+      if (!emojiSelection.size) return;
+      // Retain the modal's visual/definition order of emojis, consistent with the Symbols modal
+      const ordered = emojiItems
+        .filter(item => emojiSelection.has(item.shortcode))
+        .map(item => item.shortcode);
+      const insertion = ordered.join(' ');
+      modal.style.display = 'none';
+      cleanup();
+      replaceEditorRange(start, end, insertion, start + insertion.length, start + insertion.length);
+    }
+
+    function closeModal() {
+      modal.style.display = 'none';
+      cleanup();
+    }
+
+    function onKey(e) {
+      if (e.key === 'Escape') {
+        e.preventDefault();
+        closeModal();
+      }
+    }
+
+    function cleanup() {
+      confirmBtn.removeEventListener('click', insertEmojis);
+      cancelBtn.removeEventListener('click', closeModal);
+      searchInput.removeEventListener('input', applyFilter);
+      searchInput.removeEventListener('keydown', onKey);
+      grid.removeEventListener('scroll', handleScroll);
+    }
+
+    loadEmojiEntries().then((entries) => {
+      if (!entries.length) {
+        emptyMessage.textContent = 'Unable to load emojis.';
+        emptyMessage.style.display = 'block';
+        // PERF-007: Clear elements using textContent
+        grid.textContent = '';
+        emojiItems = [];
+        announceToScreenReader("Failed to load emojis.");
+        return;
+      }
+      currentFilteredEntries = entries;
+      renderEmojiChunk(true);
+      emptyMessage.textContent = 'No emojis found.';
+      announceToScreenReader("Emojis loaded. " + entries.length + " items available.");
+      updateInsertState();
+    });
+
+    confirmBtn.addEventListener('click', insertEmojis);
+    cancelBtn.addEventListener('click', closeModal);
+    searchInput.addEventListener('input', applyFilter);
+    searchInput.addEventListener('keydown', onKey);
+    grid.addEventListener('scroll', handleScroll);
+
+    requestAnimationFrame(() => searchInput.focus());
+  }
+
+  function openSymbolsModal() {
+    const modal = document.getElementById('symbols-modal');
+    const grid = document.getElementById('symbols-modal-grid');
+    const emptyMessage = document.getElementById('symbols-modal-empty');
+    const searchInput = document.getElementById('symbols-modal-search');
+    const confirmBtn = document.getElementById('symbols-modal-insert');
+    const cancelBtn = document.getElementById('symbols-modal-cancel');
+    if (!modal || !grid || !emptyMessage || !searchInput || !confirmBtn || !cancelBtn) return;
+    const start = markdownEditor.selectionStart;
+    const end = markdownEditor.selectionEnd;
+    modal.style.display = 'flex';
+    confirmBtn.disabled = true;
+    searchInput.value = '';
+    symbolSelection.clear();
+    // PERF-007: Clear elements using textContent
+    grid.textContent = '';
+
+    const sectionEntries = [];
+    SYMBOL_SECTIONS.forEach((section) => {
+      const sectionWrapper = document.createElement('div');
+      sectionWrapper.className = 'symbol-section';
+      const title = document.createElement('p');
+      title.className = 'symbol-section-title';
+      title.textContent = section.title;
+      const sectionGrid = document.createElement('div');
+      sectionGrid.className = 'symbol-section-grid';
+      const sectionItems = section.items.map((entry) => {
+        const item = document.createElement('button');
+        item.type = 'button';
+        item.className = 'symbol-item';
+        item.setAttribute('aria-pressed', 'false');
+        const preview = document.createElement('span');
+        preview.className = 'symbol-preview';
+        preview.textContent = entry.symbol;
+        const codeRow = document.createElement('div');
+        codeRow.className = 'symbol-code';
+        const code = document.createElement('span');
+        code.textContent = entry.entity;
+        const copyBtn = document.createElement('button');
+        copyBtn.type = 'button';
+        copyBtn.className = 'symbol-copy-btn';
+        copyBtn.setAttribute('aria-label', `Copy ${entry.entity}`);
+        copyBtn.innerHTML = '<i class="bi bi-clipboard"></i>';
+        copyBtn.addEventListener('click', (event) => {
+          event.stopPropagation();
+          copyTextToClipboard(entry.entity)
+            .then(() => flashCopyButton(copyBtn))
+            .catch((error) => console.error('Copy failed:', error));
+        });
+        codeRow.appendChild(code);
+        codeRow.appendChild(copyBtn);
+        item.appendChild(preview);
+        item.appendChild(codeRow);
+
+        item.dataset.search = `${entry.symbol} ${entry.entity} ${entry.name}`.toLowerCase();
+        item.dataset.entity = entry.entity;
+        item.addEventListener('click', () => {
+          if (symbolSelection.has(entry.entity)) {
+            symbolSelection.delete(entry.entity);
+            item.classList.remove('is-selected');
+          } else {
+            symbolSelection.add(entry.entity);
+            item.classList.add('is-selected');
+          }
+          item.setAttribute('aria-pressed', symbolSelection.has(entry.entity).toString());
+          confirmBtn.disabled = symbolSelection.size === 0;
+        });
+
+        sectionGrid.appendChild(item);
+        return { element: item, search: item.dataset.search, entity: entry.entity };
+      });
+      sectionWrapper.appendChild(title);
+      sectionWrapper.appendChild(sectionGrid);
+      grid.appendChild(sectionWrapper);
+      sectionEntries.push({ wrapper: sectionWrapper, items: sectionItems });
+    });
+
+    symbolItems = sectionEntries.flatMap((section) => section.items);
+
+    function applyFilter() {
+      const query = searchInput.value.trim().toLowerCase();
+      let visibleCount = 0;
+      sectionEntries.forEach((section) => {
+        let sectionVisible = 0;
+        section.items.forEach((item) => {
+          const match = !query || item.search.includes(query);
+          item.element.style.display = match ? '' : 'none';
+          if (match) {
+            visibleCount += 1;
+            sectionVisible += 1;
+          }
+        });
+        section.wrapper.style.display = sectionVisible ? '' : 'none';
+      });
+      emptyMessage.style.display = visibleCount ? 'none' : 'block';
+    }
+
+    function insertSymbols() {
+      if (!symbolSelection.size) return;
+      const ordered = symbolItems
+        .filter((item) => symbolSelection.has(item.entity))
+        .map((item) => item.entity);
+      const insertion = ordered.join(' ');
+      modal.style.display = 'none';
+      cleanup();
+      replaceEditorRange(start, end, insertion, start + insertion.length, start + insertion.length);
+    }
+
+    function closeModal() {
+      modal.style.display = 'none';
+      cleanup();
+    }
+
+    function onKey(e) {
+      if (e.key === 'Escape') {
+        e.preventDefault();
+        closeModal();
+      }
+    }
+
+    function cleanup() {
+      confirmBtn.removeEventListener('click', insertSymbols);
+      cancelBtn.removeEventListener('click', closeModal);
+      searchInput.removeEventListener('input', applyFilter);
+      searchInput.removeEventListener('keydown', onKey);
+    }
+
+    emptyMessage.textContent = 'No symbols found.';
+    applyFilter();
+    confirmBtn.addEventListener('click', insertSymbols);
+    cancelBtn.addEventListener('click', closeModal);
+    searchInput.addEventListener('input', applyFilter);
+    searchInput.addEventListener('keydown', onKey);
+    requestAnimationFrame(() => searchInput.focus());
+  }
+
+  function openAlertModal() {
+    const modal = document.getElementById('alert-modal');
+    const grid = document.getElementById('alert-modal-grid');
+    const confirmBtn = document.getElementById('alert-modal-insert');
+    const cancelBtn = document.getElementById('alert-modal-cancel');
+    if (!modal || !grid || !confirmBtn || !cancelBtn) return;
+    const start = markdownEditor.selectionStart;
+    const end = markdownEditor.selectionEnd;
+    modal.style.display = 'flex';
+    // PERF-007: Clear elements using textContent
+    grid.textContent = '';
+
+    const alertTypes = ['note', 'tip', 'important', 'warning', 'caution'];
+    let selectedType = alertTypes[0];
+    const options = [];
+    alertTypes.forEach((type) => {
+      const meta = GITHUB_ALERT_META[type] || { label: type };
+      const option = document.createElement('button');
+      option.type = 'button';
+      option.className = 'alert-option';
+      option.dataset.alertType = type;
+      option.setAttribute('aria-pressed', (type === selectedType).toString());
+      const preview = document.createElement('div');
+      preview.className = 'alert-preview';
+      preview.appendChild(createAlertPreview(type, meta));
+      option.appendChild(preview);
+      if (type === selectedType) option.classList.add('is-selected');
+      option.addEventListener('click', () => {
+        selectedType = type;
+        options.forEach((item) => {
+          const isSelected = item === option;
+          item.classList.toggle('is-selected', isSelected);
+          item.setAttribute('aria-pressed', isSelected.toString());
+        });
+      });
+      options.push(option);
+      grid.appendChild(option);
+    });
+
+    function insertAlert() {
+      const type = selectedType.toUpperCase();
+      const meta = GITHUB_ALERT_META[selectedType] || { label: selectedType };
+      const body = `${meta.label} details go here.`;
+      const block = `> [!${type}]\n> ${body}\n`;
+      modal.style.display = 'none';
+      cleanup();
+      insertMarkdownBlock(block, start, end);
+    }
+
+    function closeModal() {
+      modal.style.display = 'none';
+      cleanup();
+    }
+
+    function onKey(e) {
+      if (e.key === 'Escape') {
+        e.preventDefault();
+        closeModal();
+      }
+    }
+
+    function cleanup() {
+      confirmBtn.removeEventListener('click', insertAlert);
+      cancelBtn.removeEventListener('click', closeModal);
+      modal.removeEventListener('keydown', onKey);
+    }
+
+    confirmBtn.addEventListener('click', insertAlert);
+    cancelBtn.addEventListener('click', closeModal);
+    modal.addEventListener('keydown', onKey);
+  }
+
+  function insertMarkdownLink() {
+    const modal = document.getElementById('link-modal');
+    const urlInput = document.getElementById('link-modal-url');
+    const textInput = document.getElementById('link-modal-text');
+    const confirmBtn = document.getElementById('link-modal-apply');
+    const cancelBtn = document.getElementById('link-modal-cancel');
+    if (!modal || !urlInput || !textInput || !confirmBtn || !cancelBtn) return;
+    const start = markdownEditor.selectionStart;
+    const end = markdownEditor.selectionEnd;
+    const selected = markdownEditor.value.slice(start, end);
+    urlInput.value = 'https://';
+    textInput.value = selected || '';
+    modal.style.display = 'flex';
+
+    function applyLink() {
+      const url = urlInput.value.trim() || 'https://';
+      const linkText = textInput.value.trim() || selected || 'link text';
+      const replacement = '[' + linkText + '](' + url + ')';
+      modal.style.display = 'none';
+      cleanup();
+      replaceEditorRange(start, end, replacement, start + replacement.length, start + replacement.length);
+    }
+
+    function closeModal() {
+      modal.style.display = 'none';
+      cleanup();
+    }
+
+    function onKey(e) {
+      if (e.key === 'Enter') {
+        e.preventDefault();
+        applyLink();
+      } else if (e.key === 'Escape') {
+        e.preventDefault();
+        closeModal();
+      }
+    }
+
+    function cleanup() {
+      confirmBtn.removeEventListener('click', applyLink);
+      cancelBtn.removeEventListener('click', closeModal);
+      urlInput.removeEventListener('keydown', onKey);
+      textInput.removeEventListener('keydown', onKey);
+    }
+
+    confirmBtn.addEventListener('click', applyLink);
+    cancelBtn.addEventListener('click', closeModal);
+    urlInput.addEventListener('keydown', onKey);
+    textInput.addEventListener('keydown', onKey);
+
+    requestAnimationFrame(function() {
+      urlInput.focus();
+      urlInput.select();
+    });
+  }
+
+  function insertMarkdownImage() {
+    const modal = document.getElementById('image-modal');
+    const uploadOption = document.getElementById('image-source-upload');
+    const urlOption = document.getElementById('image-source-url');
+    const uploadFields = document.getElementById('image-upload-fields');
+    const urlFields = document.getElementById('image-url-fields');
+    const fileInput = document.getElementById('image-modal-file');
+    const urlInput = document.getElementById('image-modal-url');
+    const altInput = document.getElementById('image-modal-alt');
+    const confirmBtn = document.getElementById('image-modal-insert');
+    const cancelBtn = document.getElementById('image-modal-cancel');
+    if (!modal || !uploadOption || !urlOption || !uploadFields || !urlFields || !fileInput || !urlInput || !altInput || !confirmBtn || !cancelBtn) return;
+    const start = markdownEditor.selectionStart;
+    const end = markdownEditor.selectionEnd;
+    const selected = markdownEditor.value.slice(start, end);
+    urlInput.value = 'https://';
+    altInput.value = selected || '';
+    fileInput.value = '';
+    urlOption.checked = true;
+    uploadOption.checked = false;
+    modal.style.display = 'flex';
+
+    function buildImageMarkdown(url) {
+      const titleText = altInput.value.trim();
+      const altText = titleText || 'alt text';
+      const safeTitle = sanitizeMarkdownTitle(titleText);
+      const titlePart = safeTitle ? ' "' + safeTitle + '"' : '';
+      return '![' + altText + '](' + url + titlePart + ')';
+    }
+
+    function insertImage(url) {
+      const safeUrl = url.trim() || 'https://';
+      const replacement = buildImageMarkdown(safeUrl);
+      modal.style.display = 'none';
+      cleanup();
+      replaceEditorRange(start, end, replacement, start + replacement.length, start + replacement.length);
+    }
+
+    function insertFromFile(file) {
+      const objectUrl = URL.createObjectURL(file);
+      imageObjectUrls.add(objectUrl);
+      insertImage(objectUrl);
+    }
+
+    function updateMode(shouldFocus) {
+      const isUpload = uploadOption.checked;
+      uploadFields.style.display = isUpload ? 'flex' : 'none';
+      urlFields.style.display = isUpload ? 'none' : 'flex';
+      if (shouldFocus) {
+        requestAnimationFrame(function() {
+          if (isUpload) {
+            fileInput.focus();
+          } else {
+            urlInput.focus();
+            urlInput.select();
+          }
+        });
+      }
+    }
+
+    function onModeChange() {
+      updateMode(true);
+    }
+
+    function onFileChange() {
+      const file = fileInput.files && fileInput.files[0];
+      if (file) {
+        insertFromFile(file);
+      }
+    }
+
+    function onKey(e) {
+      if (e.key === 'Enter') {
+        e.preventDefault();
+        if (uploadOption.checked) {
+          const file = fileInput.files && fileInput.files[0];
+          if (file) insertFromFile(file);
+          else fileInput.click();
+        } else {
+          insertImage(urlInput.value);
+        }
+      } else if (e.key === 'Escape') {
+        e.preventDefault();
+        closeModal();
+      }
+    }
+
+    function closeModal() {
+      modal.style.display = 'none';
+      cleanup();
+    }
+
+    function cleanup() {
+      confirmBtn.removeEventListener('click', onConfirm);
+      cancelBtn.removeEventListener('click', closeModal);
+      uploadOption.removeEventListener('change', onModeChange);
+      urlOption.removeEventListener('change', onModeChange);
+      fileInput.removeEventListener('change', onFileChange);
+      fileInput.removeEventListener('keydown', onKey);
+      urlInput.removeEventListener('keydown', onKey);
+      altInput.removeEventListener('keydown', onKey);
+    }
+
+    function onConfirm() {
+      if (uploadOption.checked) {
+        const file = fileInput.files && fileInput.files[0];
+        if (file) insertFromFile(file);
+        else fileInput.click();
+      } else {
+        insertImage(urlInput.value);
+      }
+    }
+
+    confirmBtn.addEventListener('click', onConfirm);
+    cancelBtn.addEventListener('click', closeModal);
+    uploadOption.addEventListener('change', onModeChange);
+    urlOption.addEventListener('change', onModeChange);
+    fileInput.addEventListener('change', onFileChange);
+    fileInput.addEventListener('keydown', onKey);
+    urlInput.addEventListener('keydown', onKey);
+    altInput.addEventListener('keydown', onKey);
+    updateMode(true);
+  }
+
+  function insertMarkdownReference() {
+    const modal = document.getElementById('reference-modal');
+    const numberInput = document.getElementById('reference-modal-number');
+    const urlInput = document.getElementById('reference-modal-url');
+    const titleInput = document.getElementById('reference-modal-title-input');
+    const confirmBtn = document.getElementById('reference-modal-apply');
+    const cancelBtn = document.getElementById('reference-modal-cancel');
+    if (!modal || !numberInput || !urlInput || !titleInput || !confirmBtn || !cancelBtn) return;
+    const start = markdownEditor.selectionStart;
+    const end = markdownEditor.selectionEnd;
+    const currentValue = markdownEditor.value;
+    const used = getUsedReferenceNumbers(currentValue);
+    const maxUsed = used.size ? Math.max(...used) : 0;
+    referenceCounter = Math.max(1, maxUsed + 1);
+    const suggestedNumber = getNextAvailableReferenceNumber(used, referenceCounter);
+    numberInput.value = '[' + suggestedNumber + ']';
+    urlInput.value = 'https://';
+    titleInput.value = '';
+    modal.style.display = 'flex';
+
+    function insertReference() {
+      const latestValue = markdownEditor.value;
+      const usedNumbers = getUsedReferenceNumbers(latestValue);
+      const parsed = parseInt(numberInput.value.replace(/[^\d]/g, ''), 10);
+      const baseNumber = Number.isNaN(parsed) ? suggestedNumber : parsed;
+      const finalNumber = getNextAvailableReferenceNumber(usedNumbers, baseNumber);
+      const url = urlInput.value.trim() || 'https://';
+      const title = titleInput.value.trim();
+      const safeTitle = sanitizeMarkdownTitle(title);
+      const definition = '[' + finalNumber + ']: ' + url + (safeTitle ? ' "' + safeTitle + '"' : '');
+      const selected = latestValue.slice(start, end);
+      const inlineReference = selected + '[' + finalNumber + ']';
+      const baseValue = latestValue.slice(0, start) + inlineReference + latestValue.slice(end);
+      let separator = '';
+      if (baseValue.length && !baseValue.endsWith('\n')) {
+        separator = '\n';
+      }
+      const updatedValue = baseValue + separator + definition;
+      markdownEditor.value = updatedValue;
+      markdownEditor.focus();
+      const caret = start + inlineReference.length;
+      markdownEditor.setSelectionRange(caret, caret);
+      markdownEditor.dispatchEvent(new Event('input', { bubbles: true }));
+      referenceCounter = Math.max(referenceCounter, finalNumber + 1);
+      modal.style.display = 'none';
+      cleanup();
+    }
+
+    function closeModal() {
+      modal.style.display = 'none';
+      cleanup();
+    }
+
+    function onKey(e) {
+      if (e.key === 'Enter') {
+        e.preventDefault();
+        insertReference();
+      } else if (e.key === 'Escape') {
+        e.preventDefault();
+        closeModal();
+      }
+    }
+
+    function cleanup() {
+      confirmBtn.removeEventListener('click', insertReference);
+      cancelBtn.removeEventListener('click', closeModal);
+      numberInput.removeEventListener('keydown', onKey);
+      urlInput.removeEventListener('keydown', onKey);
+      titleInput.removeEventListener('keydown', onKey);
+    }
+
+    confirmBtn.addEventListener('click', insertReference);
+    cancelBtn.addEventListener('click', closeModal);
+    numberInput.addEventListener('keydown', onKey);
+    urlInput.addEventListener('keydown', onKey);
+    titleInput.addEventListener('keydown', onKey);
+
+    requestAnimationFrame(function() {
+      numberInput.focus();
+      numberInput.select();
+    });
+  }
+
+  function escapeRegExp(text) {
+    return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+  }
+
+  function getFocusableElements(container) {
+    return Array.from(container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'))
+      .filter(element => !element.disabled && element.offsetParent !== null);
+  }
+
+  function trapFocusInModal(modal, event) {
+    const focusable = getFocusableElements(modal);
+    if (!focusable.length) {
+      event.preventDefault();
+      return;
+    }
+    const first = focusable[0];
+    const last = focusable[focusable.length - 1];
+    if (event.shiftKey && document.activeElement === first) {
+      event.preventDefault();
+      last.focus();
+    } else if (!event.shiftKey && document.activeElement === last) {
+      event.preventDefault();
+      first.focus();
+    }
+  }
+
+  function openAppModal(modal, options = {}) {
+    if (!modal) return;
+    if (activeModal && activeModal !== modal) {
+      closeAppModal(activeModal);
+    }
+    lastFocusedElement = document.activeElement;
+    modal.style.display = 'flex';
+    requestAnimationFrame(function() {
+      modal.classList.add('is-visible');
+    });
+    modal.setAttribute('aria-hidden', 'false');
+    activeModal = modal;
+    const focusTarget = options.focusTarget || getFocusableElements(modal)[0];
+    if (focusTarget) {
+      focusTarget.focus();
+    }
+    const handleKeydown = function(event) {
+      if (event.key === 'Escape') {
+        event.preventDefault();
+        if (options.onClose) {
+          options.onClose();
+        } else {
+          closeAppModal(modal);
+        }
+      } else if (event.key === 'Tab') {
+        trapFocusInModal(modal, event);
+      }
+    };
+    const handlePointerDown = function(event) {
+      if (event.target === modal) {
+        if (options.onClose) {
+          options.onClose();
+        } else {
+          closeAppModal(modal);
+        }
+      }
+    };
+    modal.addEventListener('keydown', handleKeydown);
+    modal.addEventListener('mousedown', handlePointerDown);
+    modal._modalHandlers = { handleKeydown, handlePointerDown };
+  }
+
+  function closeAppModal(modal) {
+    if (!modal) return;
+    modal.classList.remove('is-visible');
+    modal.setAttribute('aria-hidden', 'true');
+    const handlers = modal._modalHandlers || {};
+    if (handlers.handleKeydown) modal.removeEventListener('keydown', handlers.handleKeydown);
+    if (handlers.handlePointerDown) modal.removeEventListener('mousedown', handlers.handlePointerDown);
+    if (activeModal === modal) activeModal = null;
+    window.setTimeout(function() {
+      if (!modal.classList.contains('is-visible')) {
+        modal.style.display = 'none';
+      }
+    }, 200);
+    if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') {
+      lastFocusedElement.focus();
+    }
+  }
+
+  function updateFindHighlights() {
+    updatePreviewFindHighlights();
+
+    if (!editorHighlightLayer) return;
+    if (!isEditorVisible()) return;
+    if (!isFindModalOpen || !findReplaceInput || !findReplaceInput.value || !findMatches.length) {
+      if (editorHighlightLayer.textContent !== '') {
+        editorHighlightLayer.textContent = '';
+      }
+      return;
+    }
+    const text = markdownEditor.value || '';
+    const scrollTop = cachedScrollTop;
+    const scrollLeft = cachedScrollLeft;
+    const fragment = document.createDocumentFragment();
+    let lastIndex = 0;
+    findMatches.forEach(function(match, index) {
+      fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.start)));
+      const mark = document.createElement('mark');
+      mark.className = 'find-highlight' + (index === activeFindIndex ? ' active' : '');
+      mark.textContent = text.slice(match.start, match.end);
+      fragment.appendChild(mark);
+      lastIndex = match.end;
+    });
+    fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
+    editorHighlightLayer.textContent = '';
+    editorHighlightLayer.appendChild(fragment);
+    editorHighlightLayer.scrollTop = scrollTop;
+    editorHighlightLayer.scrollLeft = scrollLeft;
+  }
+
+  let previewHighlights = [];
+  let activePreviewHighlightIndex = -1;
+
+  function isPreviewVisible() {
+    return currentViewMode === 'preview' || currentViewMode === 'split';
+  }
+
+  function clearPreviewFindHighlights() {
+    if (!markdownPreview) return;
+    const highlights = markdownPreview.querySelectorAll('.preview-find-highlight');
+    highlights.forEach(function(el) {
+      const parent = el.parentNode;
+      if (parent) {
+        parent.replaceChild(document.createTextNode(el.textContent), el);
+      }
+    });
+    markdownPreview.normalize();
+  }
+
+  function highlightPreviewText(node, regex) {
+    if (!node) return;
+    if (node.nodeType === Node.TEXT_NODE) {
+      const val = node.nodeValue;
+      if (!val) return;
+      
+      regex.lastIndex = 0;
+      let match;
+      const matches = [];
+      while ((match = regex.exec(val)) !== null) {
+        if (match[0].length === 0) {
+          regex.lastIndex++;
+          continue;
+        }
+        matches.push({
+          start: match.index,
+          end: match.index + match[0].length,
+          text: match[0]
+        });
+      }
+      
+      if (matches.length > 0) {
+        const parent = node.parentNode;
+        if (!parent) return;
+        
+        const fragment = document.createDocumentFragment();
+        let lastIdx = 0;
+        
+        matches.forEach(function(m) {
+          if (m.start > lastIdx) {
+            fragment.appendChild(document.createTextNode(val.slice(lastIdx, m.start)));
+          }
+          const mark = document.createElement('mark');
+          mark.className = 'preview-find-highlight';
+          mark.textContent = m.text;
+          fragment.appendChild(mark);
+          lastIdx = m.end;
+        });
+        
+        if (lastIdx < val.length) {
+          fragment.appendChild(document.createTextNode(val.slice(lastIdx)));
+        }
+        
+        parent.replaceChild(fragment, node);
+      }
+    } else if (node.nodeType === Node.ELEMENT_NODE) {
+      const tagName = node.tagName.toLowerCase();
+      if (tagName === 'script' || tagName === 'style' || tagName === 'textarea' || tagName === 'noscript' || tagName === 'svg') {
+        return;
+      }
+      if (node.classList.contains('mermaid') || node.classList.contains('mjx-container') || node.closest('.mermaid') || node.closest('.mjx-container')) {
+        return;
+      }
+      
+      const children = Array.from(node.childNodes);
+      children.forEach(function(child) {
+        highlightPreviewText(child, regex);
+      });
+    }
+  }
+
+  function updatePreviewFindHighlights() {
+    clearPreviewFindHighlights();
+    previewHighlights = [];
+    
+    if (!isFindModalOpen || !findReplaceInput || !findReplaceInput.value || !isPreviewVisible()) {
+      return;
+    }
+    
+    const query = findReplaceInput.value;
+    const isRegex = document.getElementById('find-regex').classList.contains('active');
+    const isCaseSensitive = document.getElementById('find-case').classList.contains('active');
+    const isWholeWord = document.getElementById('find-word').classList.contains('active');
+    
+    let regex;
+    try {
+      let pattern = isRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+      if (isWholeWord) {
+        pattern = `\\b${pattern}\\b`;
+      }
+      const flags = isCaseSensitive ? 'g' : 'gi';
+      regex = new RegExp(pattern, flags);
+    } catch (e) {
+      return;
+    }
+    
+    highlightPreviewText(markdownPreview, regex);
+    previewHighlights = Array.from(markdownPreview.querySelectorAll('.preview-find-highlight'));
+    updateActivePreviewHighlight();
+  }
+
+  function updateActivePreviewHighlight() {
+    previewHighlights.forEach(function(el) {
+      el.classList.remove('active');
+    });
+    
+    if (!previewHighlights.length) {
+      activePreviewHighlightIndex = -1;
+      return;
+    }
+    
+    if (findMatches.length > 0 && activeFindIndex >= 0) {
+      const ratio = activeFindIndex / findMatches.length;
+      activePreviewHighlightIndex = Math.min(
+        previewHighlights.length - 1,
+        Math.floor(ratio * previewHighlights.length)
+      );
+    } else {
+      activePreviewHighlightIndex = 0;
+    }
+    
+    if (activePreviewHighlightIndex >= 0 && activePreviewHighlightIndex < previewHighlights.length) {
+      const activeEl = previewHighlights[activePreviewHighlightIndex];
+      activeEl.classList.add('active');
+      scrollPreviewHighlightIntoView(activeEl);
+    }
+  }
+
+  function scrollPreviewHighlightIntoView(element) {
+    if (!element || !previewPane) return;
+    const paneRect = previewPane.getBoundingClientRect();
+    const elemRect = element.getBoundingClientRect();
+    const isVisible = (
+      elemRect.top >= paneRect.top + 40 &&
+      elemRect.bottom <= paneRect.bottom - 40
+    );
+    if (!isVisible) {
+      const scrollTop = previewPane.scrollTop + (elemRect.top - paneRect.top) - (paneRect.height / 2) + (elemRect.height / 2);
+      previewPane.scrollTop = scrollTop;
+    }
+  }
+
+  function syncHighlightScroll() {
+    if (!editorHighlightLayer) return;
+    editorHighlightLayer.scrollTop = cachedScrollTop;
+    editorHighlightLayer.scrollLeft = cachedScrollLeft;
+  }
+
+  function scheduleEditorOverlayScrollSync() {
+    if (editorOverlayScrollFrame) return;
+    editorOverlayScrollFrame = requestAnimationFrame(function() {
+      editorOverlayScrollFrame = null;
+      syncHighlightScroll();
+      syncLineNumberScroll();
+    });
+  }
+
+  function updateLineNumberGutter(lineCount) {
+    if (!editorPaneElement) return;
+    const digits = String(Math.max(1, lineCount)).length;
+    const gutterSize = `${Math.max(LINE_NUMBER_GUTTER_MIN_CH, digits + LINE_NUMBER_GUTTER_PADDING_CH)}ch`;
+    editorPaneElement.style.setProperty('--line-number-gutter', gutterSize);
+  }
+
+  function getLineHeight(styles) {
+    const computed = parseFloat(styles.lineHeight);
+    if (!Number.isNaN(computed)) return computed;
+    const fontSize = parseFloat(styles.fontSize) || 14;
+    return fontSize * 1.5;
+  }
+
+  function getWrappedLineCountMonospace(lineText, maxCharsPerLine) {
+    if (!lineText) return 1;
+    const words = lineText.replace(/\t/g, '    ').split(' ');
+    let linesCount = 1;
+    let currentLineLength = 0;
+    
+    for (let i = 0; i < words.length; i++) {
+      const word = words[i];
+      const wordLength = word.length;
+      
+      if (wordLength === 0) {
+        if (currentLineLength + 1 > maxCharsPerLine) {
+          linesCount++;
+          currentLineLength = 1;
+        } else {
+          currentLineLength++;
+        }
+        continue;
+      }
+      
+      if (wordLength > maxCharsPerLine) {
+        const remainingSpace = maxCharsPerLine - currentLineLength;
+        if (remainingSpace > 0 && currentLineLength > 0) {
+          const firstPart = wordLength - remainingSpace;
+          linesCount += 1 + Math.floor(firstPart / maxCharsPerLine);
+          currentLineLength = firstPart % maxCharsPerLine;
+        } else {
+          linesCount += Math.floor(wordLength / maxCharsPerLine);
+          currentLineLength = wordLength % maxCharsPerLine;
+        }
+        continue;
+      }
+      
+      const spaceRequired = currentLineLength === 0 ? 0 : 1;
+      if (currentLineLength + spaceRequired + wordLength > maxCharsPerLine) {
+        linesCount++;
+        currentLineLength = wordLength;
+      } else {
+        currentLineLength += spaceRequired + wordLength;
+      }
+    }
+    
+    return Math.max(1, linesCount);
+  }
+
+  const lineCache = new Map();
+  let cachedPaddingLeft = 10;
+  let cachedPaddingRight = 10;
+  let cachedCharWidth = 0;
+  let cachedLineHeight = 21;
+  let cachedEditorWidth = 0;
+  let cachedMaxCharsPerLine = 80;
+  let cachedScrollTop = 0;
+  let cachedScrollLeft = 0;
+  let isGeometryInitialized = false;
+  let lastLineNumberLineCount = 0;
+  let lastLineNumberDocumentLength = 0;
+
+  function countLinesFast(text) {
+    if (!text) return 1;
+    let count = 1;
+    for (let i = 0; i < text.length; i += 1) {
+      if (text.charCodeAt(i) === 10) count += 1;
+    }
+    return count;
+  }
+
+  function countLinesBeforeIndex(text, endIndex) {
+    let count = 0;
+    const max = Math.max(0, Math.min(text.length, endIndex));
+    for (let i = 0; i < max; i += 1) {
+      if (text.charCodeAt(i) === 10) count += 1;
+    }
+    return count;
+  }
+
+  function initEditorGeometry() {
+    if (!markdownEditor) return;
+    const styles = window.getComputedStyle(markdownEditor);
+    cachedPaddingLeft = parseFloat(styles.paddingLeft) || 10;
+    cachedPaddingRight = parseFloat(styles.paddingRight) || 10;
+    
+    // Measure character width
+    const testSpan = document.createElement('span');
+    testSpan.style.fontFamily = styles.fontFamily;
+    testSpan.style.fontSize = styles.fontSize;
+    testSpan.style.visibility = 'hidden';
+    testSpan.style.position = 'absolute';
+    testSpan.style.whiteSpace = 'pre';
+    testSpan.textContent = 'a'.repeat(100);
+    document.body.appendChild(testSpan);
+    cachedCharWidth = testSpan.getBoundingClientRect().width / 100;
+    document.body.removeChild(testSpan);
+    
+    // Calculate line height
+    const computed = parseFloat(styles.lineHeight);
+    if (!Number.isNaN(computed)) {
+      cachedLineHeight = computed;
+    } else {
+      const fontSize = parseFloat(styles.fontSize) || 14;
+      cachedLineHeight = fontSize * 1.5;
+    }
+    
+    isGeometryInitialized = true;
+    lineCache.clear();
+  }
+
+  function refreshEditorWidth() {
+    if (!markdownEditor) return;
+    if (!isGeometryInitialized) {
+      initEditorGeometry();
+    }
+    cachedEditorWidth = markdownEditor.clientWidth;
+    const availableWidth = cachedEditorWidth - cachedPaddingLeft - cachedPaddingRight;
+    const nextMaxCharsPerLine = Math.max(1, Math.floor(availableWidth / cachedCharWidth));
+    if (nextMaxCharsPerLine !== cachedMaxCharsPerLine) {
+      cachedMaxCharsPerLine = nextMaxCharsPerLine;
+      lineCache.clear();
+    }
+    
+    cachedScrollTop = markdownEditor.scrollTop;
+    cachedScrollLeft = markdownEditor.scrollLeft;
+  }
+
+  function updateLineNumberItem(item, index, lineText, lineHeight) {
+    if (!item) return;
+    let wrapHeight = lineCache.get(lineText);
+    if (wrapHeight === undefined) {
+      const wrapCount = getWrappedLineCountMonospace(lineText, cachedMaxCharsPerLine);
+      wrapHeight = wrapCount * lineHeight;
+      if (lineCache.size >= LINE_CACHE_MAX_ENTRIES) {
+        lineCache.clear();
+      }
+      lineCache.set(lineText, wrapHeight);
+    }
+
+    const targetText = String(index + 1);
+    if (item.textContent !== targetText) {
+      item.textContent = targetText;
+    }
+    const targetHeight = `${wrapHeight}px`;
+    if (item.style.height !== targetHeight) {
+      item.style.height = targetHeight;
+    }
+  }
+
+  function updateActiveLineNumberHeight(text, lineCount, lineHeight) {
+    const caret = markdownEditor.selectionStart || 0;
+    const lineIndex = Math.min(lineCount - 1, countLinesBeforeIndex(text, caret));
+    const lineStart = text.lastIndexOf('\n', Math.max(0, caret - 1)) + 1;
+    const lineEndIndex = text.indexOf('\n', caret);
+    const lineEnd = lineEndIndex === -1 ? text.length : lineEndIndex;
+    updateLineNumberItem(lineNumbers.children[lineIndex], lineIndex, text.slice(lineStart, lineEnd), lineHeight);
+  }
+
+  function updateLineNumbers(options) {
+    const opts = options || {};
+    if (!lineNumbers || !markdownEditor) return;
+    if (!isEditorVisible()) return;
+    const text = markdownEditor.value || '';
+    const lineCount = countLinesFast(text);
+
+    if (cachedEditorWidth === 0) {
+      refreshEditorWidth();
+    }
+
+    updateLineNumberGutter(lineCount);
+    const lineHeight = cachedLineHeight;
+
+    const existingItems = lineNumbers.children;
+    const existingCount = existingItems.length;
+    const isLargeEditorDocument = text.length >= LARGE_DOCUMENT_THRESHOLD;
+    const canUseActiveLineFastPath =
+      isLargeEditorDocument &&
+      !opts.force &&
+      lineCount === lastLineNumberLineCount &&
+      existingCount === lineCount;
+
+    if (canUseActiveLineFastPath) {
+      updateActiveLineNumberHeight(text, lineCount, lineHeight);
+      lastLineNumberDocumentLength = text.length;
+      syncLineNumberScroll();
+      return;
+    }
+
+    const lines = text.split('\n');
+
+    // Adjust the number of DOM elements in-place to avoid complete tear-down
+    if (existingCount < lineCount) {
+      const fragment = document.createDocumentFragment();
+      for (let i = existingCount; i < lineCount; i += 1) {
+        const lineNumber = document.createElement('div');
+        lineNumber.className = 'line-number';
+        fragment.appendChild(lineNumber);
+      }
+      lineNumbers.appendChild(fragment);
+    } else if (existingCount > lineCount) {
+      while (lineNumbers.children.length > lineCount) {
+        lineNumbers.removeChild(lineNumbers.lastChild);
+      }
+    }
+
+    // Update only the heights and numbers that changed, using monospace simulator to avoid forced reflows
+    for (let i = 0; i < lineCount; i += 1) {
+      updateLineNumberItem(existingItems[i], i, lines[i], lineHeight);
+    }
+
+    lastLineNumberLineCount = lineCount;
+    lastLineNumberDocumentLength = text.length;
+    syncLineNumberScroll();
+  }
+
+  function scheduleLineNumberUpdate(options) {
+    const opts = options || {};
+    if (!lineNumbers) return;
+    if (!isEditorVisible()) return;
+
+    if (opts.force) {
+      if (lineNumberUpdateTimeout) {
+        clearTimeout(lineNumberUpdateTimeout);
+        lineNumberUpdateTimeout = null;
+      }
+      if (lineNumberUpdateFrame) {
+        cancelAnimationFrame(lineNumberUpdateFrame);
+        lineNumberUpdateFrame = null;
+      }
+    } else if (lineNumberUpdateFrame || lineNumberUpdateTimeout) {
+      return;
+    }
+
+    const text = markdownEditor ? markdownEditor.value || '' : '';
+    const delay = opts.delay !== undefined
+      ? opts.delay
+      : (opts.inputType === 'insertFromPaste' || text.length >= LARGE_DOCUMENT_THRESHOLD ? getEditorWorkDelay(text) : 0);
+
+    const runUpdate = function() {
+      lineNumberUpdateFrame = window.requestAnimationFrame(function() {
+        lineNumberUpdateFrame = null;
+        updateLineNumbers(opts);
+      });
+    };
+
+    if (delay > 0) {
+      lineNumberUpdateTimeout = setTimeout(function() {
+        lineNumberUpdateTimeout = null;
+        runUpdate();
+      }, delay);
+    } else {
+      runUpdate();
+    }
+  }
+
+  function syncLineNumberScroll() {
+    if (!lineNumbers) return;
+    lineNumbers.scrollTop = cachedScrollTop;
+  }
+
+  function syncEditorScrollOverlays() {
+    cachedScrollTop = markdownEditor.scrollTop;
+    cachedScrollLeft = markdownEditor.scrollLeft;
+    syncHighlightScroll();
+    syncLineNumberScroll();
+  }
+
+  function clampEditorScrollTop(scrollTop) {
+    const maxScrollTop = Math.max(0, markdownEditor.scrollHeight - markdownEditor.clientHeight);
+    return Math.min(maxScrollTop, Math.max(0, scrollTop));
+  }
+
+  function estimateEditorOffsetForIndex(index) {
+    if (!isGeometryInitialized || cachedEditorWidth !== markdownEditor.clientWidth) {
+      refreshEditorWidth();
+    }
+
+    const styles = window.getComputedStyle(markdownEditor);
+    const paddingTop = parseFloat(styles.paddingTop) || 10;
+    const textBefore = (markdownEditor.value || '').slice(0, Math.max(0, index));
+    const lines = textBefore.split('\n');
+    let visualRows = 0;
+
+    for (let i = 0; i < lines.length - 1; i += 1) {
+      visualRows += getWrappedLineCountMonospace(lines[i], cachedMaxCharsPerLine);
+    }
+
+    const currentLinePrefix = lines[lines.length - 1] || '';
+    visualRows += Math.max(0, getWrappedLineCountMonospace(currentLinePrefix, cachedMaxCharsPerLine) - 1);
+    return paddingTop + (visualRows * cachedLineHeight);
+  }
+
+  function getActiveFindHighlight() {
+    if (!editorHighlightLayer) return null;
+    return editorHighlightLayer.querySelector('.find-highlight.active');
+  }
+
+  function scrollActiveMatchIntoView(match) {
+    let matchTop = null;
+    let matchHeight = cachedLineHeight;
+    let activeHighlight = getActiveFindHighlight();
+
+    if (!activeHighlight) {
+      updateFindHighlights();
+      activeHighlight = getActiveFindHighlight();
+    }
+
+    if (activeHighlight) {
+      matchTop = activeHighlight.offsetTop;
+      matchHeight = activeHighlight.offsetHeight || matchHeight;
+    } else {
+      matchTop = estimateEditorOffsetForIndex(match.start);
+    }
+
+    const targetScrollTop = clampEditorScrollTop(
+      matchTop - (markdownEditor.clientHeight / 2) + (matchHeight / 2)
+    );
+
+    markdownEditor.scrollTop = targetScrollTop;
+    syncEditorScrollOverlays();
+  }
+
+  // Class encapsulating Search & Replace Engine
+  class FindReplaceEngine {
+    constructor(editor) {
+      this.editor = editor;
+      this.history = { find: [], replace: [] };
+      this.activeMatches = [];
+      this.currentMatchIndex = -1;
+      this._cachedScopeText = null;
+      this._cachedScopeMap = null;
+    }
+
+    buildASTScopeMap(text) {
+      // PERF-026: Cache scope map to avoid re-lexing on every search keystroke
+      if (text === this._cachedScopeText && this._cachedScopeMap) {
+        return this._cachedScopeMap;
+      }
+      if (typeof marked === 'undefined' || !marked.lexer) return [];
+      try {
+        const tokens = marked.lexer(text);
+        const scopeMap = [];
+        let currentIndex = 0;
+
+        const traverse = (tokenList) => {
+          for (const token of tokenList) {
+            const start = text.indexOf(token.raw, currentIndex);
+            if (start === -1) continue;
+            const end = start + token.raw.length;
+            currentIndex = end;
+
+            let scope = 'plain';
+            if (token.type === 'heading') scope = 'heading';
+            else if (token.type === 'code') {
+              if (token.lang === 'mermaid') scope = 'mermaid';
+              else scope = 'code';
+            } else if (token.type === 'paragraph' && token.raw.startsWith('$$') && token.raw.endsWith('$$')) {
+              scope = 'latex';
+            }
+
+            scopeMap.push({ start, end, scope, type: token.type });
+            if (token.tokens) traverse(token.tokens);
+          }
+        };
+
+        traverse(tokens);
+        this._cachedScopeText = text;
+        this._cachedScopeMap = scopeMap;
+        return scopeMap;
+      } catch (e) {
+        console.warn("AST scope parsing failed:", e);
+        return [];
+      }
+    }
+
+    compileRegExp(query, isRegex, isCaseSensitive, isWholeWord) {
+      if (!query) return null;
+      let pattern = isRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+      if (isWholeWord) {
+        pattern = `\\b${pattern}\\b`;
+      }
+      const flags = isCaseSensitive ? 'gd' : 'gid';
+      return new RegExp(pattern, flags);
+    }
+
+    executeSearch(options) {
+      const { query, isRegex, isCaseSensitive, isWholeWord, scopeFilter, findInSelection } = options;
+      const fullText = this.editor.value || '';
+      
+      let searchRange = { start: 0, end: fullText.length };
+      if (findInSelection) {
+        searchRange.start = this.editor.selectionStart;
+        searchRange.end = this.editor.selectionEnd;
+      }
+
+      let regex;
+      try {
+        regex = this.compileRegExp(query, isRegex, isCaseSensitive, isWholeWord);
+      } catch (err) {
+        throw new Error(err.message);
+      }
+
+      if (!regex) {
+        this.activeMatches = [];
+        this.currentMatchIndex = -1;
+        return this.activeMatches;
+      }
+
+      const rawMatches = [];
+      let match;
+      
+      while ((match = regex.exec(fullText)) !== null) {
+        if (match.index >= searchRange.end) break;
+        if (match.index >= searchRange.start) {
+          rawMatches.push({
+            start: match.index,
+            end: match.index + match[0].length,
+            value: match[0],
+            groups: match.groups || null,
+            matchArray: match
+          });
+        }
+        if (regex.lastIndex === match.index) {
+          regex.lastIndex++;
+        }
+      }
+
+      if (scopeFilter && scopeFilter !== 'entire') {
+        const scopeMap = this.buildASTScopeMap(fullText);
+        this.activeMatches = rawMatches.filter(m => {
+          const matchingScope = scopeMap.find(s => m.start >= s.start && m.end <= s.end);
+          return matchingScope && matchingScope.scope === scopeFilter;
+        });
+      } else {
+        this.activeMatches = rawMatches;
+      }
+
+      this.currentMatchIndex = this.activeMatches.length > 0 ? 0 : -1;
+      this.addHistory('find', query);
+      return this.activeMatches;
+    }
+
+    preserveCase(source, replacement) {
+      if (source === source.toUpperCase()) {
+        return replacement.toUpperCase();
+      }
+      if (source === source.toLowerCase()) {
+        return replacement.toLowerCase();
+      }
+      if (source[0] === source[0].toUpperCase() && source.slice(1) === source.slice(1).toLowerCase()) {
+        return replacement[0].toUpperCase() + replacement.slice(1).toLowerCase();
+      }
+      return replacement;
+    }
+
+    applyCaptureGroups(match, replacementTemplate) {
+      if (!match.matchArray) return replacementTemplate;
+      let result = replacementTemplate;
+      
+      result = result.replace(/\$(\d+)/g, (m, number) => {
+        const idx = parseInt(number, 10);
+        return match.matchArray[idx] !== undefined ? match.matchArray[idx] : m;
+      });
+
+      if (match.groups) {
+        result = result.replace(/\$<([^>]+)>/g, (m, name) => {
+          return match.groups[name] !== undefined ? match.groups[name] : m;
+        });
+      }
+
+      return result;
+    }
+
+    executeReplace(match, replacementTemplate, options) {
+      const { preserveCase, isRegex } = options;
+      const text = this.editor.value;
+
+      let finalReplacement = replacementTemplate;
+      if (isRegex) {
+        finalReplacement = this.applyCaptureGroups(match, finalReplacement);
+      }
+      if (preserveCase) {
+        finalReplacement = this.preserveCase(match.value, finalReplacement);
+      }
+
+      const before = text.slice(0, match.start);
+      const after = text.slice(match.end);
+      this.editor.value = before + finalReplacement + after;
+      this.editor.dispatchEvent(new Event('input', { bubbles: true }));
+
+      this.addHistory('replace', replacementTemplate);
+      return finalReplacement.length - match.value.length;
+    }
+
+    addHistory(type, query) {
+      if (!query) return;
+      const list = this.history[type];
+      const index = list.indexOf(query);
+      if (index !== -1) {
+        list.splice(index, 1);
+      }
+      list.unshift(query);
+      if (list.length > 10) {
+        list.pop();
+      }
+    }
+  }
+
+  // Verification helper for rich text blocks
+  function validateBlockSyntax(originalBlockText, newBlockText, scope) {
+    if (scope === 'latex') {
+      const origDisplay = (originalBlockText.match(/\$\$/g) || []).length;
+      const newDisplay = (newBlockText.match(/\$\$/g) || []).length;
+      const origInline = (originalBlockText.match(/[^\$]\$[^\$]/g) || []).length;
+      const newInline = (newBlockText.match(/[^\$]\$[^\$]/g) || []).length;
+
+      if (origDisplay !== newDisplay || origInline !== newInline) {
+        return { valid: false, reason: "LaTeX math block delimiters are unbalanced." };
+      }
+    }
+
+    if (scope === 'mermaid') {
+      const diagramTypePattern = /^(graph|flowchart|sequenceDiagram|classDiagram|stateDiagram-v2|erDiagram|gantt|pie|quadrantChart|c4Context|mindmap|timeline|zenuml)/i;
+      if (!diagramTypePattern.test(newBlockText.trim())) {
+        return { valid: false, reason: "Missing diagram type definition (e.g. flowchart TD)." };
+      }
+    }
+    return { valid: true };
+  }
+
+  // Global Engine Instance
+  let frEngine = null;
+  let isFrDocked = false;
+  let dragOffset = { x: 0, y: 0 };
+  let isPanelDragging = false;
+  let lastFloatingLeft = null;
+  let lastFloatingTop = null;
+  let lastFloatingRight = null;
+
+  function initFindReplacePanelDrag() {
+    const handle = document.getElementById('find-replace-drag-handle');
+    const panel = document.getElementById('find-replace-modal');
+    if (!handle || !panel) return;
+
+    const startDrag = (clientX, clientY) => {
+      isPanelDragging = true;
+      dragOffset.x = clientX - panel.offsetLeft;
+      dragOffset.y = clientY - panel.offsetTop;
+      document.body.classList.add('resizing');
+    };
+
+    const moveDrag = (clientX, clientY) => {
+      const x = clientX - dragOffset.x;
+      const y = clientY - dragOffset.y;
+      
+      // Keep panel inside viewport boundaries
+      const maxX = window.innerWidth - panel.offsetWidth;
+      const maxY = window.innerHeight - panel.offsetHeight;
+      const newLeft = `${Math.max(0, Math.min(maxX, x))}px`;
+      const newTop = `${Math.max(0, Math.min(maxY, y))}px`;
+      panel.style.left = newLeft;
+      panel.style.top = newTop;
+      panel.style.right = 'auto';
+
+      lastFloatingLeft = newLeft;
+      lastFloatingTop = newTop;
+      lastFloatingRight = 'auto';
+    };
+
+    const stopDrag = () => {
+      if (isPanelDragging) {
+        isPanelDragging = false;
+        document.body.classList.remove('resizing');
+      }
+    };
+
+    // Mouse events
+    handle.addEventListener('mousedown', (e) => {
+      if (isFrDocked) return;
+      if (window.innerWidth < 768) return; // Do NOT allow dragging on mobile layouts
+      if (e.target.closest('.find-replace-header-actions')) return;
+      startDrag(e.clientX, e.clientY);
+    });
+
+    document.addEventListener('mousemove', (e) => {
+      if (!isPanelDragging || isFrDocked) return;
+      moveDrag(e.clientX, e.clientY);
+    });
+
+    document.addEventListener('mouseup', stopDrag);
+
+    // Touch events for tablets
+    handle.addEventListener('touchstart', (e) => {
+      if (isFrDocked) return;
+      if (window.innerWidth < 768) return; // Do NOT allow dragging on mobile layouts
+      if (e.target.closest('.find-replace-header-actions')) return;
+      if (e.touches && e.touches[0]) {
+        startDrag(e.touches[0].clientX, e.touches[0].clientY);
+      }
+    }, { passive: true });
+
+    document.addEventListener('touchmove', (e) => {
+      if (!isPanelDragging || isFrDocked) return;
+      if (e.touches && e.touches[0]) {
+        moveDrag(e.touches[0].clientX, e.touches[0].clientY);
+      }
+    }, { passive: true });
+
+    document.addEventListener('touchend', stopDrag);
+  }
+
+  let frPreferredDocked = false;
+
+  function toggleFrDockMode(forceFloat = false) {
+    // If forceFloat is an Event (e.g. from click listener directly), treat as false
+    if (forceFloat instanceof Event || (forceFloat && typeof forceFloat === 'object')) {
+      forceFloat = false;
+    }
+
+    const panel = document.getElementById('find-replace-modal');
+    const dockBtn = document.getElementById('find-replace-dock');
+    const contentCont = document.querySelector('.content-container');
+    if (!panel || !dockBtn || !contentCont) return;
+
+    // Save active element focus and selection before DOM movement
+    const activeEl = document.activeElement;
+    const activeId = activeEl ? activeEl.id : null;
+    const isInput = activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'SELECT' || activeEl.tagName === 'TEXTAREA');
+    let selStart = 0;
+    let selEnd = 0;
+    if (isInput && typeof activeEl.selectionStart === 'number') {
+      selStart = activeEl.selectionStart;
+      selEnd = activeEl.selectionEnd;
+    }
+
+    if (window.innerWidth < 1080 || forceFloat) {
+      isFrDocked = false;
+      panel.classList.remove('docked');
+      if (panel.parentElement !== document.body) {
+        document.body.appendChild(panel);
+      }
+      contentCont.classList.remove('fr-docked');
+      contentCont.style.setProperty('--dock-width', '0px');
+
+      panel.style.left = lastFloatingLeft !== null ? lastFloatingLeft : '';
+      panel.style.top = lastFloatingTop !== null ? lastFloatingTop : '';
+      panel.style.right = lastFloatingRight !== null ? lastFloatingRight : '';
+      
+      dockBtn.innerHTML = '<i class="bi bi-layout-sidebar-reverse"></i>';
+      dockBtn.title = "Toggle Dock Mode";
+      
+      panel.style.display = 'flex';
+      applyPaneWidths();
+      
+      // Restore focus and selection
+      if (activeId) {
+        const el = document.getElementById(activeId);
+        if (el) {
+          el.focus();
+          if (isInput && typeof el.selectionStart === 'number') {
+            el.setSelectionRange(selStart, selEnd);
+          }
+        }
+      }
+      return;
+    }
+
+    isFrDocked = !isFrDocked;
+    
+    // Save preference to localStorage
+    frPreferredDocked = isFrDocked;
+    localStorage.setItem('find-replace-docked', frPreferredDocked ? 'true' : 'false');
+
+    if (isFrDocked) {
+      panel.classList.add('docked');
+      panel.style.left = 'auto';
+      panel.style.top = 'auto';
+      panel.style.right = 'auto';
+      
+      // Append panel to dock container (.content-container)
+      contentCont.appendChild(panel);
+      contentCont.classList.add('fr-docked');
+      contentCont.style.setProperty('--dock-width', '340px');
+
+      dockBtn.innerHTML = '<i class="bi bi-window"></i>';
+      dockBtn.title = "Toggle Floating Mode";
+    } else {
+      panel.classList.remove('docked');
+      
+      // Reset position and float on body
+      document.body.appendChild(panel);
+      contentCont.classList.remove('fr-docked');
+      contentCont.style.setProperty('--dock-width', '0px');
+
+      panel.style.left = lastFloatingLeft !== null ? lastFloatingLeft : '';
+      panel.style.top = lastFloatingTop !== null ? lastFloatingTop : '';
+      panel.style.right = lastFloatingRight !== null ? lastFloatingRight : '';
+      
+      dockBtn.innerHTML = '<i class="bi bi-layout-sidebar-reverse"></i>';
+      dockBtn.title = "Toggle Dock Mode";
+    }
+    
+    // Ensure display is flex and recalculate split panes
+    panel.style.display = 'flex';
+    applyPaneWidths();
+
+    // Restore focus and selection after layout change
+    if (activeId) {
+      const el = document.getElementById(activeId);
+      if (el) {
+        el.focus();
+        if (isInput && typeof el.selectionStart === 'number') {
+          el.setSelectionRange(selStart, selEnd);
+        }
+      }
+    }
+  }
+
+  function updateFindControls() {
+    const total = findMatches.length;
+    const current = total && activeFindIndex >= 0 ? activeFindIndex + 1 : 0;
+    
+    const countSpan = document.getElementById('find-replace-count');
+    if (countSpan) {
+      countSpan.textContent = `${current} of ${total} matches`;
+    }
+
+    const prevBtn = document.getElementById('find-prev');
+    const nextBtn = document.getElementById('find-next');
+    const replaceCurrentBtn = document.getElementById('find-replace-current');
+    const replaceAllBtn = document.getElementById('find-replace-all');
+
+    const hasMatches = total > 0;
+    const hasQuery = !!(findReplaceInput && findReplaceInput.value);
+
+    if (prevBtn) prevBtn.disabled = !hasMatches;
+    if (nextBtn) nextBtn.disabled = !hasMatches;
+    if (replaceCurrentBtn) replaceCurrentBtn.disabled = !hasMatches;
+    if (replaceAllBtn) replaceAllBtn.disabled = !hasQuery || !hasMatches;
+  }
+
+  function refreshFindMatches(options) {
+    clearTimeout(findRefreshTimeout);
+    findRefreshTimeout = null;
+    const opts = options || {};
+    const query = findReplaceInput ? findReplaceInput.value : '';
+    const errorBox = document.getElementById('find-replace-error');
+    const errorMsg = document.getElementById('regex-error-msg');
+
+    if (errorBox) errorBox.style.display = 'none';
+
+    if (!isFindModalOpen || !query) {
+      findMatches = [];
+      activeFindIndex = -1;
+      updateFindControls();
+      updateFindHighlights();
+      return;
+    }
+
+    const isRegex = document.getElementById('find-regex').classList.contains('active');
+    const isCaseSensitive = document.getElementById('find-case').classList.contains('active');
+    const isWholeWord = document.getElementById('find-word').classList.contains('active');
+    const scopeFilter = document.getElementById('find-replace-scope').value;
+    const findInSelection = document.getElementById('find-sel').classList.contains('active');
+
+    try {
+      findMatches = frEngine.executeSearch({
+        query,
+        isRegex,
+        isCaseSensitive,
+        isWholeWord,
+        scopeFilter,
+        findInSelection
+      });
+    } catch (err) {
+      findMatches = [];
+      activeFindIndex = -1;
+      if (errorBox && errorMsg) {
+        errorMsg.textContent = err.message;
+        errorBox.style.display = 'block';
+      }
+    }
+
+    const shouldResetActiveIndex = opts.resetIndex || query !== lastFindQuery;
+    if (shouldResetActiveIndex) {
+      activeFindIndex = findMatches.length ? 0 : -1;
+    } else if (activeFindIndex >= findMatches.length) {
+      activeFindIndex = findMatches.length - 1;
+    }
+    lastFindQuery = query;
+    updateFindControls();
+    updateFindHighlights();
+    if (shouldResetActiveIndex && findMatches.length && activeFindIndex >= 0) {
+      scrollActiveMatchIntoView(findMatches[activeFindIndex]);
+    }
+    updateHistoryDropdowns();
+  }
+
+  function scheduleFindRefresh(options) {
+    clearTimeout(findRefreshTimeout);
+    const text = markdownEditor ? markdownEditor.value || '' : '';
+    const delay = text.length >= LARGE_DOCUMENT_THRESHOLD ? LARGE_FIND_REFRESH_DELAY : FIND_REFRESH_DELAY;
+    findRefreshTimeout = setTimeout(function() {
+      findRefreshTimeout = null;
+      refreshFindMatches(options);
+    }, delay);
+  }
+
+  function selectActiveMatch() {
+    if (!findMatches.length || activeFindIndex < 0) return;
+    const match = findMatches[activeFindIndex];
+    markdownEditor.focus();
+    markdownEditor.setSelectionRange(match.start, match.end);
+
+    try {
+      scrollActiveMatchIntoView(match);
+    } catch (e) {
+      console.warn("Viewport centering scroll failed:", e);
+    }
+  }
+
+  function cycleFindMatch(direction) {
+    const totalMatches = findMatches.length;
+    if (!totalMatches) return;
+    activeFindIndex = (activeFindIndex + direction + totalMatches) % totalMatches;
+    updateFindControls();
+    updateFindHighlights();
+    selectActiveMatch();
+  }
+
+  function openFindReplaceModal() {
+    if (!findReplaceModal || !findReplaceInput) return;
+    
+    if (!frEngine) {
+      frEngine = new FindReplaceEngine(markdownEditor);
+    }
+
+    isFindModalOpen = true;
+    const selected = markdownEditor.value.slice(markdownEditor.selectionStart, markdownEditor.selectionEnd);
+    if (selected && selected.length < 100) {
+      findReplaceInput.value = selected;
+    }
+
+    // Restore docked/floating mode preference
+    let wasDockedPref = localStorage.getItem('find-replace-docked') === 'true';
+    
+    // Force floating-only mode on mobile/tablet viewports
+    if (window.innerWidth < 1080) {
+      wasDockedPref = false;
+    }
+    
+    if (wasDockedPref) {
+      isFrDocked = false; // Set false so toggleFrDockMode() turns it to true
+      toggleFrDockMode();
+    } else {
+      isFrDocked = true; // Set true so toggleFrDockMode() turns it to false
+      toggleFrDockMode();
+    }
+
+    findReplaceModal.style.display = 'flex';
+    
+    requestAnimationFrame(function() {
+      findReplaceInput.focus();
+      findReplaceInput.select();
+    });
+    
+    refreshFindMatches({ resetIndex: true });
+    if (findMatches.length) {
+      selectActiveMatch();
+    }
+  }
+
+  function closeFindReplaceModal() {
+    isFindModalOpen = false;
+    const panel = document.getElementById('find-replace-modal');
+    const contentCont = document.querySelector('.content-container');
+    if (panel) {
+      panel.style.display = 'none';
+      if (isFrDocked) {
+        // Reset split layout styles when closed
+        if (contentCont) {
+          contentCont.classList.remove('fr-docked');
+          contentCont.style.setProperty('--dock-width', '0px');
+          applyPaneWidths();
+        }
+      }
+    }
+    findMatches = [];
+    activeFindIndex = -1;
+    updateFindControls();
+    updateFindHighlights();
+  }
+
+  function replaceCurrentMatch() {
+    if (!findMatches.length || activeFindIndex < 0) return;
+    const replacement = findReplaceWith ? findReplaceWith.value : '';
+    const match = findMatches[activeFindIndex];
+
+    const preserveCase = document.getElementById('replace-preserve-case').classList.contains('active');
+    const isRegex = document.getElementById('find-regex').classList.contains('active');
+
+    // Syntax validation
+    const scopeFilter = document.getElementById('find-replace-scope').value;
+    if (scopeFilter === 'latex' || scopeFilter === 'mermaid') {
+      const check = validateBlockSyntax(match.value, replacement, scopeFilter);
+      if (!check.valid) {
+        alert(`Blocked replacement: ${check.reason}`);
+        return;
+      }
+    }
+
+    frEngine.executeReplace(match, replacement, { preserveCase, isRegex });
+    
+    refreshFindMatches();
+    if (findMatches.length) {
+      activeFindIndex = Math.min(activeFindIndex, findMatches.length - 1);
+      selectActiveMatch();
+    }
+  }
+
+  function replaceAllMatches() {
+    if (!findMatches.length) return;
+    
+    const query = findReplaceInput ? findReplaceInput.value : '';
+    const replacement = findReplaceWith ? findReplaceWith.value : '';
+    const showDiff = document.getElementById('find-replace-diff-toggle').checked;
+
+    if (showDiff) {
+      renderDiffPreview();
+      return;
+    }
+
+    executeBulkReplace();
+  }
+
+  function executeBulkReplace() {
+    const replacement = findReplaceWith ? findReplaceWith.value : '';
+    const preserveCase = document.getElementById('replace-preserve-case').classList.contains('active');
+    const isRegex = document.getElementById('find-regex').classList.contains('active');
+    
+    // Reverse sorting to replace from bottom up
+    const matchesCopy = [...findMatches];
+    matchesCopy.sort((a, b) => b.start - a.start);
+
+    for (const match of matchesCopy) {
+      frEngine.executeReplace(match, replacement, { preserveCase, isRegex });
+    }
+
+    refreshFindMatches({ resetIndex: true });
+    if (findMatches.length) {
+      selectActiveMatch();
+    }
+  }
+
+  function renderDiffPreview() {
+    const container = document.getElementById('find-replace-diff-container');
+    const modal = document.getElementById('find-replace-diff-modal');
+    if (!container || !modal) return;
+
+    const replacement = findReplaceWith ? findReplaceWith.value : '';
+    const preserveCase = document.getElementById('replace-preserve-case').classList.contains('active');
+    const isRegex = document.getElementById('find-regex').classList.contains('active');
+
+    const lines = markdownEditor.value.split('\n');
+    const matchesCopy = [...findMatches];
+    matchesCopy.sort((a, b) => b.start - a.start);
+
+    // Draft the replaced value in memory
+    let draftValue = markdownEditor.value;
+    for (const match of matchesCopy) {
+      let finalRepl = replacement;
+      if (isRegex) {
+        finalRepl = frEngine.applyCaptureGroups(match, finalRepl);
+      }
+      if (preserveCase) {
+        finalRepl = frEngine.preserveCase(match.value, finalRepl);
+      }
+      draftValue = draftValue.slice(0, match.start) + finalRepl + draftValue.slice(match.end);
+    }
+
+    const draftLines = draftValue.split('\n');
+
+    // PERF-007: Clear elements using textContent
+    container.textContent = '';
+    const fragment = document.createDocumentFragment();
+
+    const maxLines = Math.max(lines.length, draftLines.length);
+    for (let i = 0; i < maxLines; i++) {
+      const origLine = lines[i] !== undefined ? lines[i] : null;
+      const newLine = draftLines[i] !== undefined ? draftLines[i] : null;
+
+      if (origLine !== newLine) {
+        if (origLine !== null) {
+          const delLine = document.createElement('div');
+          delLine.className = 'diff-line deletion';
+          delLine.innerHTML = `<span class="diff-line-num">${i + 1}</span><span class="diff-line-content">- ${escapeHtml(origLine)}</span>`;
+          fragment.appendChild(delLine);
+        }
+        if (newLine !== null) {
+          const addLine = document.createElement('div');
+          addLine.className = 'diff-line addition';
+          addLine.innerHTML = `<span class="diff-line-num">${i + 1}</span><span class="diff-line-content">+ ${escapeHtml(newLine)}</span>`;
+          fragment.appendChild(addLine);
+        }
+      } else if (origLine !== null) {
+        // Show context for matching lines to prevent giant blank diff spaces
+        // Show context only if it surrounds a modified line
+        const hasDiffNearby = Array.from({length: 5}, (_, idx) => i - 2 + idx)
+          .some(lineIdx => lines[lineIdx] !== undefined && draftLines[lineIdx] !== undefined && lines[lineIdx] !== draftLines[lineIdx]);
+        
+        if (hasDiffNearby) {
+          const ctxLine = document.createElement('div');
+          ctxLine.className = 'diff-line context';
+          ctxLine.innerHTML = `<span class="diff-line-num">${i + 1}</span><span class="diff-line-content">  ${escapeHtml(origLine)}</span>`;
+          fragment.appendChild(ctxLine);
+        }
+      }
+    }
+
+    container.appendChild(fragment);
+    openAppModal(modal, {
+      focusTarget: document.getElementById('find-replace-diff-confirm'),
+      onClose: () => closeAppModal(modal)
+    });
+  }
+
+  function updateHistoryDropdowns() {
+    const select = document.getElementById('find-replace-history');
+    if (!select || !frEngine) return;
+
+    // Preserve the first option
+    select.innerHTML = '<option value="">Recent queries...</option>';
+    
+    frEngine.history.find.forEach(q => {
+      const opt = document.createElement('option');
+      opt.value = q;
+      opt.textContent = q;
+      select.appendChild(opt);
+    });
+  }
+
+  function initFindReplaceModal() {
+    const modal = document.getElementById('find-replace-modal');
+    if (!modal) return;
+
+    initFindReplacePanelDrag();
+
+    // Toggle options
+    const toggleButtons = ['find-case', 'find-word', 'find-regex', 'find-sel', 'replace-preserve-case', 'find-wrap'];
+    toggleButtons.forEach(id => {
+      const btn = document.getElementById(id);
+      if (btn) {
+        btn.addEventListener('click', () => {
+          const isActive = btn.classList.contains('active');
+          if (isActive) {
+            btn.classList.remove('active');
+            btn.setAttribute('aria-pressed', 'false');
+          } else {
+            btn.classList.add('active');
+            btn.setAttribute('aria-pressed', 'true');
+          }
+          refreshFindMatches({ resetIndex: true });
+        });
+      }
+    });
+
+    // History select handler
+    const historySelect = document.getElementById('find-replace-history');
+    if (historySelect) {
+      historySelect.addEventListener('change', () => {
+        if (historySelect.value) {
+          findReplaceInput.value = historySelect.value;
+          refreshFindMatches({ resetIndex: true });
+        }
+      });
+    }
+
+    // Scope select handler
+    const scopeSelect = document.getElementById('find-replace-scope');
+    if (scopeSelect) {
+      scopeSelect.addEventListener('change', () => {
+        refreshFindMatches({ resetIndex: true });
+      });
+    }
+
+    // Reset position handler
+    const resetBtn = document.getElementById('find-replace-reset');
+    const resetFooterBtn = document.getElementById('find-replace-reset-footer');
+    const doResetPosition = () => {
+      lastFloatingLeft = null;
+      lastFloatingTop = null;
+      lastFloatingRight = null;
+      modal.style.left = '';
+      modal.style.top = '';
+      modal.style.right = '';
+    };
+
+    if (resetBtn) {
+      resetBtn.addEventListener('click', doResetPosition);
+    }
+    if (resetFooterBtn) {
+      resetFooterBtn.addEventListener('click', doResetPosition);
+    }
+
+    // Dock toggle handler
+    const dockBtn = document.getElementById('find-replace-dock');
+    if (dockBtn) {
+      dockBtn.addEventListener('click', () => toggleFrDockMode(false));
+    }
+
+    // Advanced Drawer Toggle
+    const drawerToggle = document.getElementById('fr-drawer-toggle');
+    const drawerContent = document.getElementById('fr-drawer-content');
+    if (drawerToggle && drawerContent) {
+      drawerToggle.addEventListener('click', () => {
+        const isOpen = drawerContent.style.display === 'flex';
+        if (isOpen) {
+          drawerContent.style.display = 'none';
+          drawerToggle.setAttribute('aria-expanded', 'false');
+          drawerToggle.innerHTML = '<i class="bi bi-chevron-right me-1"></i> Advanced Options';
+        } else {
+          drawerContent.style.display = 'flex';
+          drawerToggle.setAttribute('aria-expanded', 'true');
+          drawerToggle.innerHTML = '<i class="bi bi-chevron-down me-1"></i> Advanced Options';
+        }
+      });
+    }
+
+    // Inputs
+    if (findReplaceInput) {
+      findReplaceInput.addEventListener('input', function() {
+        refreshFindMatches({ resetIndex: true });
+      });
+      findReplaceInput.addEventListener('keydown', function(event) {
+        if (event.key === 'Enter') {
+          event.preventDefault();
+          cycleFindMatch(event.shiftKey ? -1 : 1);
+        }
+      });
+    }
+
+    if (findReplaceWith) {
+      findReplaceWith.addEventListener('input', updateFindControls);
+    }
+
+    // Navigation buttons
+    const prevBtn = document.getElementById('find-prev');
+    const nextBtn = document.getElementById('find-next');
+    if (prevBtn) prevBtn.addEventListener('click', () => cycleFindMatch(-1));
+    if (nextBtn) nextBtn.addEventListener('click', () => cycleFindMatch(1));
+
+    // Action buttons
+    const currentBtn = document.getElementById('find-replace-current');
+    const allBtn = document.getElementById('find-replace-all');
+    if (currentBtn) currentBtn.addEventListener('click', replaceCurrentMatch);
+    if (allBtn) allBtn.addEventListener('click', replaceAllMatches);
+
+    // Close buttons
+    const closeBtn = document.getElementById('find-replace-close');
+    const closeIcon = document.getElementById('find-replace-close-icon');
+    if (closeBtn) closeBtn.addEventListener('click', closeFindReplaceModal);
+    if (closeIcon) closeIcon.addEventListener('click', closeFindReplaceModal);
+
+    // Diff modal confirmation triggers
+    const diffConfirmBtn = document.getElementById('find-replace-diff-confirm');
+    const diffCancelBtn = document.getElementById('find-replace-diff-cancel');
+    const diffCloseIcon = document.getElementById('find-replace-diff-close-icon');
+    const diffModal = document.getElementById('find-replace-diff-modal');
+
+    if (diffConfirmBtn) {
+      diffConfirmBtn.addEventListener('click', () => {
+        executeBulkReplace();
+        closeAppModal(diffModal);
+      });
+    }
+    if (diffCancelBtn) diffCancelBtn.addEventListener('click', () => closeAppModal(diffModal));
+    if (diffCloseIcon) diffCloseIcon.addEventListener('click', () => closeAppModal(diffModal));
+  }
+
+  function initAppModals() {
+    if (clearFormattingConfirm) {
+      clearFormattingConfirm.addEventListener('click', function() {
+        applyClearFormatting();
+        closeAppModal(clearFormattingModal);
+      });
+    }
+    if (clearFormattingCancel) {
+      clearFormattingCancel.addEventListener('click', function() { closeAppModal(clearFormattingModal); });
+    }
+    if (clearFormattingClose) {
+      clearFormattingClose.addEventListener('click', function() { closeAppModal(clearFormattingModal); });
+    }
+    if (helpModalClose) {
+      helpModalClose.addEventListener('click', function() { closeAppModal(helpModal); });
+    }
+    if (helpModalCloseIcon) {
+      helpModalCloseIcon.addEventListener('click', function() { closeAppModal(helpModal); });
+    }
+    if (aboutModalClose) {
+      aboutModalClose.addEventListener('click', function() { closeAppModal(aboutModal); });
+    }
+    if (aboutModalCloseIcon) {
+      aboutModalCloseIcon.addEventListener('click', function() { closeAppModal(aboutModal); });
+    }
+  }
+
+  function openHelpModal() {
+    if (helpModal) {
+      openAppModal(helpModal);
+    }
+  }
+
+  function openAboutModal() {
+    if (aboutModal) {
+      const aboutVersion = document.getElementById("about-version");
+      if (aboutVersion) {
+        aboutVersion.textContent = APP_VERSION;
+      }
+      openAppModal(aboutModal);
+    }
+  }
+
+  function openClearFormattingModal() {
+    if (clearFormattingModal) {
+      openAppModal(clearFormattingModal);
+    }
+  }
+
+  function runMarkdownTool(action, button) {
+    if (action === 'undo') {
+      executeUndo();
+      return;
+    }
+    if (action === 'redo') {
+      executeRedo();
+      return;
+    }
+
+    if (action === 'bold') wrapEditorSelection('**', '**', 'bold text');
+    else if (action === 'strike') wrapEditorSelection('~~', '~~', 'struck text');
+    else if (action === 'italic') wrapEditorSelection('*', '*', 'italic text');
+    else if (action === 'quote') transformEditorLines(function(line) { return line ? '> ' + line.replace(/^>\s?/, '') : '>'; });
+    else if (action === 'align-left') insertAlignmentBlock('left');
+    else if (action === 'align-center') insertAlignmentBlock('center');
+    else if (action === 'align-right') insertAlignmentBlock('right');
+    else if (action === 'title-case') transformSelectionOrCurrentLine(toTitleCase);
+    else if (action === 'uppercase') transformSelectionOrCurrentLine(function(text) { return text.toUpperCase(); });
+    else if (action === 'lowercase') transformSelectionOrCurrentLine(function(text) { return text.toLowerCase(); });
+    else if (action === 'heading') {
+      const level = parseInt(button.getAttribute('data-md-level') || '1', 10);
+      const marker = '#'.repeat(Math.max(1, Math.min(6, level))) + ' ';
+      transformEditorLines(function(line) { return marker + line.replace(/^#{1,6}\s+/, ''); });
+    } else if (action === 'unordered-list') {
+      applyMarkdownList('unordered');
+    } else if (action === 'ordered-list') {
+      applyMarkdownList('ordered');
+    } else if (action === 'horizontal-rule') insertMarkdownBlock('---\n');
+    else if (action === 'link') insertMarkdownLink();
+    else if (action === 'reference') insertMarkdownReference();
+    else if (action === 'image') insertMarkdownImage();
+    else if (action === 'inline-code') wrapEditorSelection('`', '`', 'code');
+    else if (action === 'code-block') insertMarkdownBlock('```js\n' + (markdownEditor.value.slice(markdownEditor.selectionStart, markdownEditor.selectionEnd) || 'console.log("Hello, Markdown!");') + '\n```\n');
+    else if (action === 'table') openTableModal();
+    else if (action === 'date-time') {
+      const now = new Date();
+      const datePart = now.toLocaleDateString('en-CA');
+      const timePart = now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
+      const dayName = now.toLocaleDateString('en-US', { weekday: 'long' });
+      const timestamp = `${datePart} ${timePart} ${dayName}`;
+      replaceEditorRange(markdownEditor.selectionStart, markdownEditor.selectionEnd, timestamp, markdownEditor.selectionStart + timestamp.length, markdownEditor.selectionStart + timestamp.length);
+    } else if (action === 'emoji') {
+      openEmojiModal();
+    }
+    else if (action === 'symbols') openSymbolsModal();
+    else if (action === 'alert') openAlertModal();
+    else if (action === 'terminal-block') insertMarkdownBlock('```bash\nnpm run dev\n```\n');
+    else if (action === 'fullscreen') {
+      if (document.fullscreenElement && document.exitFullscreen) document.exitFullscreen();
+      else if (document.documentElement.requestFullscreen) document.documentElement.requestFullscreen();
+    } else if (action === 'clear-formatting') openClearFormattingModal();
+    else if (action === 'find') openFindReplaceModal();
+    else if (action === 'help') openHelpModal();
+    else if (action === 'info') openAboutModal();
+  }
+
+  function initMarkdownFormatToolbar() {
+    if (!markdownFormatToolbar) return;
+    markdownFormatToolbar.addEventListener('mousedown', function(e) {
+      if (e.target.closest('[data-md-action]')) e.preventDefault();
+    });
+    markdownFormatToolbar.addEventListener('click', function(e) {
+      const button = e.target.closest('[data-md-action]');
+      if (!button) return;
+      e.preventDefault();
+      runMarkdownTool(button.getAttribute('data-md-action'), button);
+    });
+  }
+
+  // Story 1.3: Resize Divider Functions
+  function initResizer() {
+    if (!resizeDivider) return;
+
+    // Set up WAI-ARIA accessibility tags
+    resizeDivider.setAttribute('role', 'separator');
+    resizeDivider.setAttribute('aria-orientation', 'vertical');
+    resizeDivider.setAttribute('tabindex', '0');
+    resizeDivider.setAttribute('aria-valuemin', MIN_PANE_PERCENT.toString());
+    resizeDivider.setAttribute('aria-valuemax', (100 - MIN_PANE_PERCENT).toString());
+    updateResizerAria();
+
+    resizeDivider.addEventListener('mousedown', startResize);
+    document.addEventListener('mousemove', handleResize);
+    document.addEventListener('mouseup', stopResize);
+
+    // Touch support for tablets (though disabled via CSS, keeping for future)
+    resizeDivider.addEventListener('touchstart', startResizeTouch);
+    document.addEventListener('touchmove', handleResizeTouch);
+    document.addEventListener('touchend', stopResize);
+
+    resizeDivider.addEventListener('keydown', handleResizerKeydown);
+
+    function handleResizerKeydown(e) {
+      if (currentViewMode !== 'split') return;
+      
+      let delta = 0;
+      if (e.key === 'ArrowLeft') {
+        delta = -5; // Shift left by 5%
+      } else if (e.key === 'ArrowRight') {
+        delta = 5; // Shift right by 5%
+      } else {
+        return;
+      }
+      
+      e.preventDefault();
+      editorWidthPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, editorWidthPercent + delta));
+      applyPaneWidths();
+      updateResizerAria();
+    }
+
+    function updateResizerAria() {
+      resizeDivider.setAttribute('aria-valuenow', Math.round(editorWidthPercent));
+    }
+  }
+
+  function startResize(e) {
+    if (window.innerWidth < 1080) return;
+    if (currentViewMode !== 'split') return;
+    e.preventDefault();
+    isResizing = true;
+    resizeDivider.classList.add('dragging');
+    document.body.classList.add('resizing');
+  }
+
+  function startResizeTouch(e) {
+    if (window.innerWidth < 1080) return;
+    if (currentViewMode !== 'split') return;
+    e.preventDefault();
+    isResizing = true;
+    resizeDivider.classList.add('dragging');
+    document.body.classList.add('resizing');
+  }
+
+  function handleResize(e) {
+    if (!isResizing) return;
+
+    const containerRect = contentContainer.getBoundingClientRect();
+    const containerWidth = containerRect.width;
+    const mouseX = e.clientX - containerRect.left;
+
+    // Calculate percentage
+    let newEditorPercent = (mouseX / containerWidth) * 100;
+
+    // Enforce minimum pane widths
+    newEditorPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, newEditorPercent));
+
+    editorWidthPercent = newEditorPercent;
+    applyPaneWidths();
+  }
+
+  function handleResizeTouch(e) {
+    if (!isResizing || !e.touches[0]) return;
+
+    const containerRect = contentContainer.getBoundingClientRect();
+    const containerWidth = containerRect.width;
+    const touchX = e.touches[0].clientX - containerRect.left;
+
+    let newEditorPercent = (touchX / containerWidth) * 100;
+    newEditorPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, newEditorPercent));
+
+    editorWidthPercent = newEditorPercent;
+    applyPaneWidths();
+  }
+
+  function stopResize() {
+    if (!isResizing) return;
+    isResizing = false;
+    resizeDivider.classList.remove('dragging');
+    document.body.classList.remove('resizing');
+  }
+
+  function applyPaneWidths() {
+    if (window.innerWidth < 1080) {
+      resetPaneWidths();
+      return;
+    }
+    if (currentViewMode !== 'split') return;
+
+    const previewPercent = 100 - editorWidthPercent;
+    editorPaneElement.style.flex = `0 0 calc((100% - var(--dock-width, 0px)) * ${editorWidthPercent / 100} - 4px)`;
+    previewPaneElement.style.flex = `0 0 calc((100% - var(--dock-width, 0px)) * ${previewPercent / 100} - 4px)`;
+    refreshEditorWidth();
+    scheduleLineNumberUpdate();
+  }
+
+  function resetPaneWidths() {
+    editorPaneElement.style.flex = '';
+    previewPaneElement.style.flex = '';
+    refreshEditorWidth();
+  }
+
+  function openMobileMenu() {
+    mobileMenuPanel.classList.add("active");
+    mobileMenuOverlay.classList.add("active");
+  }
+  function closeMobileMenu() {
+    mobileMenuPanel.classList.remove("active");
+    mobileMenuOverlay.classList.remove("active");
+  }
+  mobileMenuToggle.addEventListener("click", openMobileMenu);
+  mobileCloseMenu.addEventListener("click", closeMobileMenu);
+  mobileMenuOverlay.addEventListener("click", closeMobileMenu);
+
+  function updateMobileStats() {
+    mobileCharCount.textContent   = charCountElement.textContent;
+    mobileWordCount.textContent   = wordCountElement.textContent;
+    mobileReadingTime.textContent = readingTimeElement.textContent;
+  }
+
+  const origUpdateStats = updateDocumentStats;
+  updateDocumentStats = function() {
+    origUpdateStats();
+    updateMobileStats();
+  };
+
+  mobileToggleSync.addEventListener("click", () => {
+    toggleSyncScrolling();
+    if (syncScrollingEnabled) {
+      mobileToggleSync.innerHTML = '<i class="bi bi-link-45deg me-2"></i> Sync Off';
+      mobileToggleSync.classList.add("sync-disabled");
+      mobileToggleSync.classList.remove("sync-enabled");
+      mobileToggleSync.classList.add("sync-active");
+    } else {
+      mobileToggleSync.innerHTML = '<i class="bi bi-link me-2"></i> Sync On';
+      mobileToggleSync.classList.add("sync-enabled");
+      mobileToggleSync.classList.remove("sync-disabled");
+      mobileToggleSync.classList.remove("sync-active");
+    }
+  });
+  mobileImportBtn.addEventListener("click", () => {
+    if (typeof Neutralino !== 'undefined') {
+      nativeImportMarkdown();
+    } else {
+      fileInput.click();
+    }
+  });
+  mobileImportGithubBtn.addEventListener("click", () => {
+    closeMobileMenu();
+    openGitHubImportModal();
+  });
+  mobileExportMd.addEventListener("click", () => exportMd.click());
+  mobileExportHtml.addEventListener("click", () => exportHtml.click());
+  mobileExportPdf.addEventListener("click", () => exportPdf.click());
+  mobileCopyMarkdown.addEventListener("click", () => copyMarkdownButton.click());
+  mobileThemeToggle.addEventListener("click", () => {
+    themeToggle.click();
+    mobileThemeToggle.innerHTML = themeToggle.innerHTML + " Toggle Dark Mode";
+  });
+
+  const mobileNewTabBtn = document.getElementById("mobile-new-tab-btn");
+  if (mobileNewTabBtn) {
+    mobileNewTabBtn.addEventListener("click", function() {
+      newTab();
+      closeMobileMenu();
+    });
+  }
+
+  const mobileTabResetBtn = document.getElementById("mobile-tab-reset-btn");
+  if (mobileTabResetBtn) {
+    mobileTabResetBtn.addEventListener("click", function() {
+      closeMobileMenu();
+      resetAllTabs();
+    });
+  }
+  
+  initTabs();
+  if (loadGlobalState().syncScrollingEnabled === false) toggleSyncScrolling();
+  updateMobileStats();
+  updateFindHighlights();
+  syncHighlightScroll();
+
+  // Defer DOM geometry measurement until after FCP/LCP critical paint path
+  setTimeout(function() {
+    initEditorGeometry();
+    refreshEditorWidth();
+    scheduleLineNumberUpdate();
+  }, 100);
+
+  // Initialize resizer - Story 1.3
+  initResizer();
+  function constrainFloatingPanelPosition() {
+    const panel = document.getElementById('find-replace-modal');
+    if (!panel || isFrDocked || panel.style.display === 'none') return;
+    if (window.innerWidth < 768) return; // Mobile layout forces fixed responsive positioning via CSS
+    
+    // Only adjust if the inline style has custom dragged coordinates
+    if (!panel.style.left || panel.style.left === 'auto') return;
+
+    const leftVal = parseFloat(panel.style.left) || 0;
+    const topVal = parseFloat(panel.style.top) || 0;
+    
+    const maxX = window.innerWidth - panel.offsetWidth;
+    const maxY = window.innerHeight - panel.offsetHeight;
+    
+    const constrainedLeft = `${Math.max(0, Math.min(maxX, leftVal))}px`;
+    const constrainedTop = `${Math.max(0, Math.min(maxY, topVal))}px`;
+    
+    panel.style.left = constrainedLeft;
+    panel.style.top = constrainedTop;
+    
+    lastFloatingLeft = constrainedLeft;
+    lastFloatingTop = constrainedTop;
+  }
+
+  let resizeLayoutTimeout = null;
+  window.addEventListener('resize', () => {
+    clearTimeout(resizeLayoutTimeout);
+    resizeLayoutTimeout = setTimeout(function() {
+      initEditorGeometry();
+      refreshEditorWidth();
+      scheduleLineNumberUpdate();
+      if (window.innerWidth < 1080 && isFrDocked && isFindModalOpen) {
+        toggleFrDockMode(true);
+      }
+      constrainFloatingPanelPosition();
+    }, 100);
+  });
+
+  // View Mode Button Event Listeners - Story 1.1
+  viewModeButtons.forEach(btn => {
+    btn.addEventListener('click', function() {
+      const mode = this.getAttribute('data-view-mode');
+      setViewMode(resolveViewToggleMode(mode));
+      saveCurrentTabState();
+    });
+  });
+
+  // Story 1.4: Mobile View Mode Button Event Listeners
+  mobileViewModeButtons.forEach(btn => {
+    btn.addEventListener('click', function() {
+      const mode = this.getAttribute('data-mode');
+      setViewMode(mode);
+      saveCurrentTabState();
+      closeMobileMenu();
+    });
+  });
+
+  markdownEditor.addEventListener("input", function(e) {
+    handleKeystrokeHistory(e);
+    debouncedRender();
+    clearTimeout(saveTabStateTimeout);
+    saveTabStateTimeout = setTimeout(saveCurrentTabState, 500);
+    if (isFindModalOpen) {
+      scheduleFindRefresh();
+    } else {
+      updateFindHighlights();
+    }
+    scheduleLineNumberUpdate({
+      inputType: e && typeof e.inputType === 'string' ? e.inputType : '',
+    });
+  });
+
+  markdownEditor.addEventListener('keydown', updateLastCursor);
+  markdownEditor.addEventListener('keyup', updateLastCursor);
+  markdownEditor.addEventListener('mousedown', updateLastCursor);
+  markdownEditor.addEventListener('mouseup', updateLastCursor);
+  markdownEditor.addEventListener('focus', updateLastCursor);
+
+  initMarkdownFormatToolbar();
+  initFindReplaceModal();
+  initAppModals();
+  
+  // Editor key handlers for list continuation and indentation
+  markdownEditor.addEventListener("keydown", function(e) {
+    if (handleListEnter(e)) {
+      return;
+    }
+
+    if (e.key === 'Tab') {
+      e.preventDefault();
+      
+      const start = this.selectionStart;
+      const end = this.selectionEnd;
+      const value = this.value;
+      
+      // Insert 2 spaces
+      const indent = '  '; // 2 spaces
+      
+      // Update textarea value
+      this.value = value.substring(0, start) + indent + value.substring(end);
+      
+      // Update cursor position
+      this.selectionStart = this.selectionEnd = start + indent.length;
+      
+      // Trigger input event to update preview
+      this.dispatchEvent(new Event('input'));
+    }
+  });
+  
+  markdownEditor.addEventListener("scroll", function() {
+    cachedScrollTop = this.scrollTop;
+    cachedScrollLeft = this.scrollLeft;
+    syncEditorToPreview();
+    scheduleEditorOverlayScrollSync();
+  });
+  previewPane.addEventListener("scroll", syncPreviewToEditor);
+  toggleSyncButton.addEventListener("click", toggleSyncScrolling);
+  if (directionToggle) {
+    directionToggle.addEventListener("click", function () {
+      const currentDir = markdownEditor ? markdownEditor.getAttribute("dir") : "ltr";
+      const direction = currentDir === "rtl" ? "ltr" : "rtl";
+      applyDirectionToContent(direction);
+      saveGlobalState({ direction });
+      updateDirectionToggleUI(direction);
+    });
+  }
+  themeToggle.addEventListener("click", function () {
+    _lastRenderedContent = null;
+    const theme =
+      document.documentElement.getAttribute("data-theme") === "dark"
+        ? "light"
+        : "dark";
+    document.documentElement.setAttribute("data-theme", theme);
+    saveGlobalState({ theme });
+
+    if (theme === "dark") {
+      themeToggle.innerHTML = '<i class="bi bi-sun"></i>';
+    } else {
+      themeToggle.innerHTML = '<i class="bi bi-moon"></i>';
+    }
+    
+    // PERF-004: Only re-render Mermaid diagrams on theme change instead of full renderMarkdown()
+    // CSS custom properties handle all other theme transitions automatically.
+    // PERF-002: Guard mermaid re-render — skip if not loaded yet
+    if (typeof mermaid !== 'undefined') {
+      initMermaid(true); // Force re-init with new theme
+      try {
+        const mermaidNodes = markdownPreview.querySelectorAll('.mermaid');
+        if (mermaidNodes.length > 0) {
+          // Clear existing rendered Mermaid SVGs and re-render with new theme
+          mermaidNodes.forEach(function(node) {
+            // Restore original diagram code to prevent parsing already-rendered SVG as source
+            const originalCode = node.getAttribute('data-original-code');
+            if (originalCode) {
+              const decodedCode = decodeURIComponent(originalCode);
+              const escapedCode = decodedCode
+                .replace(/&/g, "&amp;")
+                .replace(/</g, "&lt;")
+                .replace(/>/g, "&gt;");
+              node.innerHTML = escapedCode;
+            }
+            node.removeAttribute('data-processed');
+            const container = node.closest('.mermaid-container');
+            if (container) {
+              container.classList.add('is-loading');
+              const oldToolbar = container.querySelector('.mermaid-toolbar');
+              if (oldToolbar) oldToolbar.remove();
+            }
+          });
+          Promise.resolve(mermaid.init(undefined, mermaidNodes))
+            .then(function() {
+              markdownPreview.querySelectorAll('.mermaid-container.is-loading').forEach(function(c) {
+                c.classList.remove('is-loading');
+              });
+              addMermaidToolbars();
+            })
+            .catch(function(e) {
+              console.warn('Mermaid theme re-render failed:', e);
+              markdownPreview.querySelectorAll('.mermaid-container.is-loading').forEach(function(c) {
+                c.classList.remove('is-loading');
+              });
+            });
+        }
+      } catch (e) {
+        console.warn('Mermaid theme re-render failed:', e);
+      }
+    }
+  });
+
+  async function nativeSaveMarkdown() {
+    try {
+      const content = markdownEditor.value;
+      const result = await Neutralino.os.showSaveDialog("Save Markdown File", {
+        filters: [
+          { name: "Markdown files (*.md)", extensions: ["md", "markdown"] },
+          { name: "All files (*.*)", extensions: ["*"] }
+        ]
+      });
+      if (result) {
+        await Neutralino.filesystem.writeFile(result, content);
+        const fileName = result.split(/[/\\]/).pop().replace(/\.(md|markdown)$/i, "");
+        const activeTab = tabs.find(function(t) { return t.id === activeTabId; });
+        if (activeTab) {
+          activeTab.title = fileName;
+          activeTab.content = content;
+          saveTabsToStorage(tabs);
+          renderTabBar(tabs, activeTabId);
+        }
+      }
+    } catch (e) {
+      console.error("Native save failed:", e);
+      alert("Native save failed: " + e.message);
+    }
+  }
+
+  async function nativeSaveHtml(htmlContent) {
+    try {
+      const result = await Neutralino.os.showSaveDialog("Save HTML document", {
+        filters: [
+          { name: "HTML documents (*.html)", extensions: ["html", "htm"] }
+        ]
+      });
+      if (result) {
+        await Neutralino.filesystem.writeFile(result, htmlContent);
+      }
+    } catch (e) {
+      console.error("Native HTML save failed:", e);
+      alert("Native HTML save failed: " + e.message);
+    }
+  }
+
+  async function nativeImportMarkdown() {
+    try {
+      const result = await Neutralino.os.showOpenDialog("Open Markdown file", {
+        filters: [
+          { name: "Markdown files (*.md)", extensions: ["md", "markdown"] },
+          { name: "All files (*.*)", extensions: ["*"] }
+        ],
+        multiSelections: true
+      });
+      if (result && result.length > 0) {
+        for (const filePath of result) {
+          const content = await Neutralino.filesystem.readFile(filePath);
+          const fileName = filePath.split(/[/\\]/).pop().replace(/\.(md|markdown)$/i, "");
+          newTab(content, fileName);
+        }
+      }
+    } catch (e) {
+      console.error("Native import failed:", e);
+      alert("Native import failed: " + e.message);
+    }
+  }
+
+  if (importFromFileButton) {
+    importFromFileButton.addEventListener("click", function (e) {
+      e.preventDefault();
+      if (typeof Neutralino !== 'undefined') {
+        nativeImportMarkdown();
+      } else {
+        fileInput.click();
+      }
+    });
+  }
+
+  if (importFromGithubButton) {
+    importFromGithubButton.addEventListener("click", function (e) {
+      e.preventDefault();
+      openGitHubImportModal();
+    });
+  }
+
+  if (githubImportSubmitBtn) {
+    githubImportSubmitBtn.addEventListener("click", handleGitHubImportSubmit);
+  }
+  if (githubImportCancelBtn) {
+    githubImportCancelBtn.addEventListener("click", closeGitHubImportModal);
+  }
+  const handleGitHubImportInputKeydown = function(e) {
+    if (e.key === "Enter") {
+      e.preventDefault();
+      handleGitHubImportSubmit();
+    } else if (e.key === "Escape") {
+      closeGitHubImportModal();
+    }
+  };
+  if (githubImportUrlInput) {
+    githubImportUrlInput.addEventListener("keydown", handleGitHubImportInputKeydown);
+  }
+  if (githubImportFileSelect) {
+    githubImportFileSelect.addEventListener("keydown", handleGitHubImportInputKeydown);
+  }
+  if (githubImportSelectAllBtn) {
+    githubImportSelectAllBtn.addEventListener("click", function() {
+      const allPaths = availableGitHubImportPaths.slice();
+      const shouldSelectAll = selectedGitHubImportPaths.size !== allPaths.length;
+      setGitHubSelectedPaths(shouldSelectAll ? allPaths : []);
+    });
+  }
+
+  fileInput.addEventListener("change", function (e) {
+    const file = e.target.files[0];
+    if (file) {
+      importMarkdownFile(file);
+    }
+    this.value = "";
+  });
+
+  exportMd.addEventListener("click", function () {
+    if (typeof Neutralino !== 'undefined') {
+      nativeSaveMarkdown();
+      return;
+    }
+    try {
+      const blob = new Blob([markdownEditor.value], {
+        type: "text/markdown;charset=utf-8",
+      });
+      saveAs(blob, "document.md");
+    } catch (e) {
+      console.error("Export failed:", e);
+      alert("Export failed: " + e.message);
+    }
+  });
+
+  exportHtml.addEventListener("click", function () {
+    try {
+      const { frontmatter, body } = parseFrontmatter(markdownEditor.value);
+      const tableHtml = frontmatter ? renderFrontmatterTable(frontmatter) : '';
+      const referenceData = extractReferenceDefinitions(body);
+      const html = tableHtml + marked.parse(referenceData.cleanedMarkdown);
+      const sanitizedHtml = DOMPurify.sanitize(html, {
+        ADD_TAGS: ['mjx-container', 'input'], 
+        ADD_ATTR: ['id', 'class', 'style', 'align', 'type', 'checked', 'disabled', 'data-original-code'],
+        ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel|blob):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
+      });
+      const tempContainer = document.createElement("div");
+      tempContainer.innerHTML = sanitizedHtml;
+      applyReferencePreviewLinks(tempContainer, referenceData.definitions);
+      enhanceGitHubAlerts(tempContainer);
+      const enhancedHtml = tempContainer.innerHTML;
+      const isDarkTheme =
+        document.documentElement.getAttribute("data-theme") === "dark";
+      const cssTheme = isDarkTheme
+        ? "https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown-dark.min.css"
+        : "https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown.min.css";
+      const fullHtml = `<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Markdown Export</title>
+  <link rel="stylesheet" href="${cssTheme}">
+  <script>
+      window.MathJax = {
+          loader: { load: ['[tex]/ams', '[tex]/boldsymbol'] },
+          tex: {
+              inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],
+              displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],
+              processEscapes: true,
+              packages: { '[+]': ['ams', 'boldsymbol'] }
+          }
+      };
+  </script>
+  <script defer src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.min.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/mermaid@11.6.0/dist/mermaid.min.js"></script>
+  <style>
+      html {
+          background-color: ${isDarkTheme ? "#0d1117" : "#ffffff"};
+      }
+      body {
+          margin: 0;
+          background-color: ${isDarkTheme ? "#0d1117" : "#ffffff"};
+          color: ${isDarkTheme ? "#c9d1d9" : "#24292e"};
+      }
+      .markdown-body {
+          box-sizing: border-box;
+          min-width: 200px;
+          max-width: 100%;
+          width: fit-content;
+          margin: 0 auto;
+          padding: 45px;
+          background-color: ${isDarkTheme ? "#0d1117" : "#ffffff"};
+          color: ${isDarkTheme ? "#c9d1d9" : "#24292e"};
+      }
+      .markdown-body > p,
+      .markdown-body > ul,
+      .markdown-body > ol,
+      .markdown-body > blockquote,
+      .markdown-body > h1,
+      .markdown-body > h2,
+      .markdown-body > h3,
+      .markdown-body > h4,
+      .markdown-body > h5,
+      .markdown-body > h6,
+      .markdown-body > pre,
+      .markdown-body > table,
+      .markdown-body > details,
+      .markdown-body > dl,
+      .markdown-body > hr {
+          max-width: 980px;
+          margin-left: auto !important;
+          margin-right: auto !important;
+      }
+
+
+      /* Syntax Highlighting */
+      .hljs-doctag, .hljs-keyword, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-variable.language_ { color: ${isDarkTheme ? "#ff7b72" : "#d73a49"}; }
+      .hljs-title, .hljs-title.class_, .hljs-title.class_.inherited__, .hljs-title.function_ { color: ${isDarkTheme ? "#d2a8ff" : "#6f42c1"}; }
+      .hljs-attr, .hljs-attribute, .hljs-literal, .hljs-meta, .hljs-number, .hljs-operator, .hljs-variable, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-id { color: ${isDarkTheme ? "#79c0ff" : "#005cc5"}; }
+      .hljs-regexp, .hljs-string, .hljs-meta .hljs-string { color: ${isDarkTheme ? "#a5d6ff" : "#032f62"}; }
+      .hljs-built_in, .hljs-symbol { color: ${isDarkTheme ? "#ffa657" : "#e36209"}; }
+      .hljs-comment, .hljs-code, .hljs-formula { color: ${isDarkTheme ? "#8b949e" : "#6a737d"}; }
+      .hljs-name, .hljs-quote, .hljs-selector-tag, .hljs-selector-pseudo { color: ${isDarkTheme ? "#7ee787" : "#22863a"}; }
+      .hljs-subst { color: ${isDarkTheme ? "#c9d1d9" : "#24292e"}; }
+      .hljs-section { color: ${isDarkTheme ? "#1f6feb" : "#005cc5"}; font-weight: bold; }
+      .hljs-bullet { color: ${isDarkTheme ? "#79c0ff" : "#005cc5"}; }
+      .hljs-emphasis { font-style: italic; }
+      .hljs-strong { font-weight: bold; }
+      .hljs-addition { color: ${isDarkTheme ? "#aff5b4" : "#22863a"}; background-color: ${isDarkTheme ? "#033a16" : "#f0fff4"}; }
+      .hljs-deletion { color: ${isDarkTheme ? "#ffdcd7" : "#b31d28"}; background-color: ${isDarkTheme ? "#67060c" : "#ffeef0"}; }
+
+      .markdown-alert {
+          padding: 0.5rem 1rem;
+          margin-bottom: 16px;
+          border-left: 0.25em solid;
+          border-radius: 0.375rem;
+      }
+      .markdown-alert > :last-child {
+          margin-bottom: 0;
+      }
+      .markdown-alert-title {
+          margin: 0 0 8px;
+          font-weight: 600;
+          line-height: 1.25;
+          display: flex;
+          align-items: center;
+          gap: 8px;
+      }
+      .markdown-alert-icon {
+          display: inline-flex;
+          width: 16px;
+          height: 16px;
+      }
+      .markdown-alert-icon svg {
+          width: 16px;
+          height: 16px;
+          fill: currentColor;
+      }
+      .markdown-alert-note { color: ${isDarkTheme ? "#4493f8" : "#0969da"}; border-left-color: ${isDarkTheme ? "#4493f8" : "#0969da"}; background-color: ${isDarkTheme ? "rgba(31, 111, 235, 0.15)" : "#ddf4ff"}; }
+      .markdown-alert-tip { color: ${isDarkTheme ? "#3fb950" : "#1a7f37"}; border-left-color: ${isDarkTheme ? "#3fb950" : "#1a7f37"}; background-color: ${isDarkTheme ? "rgba(35, 134, 54, 0.15)" : "#dafbe1"}; }
+      .markdown-alert-important { color: ${isDarkTheme ? "#ab7df8" : "#8250df"}; border-left-color: ${isDarkTheme ? "#ab7df8" : "#8250df"}; background-color: ${isDarkTheme ? "rgba(137, 87, 229, 0.15)" : "#fbefff"}; }
+      .markdown-alert-warning { color: ${isDarkTheme ? "#d29922" : "#9a6700"}; border-left-color: ${isDarkTheme ? "#d29922" : "#9a6700"}; background-color: ${isDarkTheme ? "rgba(210, 153, 34, 0.18)" : "#fff8c5"}; }
+      .markdown-alert-caution { color: ${isDarkTheme ? "#f85149" : "#cf222e"}; border-left-color: ${isDarkTheme ? "#f85149" : "#cf222e"}; background-color: ${isDarkTheme ? "rgba(248, 81, 73, 0.18)" : "#ffebe9"}; }
+      .markdown-alert > *:not(.markdown-alert-title) { color: ${isDarkTheme ? "#c9d1d9" : "#24292e"}; }
+
+      .frontmatter-table {
+          width: 100%;
+          border-collapse: collapse;
+          margin-bottom: 24px;
+          font-size: 14px;
+      }
+      .frontmatter-table th,
+      .frontmatter-table td {
+          border: 1px solid ${isDarkTheme ? "#30363d" : "#e1e4e8"};
+          padding: 8px 12px;
+          text-align: left;
+      }
+      .frontmatter-table th {
+          font-weight: 600;
+          background-color: ${isDarkTheme ? "#161b22" : "#f6f8fa"};
+          width: 150px;
+      }
+
+      /* Footnote styles */
+      .footnotes {
+          margin-top: 1.5rem;
+          font-size: 0.9em;
+          border-top: 1px solid ${isDarkTheme ? "#30363d" : "#eaecef"};
+          padding-top: 8px;
+      }
+      .footnotes ol {
+          padding-left: 1.5em;
+      }
+      .footnotes ol > li::marker {
+          content: "[" counter(list-item) "] ";
+          font-weight: 600;
+      }
+      .footnotes li > p {
+          margin: 0.2em 0;
+      }
+      .footnote-ref a,
+      .footnote-backref {
+          text-decoration: none;
+      }
+      .footnote-backref {
+          margin-left: 0.4em;
+      }
+      a.reference-link {
+          font-size: 0.75em;
+          letter-spacing: -0.02em;
+          line-height: 1;
+          vertical-align: super;
+          position: relative;
+          top: 0.08em;
+      }
+
+      /* Mermaid and Math styles */
+      .mermaid-container {
+          position: relative;
+          margin-bottom: 16px;
+      }
+      .math-block {
+          margin: 1em 0;
+          overflow-x: auto;
+          text-align: center;
+      }
+
+      @media (max-width: 767px) {
+          .markdown-body {
+              padding: 15px;
+          }
+      }
+  </style>
+</head>
+<body>
+  <article class="markdown-body">
+      ${enhancedHtml}
+  </article>
+      <script>
+      function fitMarkdownExportToContent() {
+          var article = document.querySelector('.markdown-body');
+          if (!article) return;
+
+          article.style.width = '';
+          article.style.maxWidth = '980px';
+
+          var overflow = article.scrollWidth - article.clientWidth;
+          if (overflow <= 1) return;
+
+          var styles = window.getComputedStyle(article);
+          var paddingLeft = parseFloat(styles.paddingLeft) || 0;
+          var paddingRight = parseFloat(styles.paddingRight) || 0;
+          var borderRight = parseFloat(styles.borderRightWidth) || 0;
+          var borderLeft = parseFloat(styles.borderLeftWidth) || 0;
+          var boxSizing = styles.boxSizing;
+
+          var requiredWidth = boxSizing === 'border-box'
+              ? Math.ceil(article.scrollWidth + borderLeft + borderRight)
+              : Math.ceil(article.scrollWidth - paddingLeft - paddingRight);
+
+          article.style.width = requiredWidth + 'px';
+          article.style.maxWidth = 'none';
+      }
+
+      function queueMarkdownExportFit() {
+          window.requestAnimationFrame(function () {
+              window.requestAnimationFrame(fitMarkdownExportToContent);
+          });
+      }
+
+      window.addEventListener('load', function () {
+          var mathReady = Promise.resolve();
+          var article = document.querySelector('.markdown-body');
+          if (article && window.MutationObserver) {
+              new MutationObserver(queueMarkdownExportFit).observe(article, {
+                  childList: true,
+                  subtree: true
+              });
+          }
+          if (window.MathJax && typeof window.MathJax.typesetPromise === 'function') {
+              mathReady = window.MathJax.typesetPromise().catch(function (err) {
+                  console.warn('MathJax typeset failed:', err);
+              });
+          }
+          if (window.mermaid) {
+              try {
+                  window.mermaid.initialize({ startOnLoad: true, theme: '${isDarkTheme ? "dark" : "default"}' });
+              } catch (e) {
+                  console.warn('Mermaid initialization failed:', e);
+              }
+          }
+          mathReady.finally(queueMarkdownExportFit);
+          queueMarkdownExportFit();
+      });
+      window.addEventListener('resize', queueMarkdownExportFit);
+  </script>
+</body>
+</html>`;
+      const blob = new Blob([fullHtml], { type: "text/html;charset=utf-8" });
+      if (typeof Neutralino !== 'undefined') {
+        nativeSaveHtml(fullHtml);
+      } else {
+        saveAs(blob, "document.html");
+      }
+    } catch (e) {
+      console.error("HTML export failed:", e);
+      alert("HTML export failed: " + e.message);
+    }
+  });
+
+  // ============================================
+  // Page-Break Detection Functions (Story 1.1)
+  // ============================================
+
+  // Page configuration constants for A4 PDF export
+  const PAGE_CONFIG = {
+    a4Width: 210,           // mm
+    a4Height: 297,          // mm
+    margin: 15,             // mm each side
+    contentWidth: 180,      // 210 - 30 (margins)
+    contentHeight: 267,     // 297 - 30 (margins)
+    windowWidth: 1000,      // html2canvas config
+    scale: 2                // html2canvas scale factor
+  };
+
+  const PDF_EXPORT_DEBUG = true;
+  let activePdfExport = null;
+
+  class PdfExportCancelledError extends Error {
+    constructor() {
+      super("PDF generation cancelled.");
+      this.name = "PdfExportCancelledError";
+    }
+  }
+
+  function logPdfExportDebug(...args) {
+    if (PDF_EXPORT_DEBUG) console.log(...args);
+  }
+
+  function throwIfPdfExportAborted(signal) {
+    if (signal && signal.aborted) {
+      throw new PdfExportCancelledError();
+    }
+  }
+
+  function runPdfAbortable(state, promise) {
+    throwIfPdfExportAborted(state.signal);
+
+    return new Promise((resolve, reject) => {
+      const handleAbort = () => reject(new PdfExportCancelledError());
+      state.signal.addEventListener("abort", handleAbort, { once: true });
+
+      Promise.resolve(promise)
+        .then(resolve, reject)
+        .finally(() => {
+          state.signal.removeEventListener("abort", handleAbort);
+        });
+    });
+  }
+
+  function formatPdfExportEta(ms) {
+    if (!Number.isFinite(ms) || ms <= 0) return "Calculating...";
+    const seconds = Math.ceil(ms / 1000);
+    if (seconds < 60) return `${seconds}s`;
+    const minutes = Math.floor(seconds / 60);
+    const remainder = seconds % 60;
+    return remainder ? `${minutes}m ${remainder}s` : `${minutes}m`;
+  }
+
+  function createPdfProgressState() {
+    const abortController = new AbortController();
+    const overlay = document.createElement("div");
+    overlay.className = "pdf-progress-overlay";
+    overlay.setAttribute("role", "dialog");
+    overlay.setAttribute("aria-modal", "true");
+    overlay.setAttribute("aria-labelledby", "pdf-progress-title");
+
+    overlay.innerHTML = `
+      <div class="pdf-progress-modal">
+        <div class="pdf-progress-header">
+          <p class="pdf-progress-title" id="pdf-progress-title">Generating PDF</p>
+          <button type="button" class="modal-close-btn pdf-progress-cancel-icon" aria-label="Cancel PDF generation" title="Cancel PDF generation">
+            <i class="bi bi-x-lg"></i>
+          </button>
+        </div>
+        <div class="pdf-progress-percent">0%</div>
+        <div class="pdf-progress-track"
+             role="progressbar"
+             aria-label="PDF generation progress"
+             aria-valuemin="0"
+             aria-valuemax="100"
+             aria-valuenow="0">
+          <div class="pdf-progress-fill"></div>
+        </div>
+        <div class="pdf-progress-details">
+          <div class="pdf-progress-detail">
+            <span>Current Step</span>
+            <strong class="pdf-progress-step">Preparing</strong>
+          </div>
+          <div class="pdf-progress-detail">
+            <span>Estimated remaining</span>
+            <strong class="pdf-progress-eta">Calculating...</strong>
+          </div>
+        </div>
+        <div class="pdf-progress-actions">
+          <button type="button" class="reset-modal-btn reset-modal-cancel pdf-progress-cancel">Cancel</button>
+        </div>
+      </div>`;
+
+    const state = {
+      abortController,
+      signal: abortController.signal,
+      startedAt: performance.now(),
+      overlay,
+      fill: overlay.querySelector(".pdf-progress-fill"),
+      percentText: overlay.querySelector(".pdf-progress-percent"),
+      progressBar: overlay.querySelector(".pdf-progress-track"),
+      stepText: overlay.querySelector(".pdf-progress-step"),
+      etaText: overlay.querySelector(".pdf-progress-eta"),
+      cancelButtons: overlay.querySelectorAll(".pdf-progress-cancel, .pdf-progress-cancel-icon"),
+      triggerHtml: new Map(),
+      tempElement: null,
+      cleanedUp: false
+    };
+
+    state.cancelButtons.forEach(button => {
+      button.addEventListener("click", () => cancelPdfExport(state));
+    });
+
+    return state;
+  }
+
+  function updatePdfProgress(state, percent, step) {
+    if (!state || state.cleanedUp) return;
+    const nextPercent = Math.max(0, Math.min(100, Math.round(percent)));
+    state.fill.style.width = `${nextPercent}%`;
+    state.percentText.textContent = `${nextPercent}%`;
+    state.progressBar.setAttribute("aria-valuenow", String(nextPercent));
+    state.stepText.textContent = step;
+
+    const elapsed = performance.now() - state.startedAt;
+    const eta = nextPercent > 5 && nextPercent < 100
+      ? (elapsed / nextPercent) * (100 - nextPercent)
+      : 0;
+    state.etaText.textContent = nextPercent >= 100 ? "Complete" : formatPdfExportEta(eta);
+  }
+
+  function setPdfExportTriggersBusy(state, busy) {
+    const triggers = [exportPdf, mobileExportPdf].filter(Boolean);
+    triggers.forEach((trigger, index) => {
+      if (busy) {
+        state.triggerHtml.set(trigger, trigger.innerHTML);
+        trigger.innerHTML = index === 0
+          ? '<i class="bi bi-hourglass-split"></i> Generating...'
+          : '<i class="bi bi-hourglass-split me-2"></i> Generating PDF...';
+        trigger.classList.add("pdf-export-loading");
+        trigger.setAttribute("aria-disabled", "true");
+        trigger.disabled = true;
+      } else {
+        if (state.triggerHtml.has(trigger)) {
+          trigger.innerHTML = state.triggerHtml.get(trigger);
+        }
+        trigger.classList.remove("pdf-export-loading");
+        trigger.removeAttribute("aria-disabled");
+        trigger.disabled = false;
+      }
+    });
+  }
+
+  function cleanupPdfExport(state) {
+    if (!state || state.cleanedUp) return;
+    state.cleanedUp = true;
+
+    if (state.tempElement && state.tempElement.parentNode) {
+      if (window.keepTempElementForAudit) {
+        window.auditedTempElement = state.tempElement;
+        console.log("Skipped tempElement removal for audit");
+      } else {
+        state.tempElement.parentNode.removeChild(state.tempElement);
+      }
+    }
+    if (state.overlay && state.overlay.parentNode) {
+      state.overlay.parentNode.removeChild(state.overlay);
+    }
+
+    setPdfExportTriggersBusy(state, false);
+    if (activePdfExport === state) {
+      activePdfExport = null;
+    }
+  }
+
+  function cancelPdfExport(state) {
+    if (!state || state.signal.aborted) return;
+    state.abortController.abort();
+    cleanupPdfExport(state);
+  }
+
+  async function waitForPdfFrame(state) {
+    throwIfPdfExportAborted(state.signal);
+    await new Promise(resolve => requestAnimationFrame(resolve));
+    throwIfPdfExportAborted(state.signal);
+  }
+
+  function markdownLikelyContainsMath(markdown) {
+    return /(^|[^\\])\$\$|\\\[|\\\(|(^|[^\\])\$[^$\n]+\$/.test(markdown);
+  }
+
+  function choosePdfCanvasScale(element) {
+    const pixelArea = element.offsetWidth * element.scrollHeight;
+    if (pixelArea > 14000000) return 1.25;
+    if (pixelArea > 8000000) return 1.5;
+    return PAGE_CONFIG.scale;
+  }
+
+  function readPixelStyle(element, propertyName) {
+    const value = window.getComputedStyle(element).getPropertyValue(propertyName);
+    return parseFloat(value) || 0;
+  }
+
+  function fitExportElementToContent(element) {
+    if (!element) return false;
+
+    const overflow = element.scrollWidth - element.clientWidth;
+    if (overflow <= 1) return false;
+
+    const paddingLeft = readPixelStyle(element, 'padding-left');
+    const paddingRight = readPixelStyle(element, 'padding-right');
+    const borderLeft = readPixelStyle(element, 'border-left-width');
+    const borderRight = readPixelStyle(element, 'border-right-width');
+    const boxSizing = window.getComputedStyle(element).boxSizing;
+
+    const requiredWidth = boxSizing === 'border-box'
+      ? Math.ceil(element.scrollWidth + borderLeft + borderRight)
+      : Math.ceil(element.scrollWidth - paddingLeft - paddingRight);
+
+    element.style.width = `${requiredWidth}px`;
+    return true;
+  }
+
+  /**
+   * Task 1: Identifies all graphic elements that may need page-break handling
+   * @param {HTMLElement} container - The container element to search within
+   * @returns {Array} Array of {element, type} objects
+   */
+  function identifyGraphicElements(container) {
+    const graphics = [];
+
+    // Query all targeting elements in precise DOM layout flow order
+    container.querySelectorAll('img, svg, pre, table, p, li, h1, h2, h3, h4, h5, h6, blockquote, hr, .math-block, mjx-container[display="true"]').forEach(el => {
+      const tag = el.tagName.toLowerCase();
+      
+      // Skip any elements nested inside blockquotes to treat blockquotes as atomic containers
+      if (el.parentElement && el.parentElement.closest('blockquote')) {
+        return;
+      }
+
+      // Skip any elements nested inside list items that contain block children (treat list items as atomic)
+      if (el.parentElement) {
+        const liAncestor = el.parentElement.closest('li');
+        if (liAncestor) {
+          const hasBlockChildren = liAncestor.querySelector('p, blockquote, pre, table, ul, ol') !== null;
+          if (hasBlockChildren) {
+            return;
+          }
+        }
+      }
+
+      let type = '';
+      
+      if (tag === 'img') type = 'img';
+      else if (tag === 'svg') {
+        if (!el.closest('mjx-container, .math-block')) {
+          type = 'svg';
+        }
+      }
+      else if (tag === 'pre') type = 'pre';
+      else if (tag === 'table') type = 'table';
+      else if (tag === 'hr') type = 'hr';
+      else if (tag === 'blockquote') {
+        type = 'blockquote';
+      }
+      else if (['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)) {
+        type = 'text';
+      } else if (tag === 'li') {
+        // Treat list items with block children as atomic containers, otherwise treat as text
+        const hasBlockChildren = el.querySelector('p, blockquote, pre, table, ul, ol') !== null;
+        if (hasBlockChildren) {
+          type = 'li';
+        } else {
+          type = 'text';
+        }
+      } else if (el.classList.contains('math-block') || tag === 'mjx-container') {
+        type = 'math';
+      }
+      
+      if (type) {
+        graphics.push({ element: el, type: type });
+      }
+    });
+
+    return graphics;
+  }
+
+  /**
+   * Calculates the computed line-height of a text element, defaulting based on tag if "normal"
+   * @param {HTMLElement} element 
+   * @returns {number} The line-height in pixels
+   */
+  function getElementLineHeight(element) {
+    const style = window.getComputedStyle(element);
+    const fontSize = parseFloat(style.fontSize) || 14;
+    let lineHeight = parseFloat(style.lineHeight);
+    
+    if (isNaN(lineHeight)) {
+      const tag = element.tagName.toLowerCase();
+      if (tag.startsWith('h')) {
+        lineHeight = fontSize * 1.25;
+      } else {
+        lineHeight = fontSize * 1.5;
+      }
+    } else if (lineHeight < 10) {
+      // Handle unitless line-height (e.g. "1.5")
+      lineHeight = lineHeight * fontSize;
+    }
+    return lineHeight;
+  }
+
+  /**
+   * Calculates the shift needed to align a split text element's lines with the page boundary
+   * @param {Object} item - The split element item
+   * @param {Array} pageBoundaries - Page break positions
+   * @returns {number} The shift in pixels
+   */
+  function calculateTextElementShift(item, pageBoundaries) {
+    const boundaryY = pageBoundaries[item.splitPageIndex];
+    if (boundaryY === undefined) return 0;
+
+    const element = item.element;
+    const style = window.getComputedStyle(element);
+    
+    const tag = element.tagName.toLowerCase();
+    const isHeading = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag);
+    
+    // Safety buffer (in pixels) to ensure text rendering, glyph ascenders,
+    // and sub-pixel anti-aliasing are pushed cleanly past the slicing boundary.
+    const SAFETY_BUFFER = 4;
+
+    // Headings should never be split. Push the entire heading to the next page.
+    if (isHeading) {
+      return (boundaryY - item.top) + SAFETY_BUFFER;
+    }
+
+    const paddingTop = parseFloat(style.paddingTop) || 0;
+    const borderTop = parseFloat(style.borderTopWidth) || 0;
+    const paddingBottom = parseFloat(style.paddingBottom) || 0;
+    const borderBottom = parseFloat(style.borderBottomWidth) || 0;
+
+    const lh = getElementLineHeight(element);
+    const contentTop = item.top + paddingTop + borderTop;
+    const contentHeight = item.height - paddingTop - paddingBottom - borderTop - borderBottom;
+    if (contentHeight <= 0) return 0;
+
+    const numLines = Math.max(1, Math.round(contentHeight / lh));
+
+    for (let i = 0; i < numLines; i++) {
+      const lineTop = contentTop + i * lh;
+      const lineBottom = contentTop + (i + 1) * lh;
+
+      // Check if this line is split by boundaryY (using a small tolerance to prevent float vibrations)
+      if (lineTop < boundaryY - 0.5 && lineBottom > boundaryY + 0.5) {
+        // Calculate shift to align the line's top with boundaryY, plus a safety buffer
+        return (boundaryY - lineTop) + SAFETY_BUFFER;
+      }
+    }
+
+    // Fallback: If a short paragraph/list item is split across pages but no single line is cut
+    // (e.g. boundary falls exactly in padding/margin), push the whole element.
+    if (item.height <= lh * 3) {
+      return (boundaryY - item.top) + SAFETY_BUFFER;
+    }
+
+    return 0;
+  }
+
+  /**
+   * Task 2: Calculates element positions relative to the container
+   * @param {Array} elements - Array of {element, type} objects
+   * @param {HTMLElement} container - The container element
+   * @returns {Array} Array with position data added
+   */
+  function calculateElementPositions(elements, container) {
+    const containerRect = container.getBoundingClientRect();
+
+    return elements.map(item => {
+      const rect = item.element.getBoundingClientRect();
+      const top = rect.top - containerRect.top;
+      const height = rect.height;
+      const bottom = top + height;
+
+      return {
+        element: item.element,
+        type: item.type,
+        top: top,
+        height: height,
+        bottom: bottom
+      };
+    });
+  }
+
+  /**
+   * Task 3: Calculates page boundary positions
+   * @param {number} totalHeight - Total height of content in pixels
+   * @param {number} elementWidth - Actual width of the rendered element in pixels
+   * @param {Object} pageConfig - Page configuration object
+   * @returns {Array} Array of y-coordinates where pages end
+   */
+  function calculatePageBoundaries(totalHeight, elementWidth, pageConfig) {
+    const aspectRatio = pageConfig.contentHeight / pageConfig.contentWidth;
+    const pageHeightPx = elementWidth * aspectRatio;
+
+    const boundaries = [];
+    let y = pageHeightPx;
+
+    while (y < totalHeight) {
+      boundaries.push(y);
+      y += pageHeightPx;
+    }
+
+    return { boundaries, pageHeightPx };
+  }
+
+  /**
+   * Task 4: Detects which elements would be split across page boundaries
+   * @param {Array} elements - Array of elements with position data
+   * @param {Array} pageBoundaries - Array of page break y-coordinates
+   * @returns {Array} Array of split elements with additional split info
+   */
+  function detectSplitElements(elements, pageBoundaries) {
+    if (!elements || elements.length === 0) return [];
+    if (!pageBoundaries || pageBoundaries.length === 0) return [];
+
+    const splitElements = [];
+
+    for (const item of elements) {
+      let startPage = 0;
+      for (let i = 0; i < pageBoundaries.length; i++) {
+        if (item.top >= pageBoundaries[i]) {
+          startPage = i + 1;
+        } else {
+          break;
+        }
+      }
+
+      let endPage = 0;
+      for (let i = 0; i < pageBoundaries.length; i++) {
+        if (item.bottom > pageBoundaries[i]) {
+          endPage = i + 1;
+        } else {
+          break;
+        }
+      }
+
+      if (endPage > startPage) {
+        const boundaryY = pageBoundaries[startPage] || pageBoundaries[0];
+        const overflowAmount = item.bottom - boundaryY;
+
+        splitElements.push({
+          element: item.element,
+          type: item.type,
+          top: item.top,
+          height: item.height,
+          splitPageIndex: startPage,
+          overflowAmount: overflowAmount
+        });
+      }
+    }
+
+    return splitElements;
+  }
+
+  /**
+   * Task 5: Main entry point for analyzing graphics for page breaks
+   * @param {HTMLElement} tempElement - The rendered content container
+   * @returns {Object} Analysis result with totalElements, splitElements, pageCount
+   */
+  function analyzeGraphicsForPageBreaks(tempElement, signal) {
+    try {
+      throwIfPdfExportAborted(signal);
+
+      const graphics = identifyGraphicElements(tempElement);
+      const elementsWithPositions = calculateElementPositions(graphics, tempElement);
+
+      throwIfPdfExportAborted(signal);
+
+      const totalHeight = Math.ceil(tempElement.getBoundingClientRect().height);
+      const elementWidth = tempElement.offsetWidth;
+      const { boundaries: pageBoundaries, pageHeightPx } = calculatePageBoundaries(
+        totalHeight,
+        elementWidth,
+        PAGE_CONFIG
+      );
+
+      const splitElements = detectSplitElements(elementsWithPositions, pageBoundaries);
+      const pageCount = pageBoundaries.length + 1;
+
+      return {
+        totalElements: graphics.length,
+        splitElements: splitElements,
+        pageCount: pageCount,
+        pageBoundaries: pageBoundaries,
+        pageHeightPx: pageHeightPx
+      };
+    } catch (error) {
+      if (error instanceof PdfExportCancelledError) throw error;
+      console.error('Page-break analysis failed:', error);
+      return {
+        totalElements: 0,
+        splitElements: [],
+        pageCount: 1,
+        pageBoundaries: [],
+        pageHeightPx: 0
+      };
+    }
+  }
+
+  // ============================================
+  // End Page-Break Detection Functions
+  // ============================================
+
+  // ============================================
+  // Page-Break Insertion Functions (Story 1.2)
+  // ============================================
+
+
+
+  /**
+   * Resets temporary styles applied to graphics elements back to their original state.
+   * This is called at the start of each layout iteration in the cascade loop.
+   * @param {HTMLElement} container - The container element to process
+   */
+  function resetGraphicsStyles(container) {
+    // Remove all dynamically inserted page-break spacers
+    container.querySelectorAll('.pdf-page-break-spacer').forEach(el => el.remove());
+
+    container.querySelectorAll('[data-pdf-original-margin-top]').forEach(el => {
+      el.style.marginTop = el.dataset.pdfOriginalMarginTop;
+      el.removeAttribute('data-pdf-original-margin-top');
+    });
+    container.querySelectorAll('[data-pdf-original-margin-bottom]').forEach(el => {
+      el.style.marginBottom = el.dataset.pdfOriginalMarginBottom;
+      el.removeAttribute('data-pdf-original-margin-bottom');
+    });
+    container.querySelectorAll('[data-pdf-original-transform]').forEach(el => {
+      el.style.transform = el.dataset.pdfOriginalTransform;
+      el.style.transformOrigin = '';
+      el.removeAttribute('data-pdf-original-transform');
+    });
+    container.querySelectorAll('[data-pdf-original-width]').forEach(el => {
+      el.style.width = el.dataset.pdfOriginalWidth;
+      el.removeAttribute('data-pdf-original-width');
+    });
+    container.querySelectorAll('[data-pdf-original-height]').forEach(el => {
+      el.style.height = el.dataset.pdfOriginalHeight;
+      el.removeAttribute('data-pdf-original-height');
+    });
+    container.querySelectorAll('[data-pdf-original-max-width]').forEach(el => {
+      el.style.maxWidth = el.dataset.pdfOriginalMaxWidth;
+      el.removeAttribute('data-pdf-original-max-width');
+    });
+    container.querySelectorAll('[data-pdf-original-font-size]').forEach(el => {
+      el.style.fontSize = el.dataset.pdfOriginalFontSize;
+      el.removeAttribute('data-pdf-original-font-size');
+    });
+    container.querySelectorAll('[data-pdf-original-overflow]').forEach(el => {
+      el.style.overflow = el.dataset.pdfOriginalOverflow;
+      el.removeAttribute('data-pdf-original-overflow');
+    });
+  }
+
+  function mergeSplitTables(container) {
+    const groupIds = new Set();
+    container.querySelectorAll('table[data-split-group-id]').forEach(t => {
+      if (t.dataset.splitGroupId) {
+        groupIds.add(t.dataset.splitGroupId);
+      }
+    });
+
+    for (const groupId of groupIds) {
+      const originalTable = container.querySelector(`table[data-split-group-id="${groupId}"]:not([data-split-part="true"])`);
+      if (!originalTable) continue;
+
+      const parts = Array.from(container.querySelectorAll(`table[data-split-group-id="${groupId}"][data-split-part="true"]`));
+      const tbody = originalTable.tBodies[0] || originalTable.querySelector('tbody') || originalTable;
+
+      for (const part of parts) {
+        const partTbody = part.tBodies[0] || part.querySelector('tbody') || part;
+        const rows = Array.from(partTbody.children).filter(child => child.tagName.toLowerCase() === 'tr');
+        for (const row of rows) {
+          tbody.appendChild(row);
+        }
+        part.remove();
+      }
+
+      const spacers = Array.from(container.querySelectorAll(`div[data-split-group-id="${groupId}"][data-split-spacer="true"]`));
+      for (const spacer of spacers) {
+        spacer.remove();
+      }
+
+      originalTable.removeAttribute('data-split-group-id');
+    }
+  }
+
+  function splitTables(container, pageHeightPx) {
+    mergeSplitTables(container);
+
+    const tables = Array.from(container.querySelectorAll('table'));
+    let groupCounter = 0;
+
+    for (const table of tables) {
+      if (table.dataset.splitPart === "true") continue;
+      const tableRect = table.getBoundingClientRect();
+      if (tableRect.height <= pageHeightPx) continue;
+
+      const tbody = table.tBodies[0] || table.querySelector('tbody');
+      if (!tbody) continue;
+
+      const rows = Array.from(tbody.children).filter(child => child.tagName.toLowerCase() === 'tr');
+      if (rows.length === 0) continue;
+
+      const groupId = `table-group-${groupCounter++}`;
+      table.dataset.splitGroupId = groupId;
+      const containerRect = container.getBoundingClientRect();
+
+      const rowPositions = rows.map(row => {
+        const rect = row.getBoundingClientRect();
+        return {
+          row: row,
+          top: rect.top - containerRect.top,
+          bottom: rect.bottom - containerRect.top,
+          height: rect.height
+        };
+      });
+
+      let currentTable = table;
+      let currentTbody = tbody;
+      let accumulatedShift = 0;
+
+      for (let i = 0; i < rowPositions.length; i++) {
+        const pos = rowPositions[i];
+        const shiftedTop = pos.top + accumulatedShift;
+        const shiftedBottom = pos.bottom + accumulatedShift;
+        const currentPageIndex = Math.floor(shiftedTop / pageHeightPx);
+        const nextPageBoundary = (currentPageIndex + 1) * pageHeightPx;
+
+        if (shiftedBottom > nextPageBoundary) {
+          const spacerHeight = nextPageBoundary - shiftedTop;
+          const originalThead = table.querySelector('thead');
+          const theadHeight = originalThead ? originalThead.getBoundingClientRect().height : 0;
+          accumulatedShift += spacerHeight + theadHeight;
+
+          const nextTable = table.cloneNode(false);
+          nextTable.removeAttribute('id');
+          nextTable.dataset.splitGroupId = groupId;
+          nextTable.dataset.splitPart = "true";
+
+          if (originalThead) nextTable.appendChild(originalThead.cloneNode(true));
+          const nextTbody = document.createElement('tbody');
+          nextTable.appendChild(nextTbody);
+
+          const spacer = document.createElement('div');
+          spacer.className = 'table-page-break-spacer';
+          spacer.dataset.splitGroupId = groupId;
+          spacer.dataset.splitSpacer = "true";
+          spacer.style.height = `${spacerHeight}px`;
+          spacer.style.margin = '0';
+          spacer.style.padding = '0';
+          spacer.style.border = 'none';
+
+          currentTable.parentNode.insertBefore(spacer, currentTable.nextSibling);
+          spacer.parentNode.insertBefore(nextTable, spacer.nextSibling);
+          currentTable = nextTable;
+          currentTbody = nextTbody;
+        }
+
+        if (currentTable !== table) currentTbody.appendChild(pos.row);
+      }
+    }
+  }
+
+  function applyPageBreaksWithCascade(tempElement, pageConfig, maxIterations = 10, signal) {
+    let iteration = 0;
+    let analysis;
+
+    const elementWidth = tempElement.offsetWidth;
+    const aspectRatio = pageConfig.contentHeight / pageConfig.contentWidth;
+    const pageHeightPx = elementWidth * aspectRatio;
+
+    const lastAdjustments = new Map(); // Store {margin, scale} for each element
+
+    do {
+      throwIfPdfExportAborted(signal);
+
+      // Reset graphics element styles applied in previous iterations
+      resetGraphicsStyles(tempElement);
+
+      // Split tables at page breaks dynamically using calculated pageHeightPx
+      splitTables(tempElement, pageHeightPx);
+
+      // We must get ALL target elements in document order
+      const graphics = identifyGraphicElements(tempElement);
+      const elementsWithPositions = calculateElementPositions(graphics, tempElement);
+
+      // Get page boundaries based on current height
+      const totalHeight = Math.ceil(tempElement.getBoundingClientRect().height);
+      const { boundaries: pageBoundaries, pageHeightPx: pageHeightPxFromAnalysis } = calculatePageBoundaries(
+        totalHeight,
+        elementWidth,
+        pageConfig
+      );
+
+      let adjustmentsMade = false;
+      let accumulatedShift = 0;
+      const currentIterationAdjustments = new Map();
+
+      for (const item of elementsWithPositions) {
+        throwIfPdfExportAborted(signal);
+
+        const currentTop = item.top + accumulatedShift;
+        const currentBottom = currentTop + item.height;
+
+        let targetMargin = 0;
+        let targetScale = 1.0;
+
+        // 1. Heading Keep-With-Next Rule (must run for all headings regardless of split)
+        const tag = item.element.tagName.toLowerCase();
+        const isHeading = item.type === 'text' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag);
+        
+        if (isHeading) {
+          let nextBoundaryY = null;
+          for (const boundary of pageBoundaries) {
+            if (currentTop < boundary) {
+              nextBoundaryY = boundary;
+              break;
+            }
+          }
+          if (nextBoundaryY !== null) {
+            const distanceToBoundary = nextBoundaryY - currentTop;
+            if (distanceToBoundary < 70) {
+              targetMargin = distanceToBoundary + 4; // Push heading entirely to next page
+            }
+          }
+        }
+
+        // 2. If not already pushed by Keep-With-Next, perform standard page-split calculations
+        if (targetMargin === 0) {
+          // Check if this element crosses any page boundary or starts extremely close to it (sub-pixel safety)
+          let splitPageIndex = -1;
+          for (let i = 0; i < pageBoundaries.length; i++) {
+            if (currentTop < pageBoundaries[i] + 12 && currentBottom > pageBoundaries[i]) {
+              splitPageIndex = i;
+              break;
+            }
+          }
+
+          if (splitPageIndex !== -1) {
+            const boundaryY = pageBoundaries[splitPageIndex];
+            const remainingSpace = boundaryY - currentTop;
+
+            if (item.type === 'text') {
+              const shiftedItem = { ...item, top: currentTop, splitPageIndex: splitPageIndex };
+              const shift = calculateTextElementShift(shiftedItem, pageBoundaries);
+              if (shift > 0.5) {
+                targetMargin = shift;
+              }
+            } else {
+              // Graphic element splitting (with larger buffer to ensure complete clearance)
+              const buffer = 15;
+              const scaleNeeded = (remainingSpace - buffer) / item.height;
+              const remainingSpacePercent = remainingSpace / pageHeightPxFromAnalysis;
+
+              const isTextContainer = ['blockquote', 'li', 'table', 'pre', 'math'].includes(item.type);
+
+              // Fit on current page by scaling if it's an image/svg and space/scale are acceptable.
+              // Otherwise, always push text/block containers to next page to prevent transform-scaling bugs.
+              if (!isTextContainer && remainingSpacePercent >= 0.20 && scaleNeeded >= 0.6) {
+                targetScale = Math.min(1.0, scaleNeeded);
+              } else {
+                // Push to next page
+                const marginNeeded = boundaryY - currentTop + buffer;
+                targetMargin = marginNeeded;
+
+                // Check if it fits the next page height after being pushed (Rule 3 Case C)
+                const newTop = currentTop + marginNeeded;
+                const newBottom = newTop + item.height;
+                const nextBoundaryY = pageBoundaries[splitPageIndex + 1] || (boundaryY + pageHeightPxFromAnalysis);
+                if (newBottom > nextBoundaryY) {
+                  const scaleToFitPage = (pageHeightPxFromAnalysis - 20) / item.height;
+                  targetScale = Math.max(0.5, Math.min(1.0, scaleToFitPage));
+                }
+              }
+            }
+          } else {
+            // Element is not split. But graphic elements taller than a page must still scale to fit!
+            if (item.type !== 'text' && item.height > pageHeightPxFromAnalysis) {
+              const scaleToFitPage = (pageHeightPxFromAnalysis - 20) / item.height;
+              targetScale = Math.max(0.5, Math.min(1.0, scaleToFitPage));
+            }
+          }
+        }
+
+        // Check if this calculated adjustment is different from the previous iteration
+        const prevAdjustment = lastAdjustments.get(item.element) || { margin: 0, scale: 1.0 };
+        if (Math.abs(targetMargin - prevAdjustment.margin) > 0.1 || Math.abs(targetScale - prevAdjustment.scale) > 0.001) {
+          adjustmentsMade = true;
+        }
+
+        currentIterationAdjustments.set(item.element, { margin: targetMargin, scale: targetScale });
+
+        // Apply style adjustments to the DOM
+        if (targetMargin > 0) {
+          let targetElement = item.element;
+
+          // Redirect inline image or svg margins to their parent block element if nested
+          if (item.type === 'svg' && item.element.parentElement) {
+            const parent = item.element.parentElement;
+            if (['p', 'li', 'blockquote'].includes(parent.tagName.toLowerCase())) {
+              targetElement = parent;
+            } else {
+              targetElement = parent;
+            }
+          } else if (item.type === 'img' && item.element.parentElement) {
+            const parent = item.element.parentElement;
+            if (item.element.classList.contains('mermaid-img')) {
+              if (parent.parentElement && parent.parentElement.classList.contains('mermaid-container')) {
+                targetElement = parent.parentElement;
+              } else {
+                targetElement = parent;
+              }
+            } else if (['p', 'li', 'blockquote'].includes(parent.tagName.toLowerCase())) {
+              targetElement = parent;
+            }
+          }
+
+          // If target is a list item, apply marginTop directly to avoid invalid HTML / collapsed spacers
+          if (targetElement.tagName.toLowerCase() === 'li') {
+            if (!targetElement.dataset.hasOwnProperty('pdfOriginalMarginTop')) {
+              targetElement.dataset.pdfOriginalMarginTop = targetElement.style.marginTop || '';
+            }
+            targetElement.style.marginTop = `${targetMargin}px`;
+          } else {
+            // Create a physical spacer element to avoid margin collapse issues entirely
+            const spacer = document.createElement('div');
+            spacer.className = 'pdf-page-break-spacer';
+            spacer.style.height = `${targetMargin}px`;
+            spacer.style.margin = '0';
+            spacer.style.padding = '0';
+            spacer.style.border = 'none';
+            spacer.style.display = 'block';
+
+            targetElement.parentNode.insertBefore(spacer, targetElement);
+          }
+          accumulatedShift += targetMargin;
+        }
+
+        if (targetScale < 1.0) {
+          applyGraphicScaling(item.element, targetScale, item.type);
+          const heightSaved = item.height * (1.0 - targetScale);
+          accumulatedShift -= heightSaved;
+        }
+      }
+
+      // Copy current adjustments to lastAdjustments
+      lastAdjustments.clear();
+      for (const [el, adj] of currentIterationAdjustments) {
+        lastAdjustments.set(el, adj);
+      }
+
+      if (!adjustmentsMade) {
+        break;
+      }
+
+      iteration++;
+    } while (iteration < maxIterations);
+
+    if (iteration >= maxIterations) {
+      console.warn('Page-break stabilization reached max iterations:', maxIterations);
+    }
+
+    // Return the final page boundaries and height analysis for the export flow
+    analysis = analyzeGraphicsForPageBreaks(tempElement, signal);
+    
+    logPdfExportDebug('Page-break cascade complete:', {
+      iterations: iteration,
+      finalSplitCount: analysis.splitElements.length
+    });
+
+    return analysis;
+  }
+
+  // ============================================
+  // End Page-Break Insertion Functions
+  // ============================================
+
+  // ============================================
+  // Oversized Graphics Scaling Functions (Story 1.3)
+  // ============================================
+
+  const MIN_SCALE_FACTOR = 0.5;
+
+  function calculateScaleFactor(elementHeight, availableHeight, buffer = 5) {
+    const targetHeight = availableHeight - buffer;
+    let scaleFactor = targetHeight / elementHeight;
+    let wasClampedToMin = false;
+
+    if (scaleFactor < MIN_SCALE_FACTOR) {
+      console.warn(
+        `Warning: Large graphic requires ${(scaleFactor * 100).toFixed(0)}% scaling. ` +
+        `Clamping to minimum ${MIN_SCALE_FACTOR * 100}%. Content may be cut off.`
+      );
+      scaleFactor = MIN_SCALE_FACTOR;
+      wasClampedToMin = true;
+    }
+
+    return { scaleFactor, wasClampedToMin };
+  }
+
+  function applyGraphicScaling(element, scaleFactor, elementType) {
+    if (!element.dataset.hasOwnProperty('pdfOriginalTransform')) {
+      element.dataset.pdfOriginalTransform = element.style.transform || '';
+    }
+    if (!element.dataset.hasOwnProperty('pdfOriginalMarginBottom')) {
+      element.dataset.pdfOriginalMarginBottom = element.style.marginBottom || '';
+    }
+
+    if (elementType === 'svg' || elementType === 'img') {
+      if (!element.dataset.hasOwnProperty('pdfOriginalWidth')) {
+        element.dataset.pdfOriginalWidth = element.style.width || '';
+      }
+      if (!element.dataset.hasOwnProperty('pdfOriginalHeight')) {
+        element.dataset.pdfOriginalHeight = element.style.height || '';
+      }
+      if (!element.dataset.hasOwnProperty('pdfOriginalMaxWidth')) {
+        element.dataset.pdfOriginalMaxWidth = element.style.maxWidth || '';
+      }
+
+      let origWidth = parseFloat(element.dataset.pdfOriginalClientWidth);
+      let origHeight = parseFloat(element.dataset.pdfOriginalClientHeight);
+
+      if (isNaN(origWidth) || isNaN(origHeight)) {
+        origWidth = element.clientWidth || element.getBoundingClientRect().width;
+        origHeight = element.clientHeight || element.getBoundingClientRect().height;
+        element.dataset.pdfOriginalClientWidth = String(origWidth);
+        element.dataset.pdfOriginalClientHeight = String(origHeight);
+      }
+
+      // Apply physical scale
+      element.style.width = `${origWidth * scaleFactor}px`;
+      element.style.height = `${origHeight * scaleFactor}px`;
+      if (elementType === 'svg') {
+        element.style.maxWidth = 'none';
+      }
+    } else {
+      // For pre, table, blockquote, math, li, etc.
+      // Use transform: scale combined with physical height and overflow hidden to guarantee no native splits
+      if (!element.dataset.hasOwnProperty('pdfOriginalHeight')) {
+        element.dataset.pdfOriginalHeight = element.style.height || '';
+      }
+      if (!element.dataset.hasOwnProperty('pdfOriginalOverflow')) {
+        element.dataset.pdfOriginalOverflow = element.style.overflow || '';
+      }
+
+      element.style.transform = `scale(${scaleFactor})`;
+      element.style.transformOrigin = 'top left';
+
+      let origHeight = parseFloat(element.dataset.pdfOriginalClientHeight);
+      if (isNaN(origHeight)) {
+        origHeight = element.offsetHeight || element.getBoundingClientRect().height;
+        element.dataset.pdfOriginalClientHeight = String(origHeight);
+      }
+
+      const scaledHeight = origHeight * scaleFactor;
+      element.style.height = `${scaledHeight}px`;
+      element.style.overflow = 'hidden';
+    }
+  }
+
+
+
+  function waitForAllImages(container) {
+    const imgs = Array.from(container.querySelectorAll('img'));
+    const promises = imgs.map(img => {
+      if (img.complete) return Promise.resolve();
+      return new Promise(resolve => {
+        img.addEventListener('load', resolve, { once: true });
+        img.addEventListener('error', resolve, { once: true });
+      });
+    });
+    return Promise.all(promises);
+  }
+
+  // ============================================
+  // End Oversized Graphics Scaling Functions
+  // ============================================
+
+  exportPdf.addEventListener("click", async function (event) {
+    event.preventDefault();
+    logPdfExportDebug("PDF export button clicked!");
+    if (activePdfExport) {
+      logPdfExportDebug("PDF export already active, ignoring click");
+      return;
+    }
+
+    const progressState = createPdfProgressState();
+    activePdfExport = progressState;
+    setPdfExportTriggersBusy(progressState, true);
+    document.body.appendChild(progressState.overlay);
+    updatePdfProgress(progressState, 3, "Starting");
+    progressState.overlay.querySelector(".pdf-progress-cancel")?.focus();
+
+    try {
+      logPdfExportDebug("PDF export try block entered. typeof jspdf:", typeof jspdf, "typeof html2canvas:", typeof html2canvas);
+      // PERF-002: Lazy-load PDF libraries on first export
+      if (typeof jspdf === 'undefined' || typeof html2canvas === 'undefined') {
+        logPdfExportDebug("Lazy loading PDF libraries started...");
+        updatePdfProgress(progressState, 8, "Loading PDF libraries");
+        await runPdfAbortable(progressState, Promise.all([
+          loadScript(CDN.jspdf).then(() => logPdfExportDebug("jspdf load callback fired")),
+          loadScript(CDN.html2canvas).then(() => logPdfExportDebug("html2canvas load callback fired"))
+        ]));
+        logPdfExportDebug("Lazy loading PDF libraries resolved.");
+        throwIfPdfExportAborted(progressState.signal);
+      }
+      logPdfExportDebug("Parsing markdown...");
+      updatePdfProgress(progressState, 15, "Parsing markdown");
+      await waitForPdfFrame(progressState);
+      const markdown = markdownEditor.value;
+      const html = marked.parse(markdown);
+      const sanitizedHtml = DOMPurify.sanitize(html, {
+        ADD_TAGS: ['mjx-container', 'svg', 'path', 'g', 'marker', 'defs', 'pattern', 'clipPath', 'input'],
+        ADD_ATTR: ['id', 'class', 'style', 'align', 'viewBox', 'd', 'fill', 'stroke', 'transform', 'marker-end', 'marker-start', 'type', 'checked', 'disabled', 'data-original-code'],
+        ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel|blob):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
+      });
+      throwIfPdfExportAborted(progressState.signal);
+
+      updatePdfProgress(progressState, 24, "Preparing document");
+      await waitForPdfFrame(progressState);
+      const tempElement = document.createElement("div");
+      progressState.tempElement = tempElement;
+      tempElement.className = "markdown-body pdf-export";
+      tempElement.innerHTML = sanitizedHtml;
+      enhanceGitHubAlerts(tempElement);
+      tempElement.style.padding = "0px";
+      tempElement.style.width = "210mm";
+      tempElement.style.margin = "0 auto";
+      tempElement.style.fontSize = "14px";
+      tempElement.style.position = "fixed";
+      tempElement.style.left = "-9999px";
+      tempElement.style.top = "0";
+
+      const currentTheme = document.documentElement.getAttribute("data-theme");
+      tempElement.style.backgroundColor = currentTheme === "dark" ? "#0d1117" : "#ffffff";
+      tempElement.style.color = currentTheme === "dark" ? "#c9d1d9" : "#24292e";
+
+      document.body.appendChild(tempElement);
+      await waitForPdfFrame(progressState);
+
+      const mermaidNodes = tempElement.querySelectorAll('.mermaid');
+      if (mermaidNodes.length > 0) {
+        updatePdfProgress(progressState, 34, "Rendering diagrams");
+        try {
+          if (typeof mermaid === 'undefined') {
+            await runPdfAbortable(progressState, loadScript(CDN.mermaid));
+          }
+          throwIfPdfExportAborted(progressState.signal);
+          initMermaid(true);
+          await runPdfAbortable(progressState, mermaid.init(undefined, mermaidNodes));
+          tempElement.querySelectorAll('.mermaid-container.is-loading').forEach(container => {
+            container.classList.remove('is-loading');
+          });
+
+          // Convert all rendered Mermaid SVGs inside tempElement to <img> tags with data URI sources
+          const compiledMermaids = tempElement.querySelectorAll('.mermaid-container');
+          compiledMermaids.forEach(container => {
+            const svgElement = container.querySelector('svg');
+            if (svgElement) {
+              const rect = svgElement.getBoundingClientRect();
+              const width = rect.width || svgElement.clientWidth || parseFloat(svgElement.getAttribute('width')) || 600;
+              const height = rect.height || svgElement.clientHeight || parseFloat(svgElement.getAttribute('height')) || 400;
+
+              const clonedSvg = svgElement.cloneNode(true);
+              clonedSvg.setAttribute('width', width);
+              clonedSvg.setAttribute('height', height);
+              if (!clonedSvg.getAttribute('viewBox')) {
+                clonedSvg.setAttribute('viewBox', `0 0 ${width} ${height}`);
+              }
+              clonedSvg.style.width = `${width}px`;
+              clonedSvg.style.height = `${height}px`;
+
+              const svgString = new XMLSerializer().serializeToString(clonedSvg);
+              const svgBase64 = btoa(unescape(encodeURIComponent(svgString)));
+              
+              const img = document.createElement('img');
+              img.className = 'mermaid-img';
+              if (svgElement.id) img.id = svgElement.id + '-img';
+              img.src = 'data:image/svg+xml;base64,' + svgBase64;
+              
+              img.style.width = `${width}px`;
+              img.style.height = `${height}px`;
+              img.style.maxWidth = '100%';
+              img.style.display = 'block';
+              img.style.margin = '0 auto';
+              
+              img.dataset.originalWidth = String(width);
+              img.dataset.originalHeight = String(height);
+
+              container.innerHTML = '';
+              container.appendChild(img);
+            }
+          });
+        } catch (mermaidError) {
+          if (mermaidError instanceof PdfExportCancelledError) throw mermaidError;
+          console.warn("Mermaid rendering issue:", mermaidError);
+          tempElement.querySelectorAll('.mermaid-container.is-loading').forEach(container => {
+            container.classList.remove('is-loading');
+          });
+        }
+        throwIfPdfExportAborted(progressState.signal);
+        await waitForPdfFrame(progressState);
+      }
+
+      if (window.MathJax && markdownLikelyContainsMath(markdown)) {
+        updatePdfProgress(progressState, 44, "Rendering math");
+        try {
+          await runPdfAbortable(progressState, MathJax.typesetPromise([tempElement]));
+        } catch (mathJaxError) {
+          if (mathJaxError instanceof PdfExportCancelledError) throw mathJaxError;
+          console.warn("MathJax rendering issue:", mathJaxError);
+        }
+        throwIfPdfExportAborted(progressState.signal);
+
+        // Hide MathJax assistive elements that cause duplicate text in PDF
+        // These are screen reader elements that html2canvas captures as visible
+        // Use multiple CSS properties to ensure html2canvas doesn't render them
+        const assistiveElements = tempElement.querySelectorAll('mjx-assistive-mml');
+        assistiveElements.forEach(el => {
+          el.style.display = 'none';
+          el.style.visibility = 'hidden';
+          el.style.position = 'absolute';
+          el.style.width = '0';
+          el.style.height = '0';
+          el.style.overflow = 'hidden';
+          el.remove(); // Remove entirely from DOM
+        });
+
+        // Also hide any MathJax script elements that might contain source
+        const mathScripts = tempElement.querySelectorAll('script[type*="math"], script[type*="tex"]');
+        mathScripts.forEach(el => el.remove());
+      }
+
+      await waitForPdfFrame(progressState);
+      fitExportElementToContent(tempElement);
+      await waitForPdfFrame(progressState);
+
+      // Await loading of all images and fonts (including converted Mermaid base64 images) before cascade sizing runs
+      updatePdfProgress(progressState, 50, "Loading document assets");
+      await runPdfAbortable(progressState, Promise.all([
+        waitForAllImages(tempElement),
+        document.fonts ? document.fonts.ready : Promise.resolve()
+      ]));
+      throwIfPdfExportAborted(progressState.signal);
+      await waitForPdfFrame(progressState);
+
+      // Analyze and apply page-breaks for graphics (Story 1.1 + 1.2)
+      updatePdfProgress(progressState, 55, "Optimizing page breaks");
+      const pageBreakAnalysis = applyPageBreaksWithCascade(tempElement, PAGE_CONFIG, 10, progressState.signal);
+      throwIfPdfExportAborted(progressState.signal);
+
+      await waitForPdfFrame(progressState);
+
+      const pdfOptions = {
+        orientation: 'portrait',
+        unit: 'mm',
+        format: 'a4',
+        compress: true,
+        hotfixes: ["px_scaling"]
+      };
+
+      const pdf = new jspdf.jsPDF(pdfOptions);
+      const pageWidth = pdf.internal.pageSize.getWidth();
+      const pageHeight = pdf.internal.pageSize.getHeight();
+      const margin = 15;
+      const contentWidth = pageWidth - (margin * 2);
+      const captureScale = choosePdfCanvasScale(tempElement);
+
+      updatePdfProgress(progressState, 65, "Capturing document");
+      const canvas = await runPdfAbortable(progressState, html2canvas(tempElement, {
+        scale: captureScale,
+        useCORS: true,
+        allowTaint: false,
+        logging: false,
+        windowWidth: Math.max(PAGE_CONFIG.windowWidth, Math.ceil(tempElement.getBoundingClientRect().width)),
+        windowHeight: Math.ceil(tempElement.getBoundingClientRect().height)
+      }));
+      await waitForPdfFrame(progressState);
+      throwIfPdfExportAborted(progressState.signal);
+
+      console.log(`[PDF DEBUG] canvas.width = ${canvas.width}, canvas.height = ${canvas.height}`);
+      console.log(`[PDF DEBUG] tempElement.offsetWidth = ${tempElement.offsetWidth}, rect.width = ${tempElement.getBoundingClientRect().width}`);
+      const scaleFactor = canvas.width / contentWidth;
+      console.log(`[PDF DEBUG] scaleFactor = ${scaleFactor}, PAGE_CONFIG.scale = ${PAGE_CONFIG.scale}, captureScale = ${captureScale}`);
+      const imgHeight = canvas.height / scaleFactor;
+      console.log(`[PDF DEBUG] imgHeight = ${imgHeight}, contentHeight = ${pageHeight - margin * 2}`);
+      // Introduce a 0.5mm tolerance to prevent rounding errors from creating a trailing blank page
+      const pagesCount = Math.ceil((imgHeight - 0.5) / (pageHeight - margin * 2));
+      console.log(`[PDF DEBUG] pagesCount = ${pagesCount}`);
+
+      updatePdfProgress(progressState, 76, "Rendering pages");
+      for (let page = 0; page < pagesCount; page++) {
+        throwIfPdfExportAborted(progressState.signal);
+        const pageProgress = 76 + ((page + 1) / pagesCount) * 18;
+        updatePdfProgress(progressState, pageProgress, `Rendering page ${page + 1} of ${pagesCount}`);
+
+        if (page > 0) pdf.addPage();
+
+        const sourceY = page * (pageHeight - margin * 2) * scaleFactor;
+        const sourceHeight = Math.min(canvas.height - sourceY, (pageHeight - margin * 2) * scaleFactor);
+        const destHeight = sourceHeight / scaleFactor;
+
+        const pageCanvas = document.createElement('canvas');
+        pageCanvas.width = canvas.width;
+        pageCanvas.height = sourceHeight;
+
+        const ctx = pageCanvas.getContext('2d');
+        ctx.drawImage(canvas, 0, sourceY, canvas.width, sourceHeight, 0, 0, canvas.width, sourceHeight);
+
+        const imgData = pageCanvas.toDataURL('image/png');
+        pdf.addImage(imgData, 'PNG', margin, margin, contentWidth, destHeight);
+        await waitForPdfFrame(progressState);
+      }
+
+      throwIfPdfExportAborted(progressState.signal);
+      updatePdfProgress(progressState, 98, "Preparing download");
+      pdf.save("document.pdf");
+      updatePdfProgress(progressState, 100, "Complete");
+
+    } catch (error) {
+      if (error instanceof PdfExportCancelledError || progressState.signal.aborted) {
+        console.info("PDF export cancelled");
+      } else {
+        console.error("PDF export failed:", error);
+        alert("PDF export failed: " + error.message);
+      }
+    } finally {
+      cleanupPdfExport(progressState);
+    }
+  });
+
+  copyMarkdownButton.addEventListener("click", function () {
+    try {
+      const markdownText = markdownEditor.value;
+      copyToClipboard(markdownText);
+    } catch (e) {
+      console.error("Copy failed:", e);
+      alert("Failed to copy Markdown: " + e.message);
+    }
+  });
+
+  async function copyTextToClipboard(text) {
+    if (navigator.clipboard && window.isSecureContext) {
+      await navigator.clipboard.writeText(text);
+      return;
+    }
+    const textArea = document.createElement("textarea");
+    textArea.value = text;
+    textArea.style.position = "fixed";
+    textArea.style.opacity = "0";
+    document.body.appendChild(textArea);
+    textArea.focus();
+    textArea.select();
+    const successful = document.execCommand("copy");
+    document.body.removeChild(textArea);
+    if (!successful) {
+      throw new Error("Copy command was unsuccessful");
+    }
+  }
+
+  async function copyToClipboard(text) {
+    try {
+      await copyTextToClipboard(text);
+      showCopiedMessage();
+    } catch (err) {
+      console.error("Copy failed:", err);
+      alert("Failed to copy HTML: " + err.message);
+    }
+  }
+
+  function showCopiedMessage() {
+    const originalText = copyMarkdownButton.innerHTML;
+    copyMarkdownButton.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
+
+    setTimeout(() => {
+      copyMarkdownButton.innerHTML = originalText;
+    }, 2000);
+  }
+
+  // ============================================
+  // Share via URL (pako compression + base64url)
+  // ============================================
+
+  const MAX_SHARE_URL_LENGTH = 32000;
+
+  function encodeMarkdownForShare(text) {
+    if (typeof pako === 'undefined') throw new Error('pako not loaded');
+    const compressed = pako.deflate(new TextEncoder().encode(text));
+    const chunkSize = 0x8000;
+    let binary = '';
+    for (let i = 0; i < compressed.length; i += chunkSize) {
+      binary += String.fromCharCode.apply(null, compressed.subarray(i, i + chunkSize));
+    }
+    return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
+  }
+
+  function decodeMarkdownFromShare(encoded) {
+    if (typeof pako === 'undefined') throw new Error('pako not loaded');
+    const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
+    const binary = atob(base64);
+    const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
+    return new TextDecoder().decode(pako.inflate(bytes));
+  }
+
+  // ============================================
+  // Share Modal
+  // ============================================
+
+  const shareModal        = document.getElementById('share-modal');
+  const shareModalCloseX  = document.getElementById('share-modal-close-icon');
+  const shareModalClose   = document.getElementById('share-modal-close');
+  const shareUrlInput     = document.getElementById('share-url-input');
+  const shareCopyBtn      = document.getElementById('share-copy-btn');
+  const shareModeView     = document.getElementById('share-mode-view');
+  const shareModeEdit     = document.getElementById('share-mode-edit');
+  const shareCardView     = document.getElementById('share-card-view');
+  const shareCardEdit     = document.getElementById('share-card-edit');
+
+  function buildShareUrl(mode) {
+    const markdownText = markdownEditor.value;
+    let encoded;
+    try {
+      encoded = encodeMarkdownForShare(markdownText);
+    } catch (e) {
+      console.error('Share encoding failed:', e);
+      return null;
+    }
+    const isLocal = window.location.origin.includes('localhost') || 
+                    window.location.origin.startsWith('file://') || 
+                    typeof Neutralino !== 'undefined';
+                    
+    const baseUrl = isLocal 
+      ? 'https://markdownviewer.pages.dev/' 
+      : window.location.origin + window.location.pathname;
+
+    const base = baseUrl + '#share=' + encoded;
+    return mode === 'edit' ? base + '&edit=1' : base;
+  }
+
+  function updateShareUrlField() {
+    const mode = shareModeView.checked ? 'view' : 'edit';
+    const url = buildShareUrl(mode);
+    if (!url) {
+      shareUrlInput.value = 'Error generating link.';
+      shareCopyBtn.disabled = true;
+      return;
+    }
+    const tooLarge = url.length > MAX_SHARE_URL_LENGTH;
+    if (tooLarge) {
+      shareUrlInput.value = 'Document too large to share via URL.';
+      shareCopyBtn.disabled = true;
+    } else {
+      shareUrlInput.value = url;
+      shareCopyBtn.disabled = false;
+    }
+  }
+
+  function openShareModal() {
+    // PERF-002: Lazy-load pako on first share
+    if (typeof pako === 'undefined') {
+      loadScript(CDN.pako).then(function() {
+        openShareModal();
+      }).catch(function(e) {
+        console.error('Failed to load pako:', e);
+        alert('Failed to load sharing library. Please check your internet connection.');
+      });
+      return;
+    }
+    // Reset to view-only by default each time
+    shareModeView.checked = true;
+    syncShareCardStyles();
+    updateShareUrlField();
+    shareModal.style.display = '';
+    requestAnimationFrame(() => {
+      shareModal.classList.add('is-visible');
+      shareModal.setAttribute('aria-hidden', 'false');
+    });
+  }
+
+  function closeShareModal() {
+    shareModal.classList.remove('is-visible');
+    shareModal.setAttribute('aria-hidden', 'true');
+    shareModal.addEventListener('transitionend', function handler() {
+      shareModal.style.display = 'none';
+      shareModal.removeEventListener('transitionend', handler);
+    });
+  }
+
+  function syncShareCardStyles() {
+    if (shareModeView.checked) {
+      shareCardView.classList.add('is-selected');
+      shareCardEdit.classList.remove('is-selected');
+    } else {
+      shareCardEdit.classList.add('is-selected');
+      shareCardView.classList.remove('is-selected');
+    }
+  }
+
+  shareModeView.addEventListener('change', function () {
+    syncShareCardStyles();
+    updateShareUrlField();
+  });
+  shareModeEdit.addEventListener('change', function () {
+    syncShareCardStyles();
+    updateShareUrlField();
+  });
+
+  shareCopyBtn.addEventListener('click', function () {
+    const url = shareUrlInput.value;
+    if (!url || shareCopyBtn.disabled) return;
+
+    function onCopied() {
+      const orig = shareCopyBtn.innerHTML;
+      shareCopyBtn.innerHTML = '<i class="bi bi-check-lg"></i>';
+      setTimeout(() => { shareCopyBtn.innerHTML = orig; }, 2000);
+    }
+
+    if (navigator.clipboard && window.isSecureContext) {
+      navigator.clipboard.writeText(url).then(onCopied).catch(() => {});
+    } else {
+      try {
+        const tmp = document.createElement('textarea');
+        tmp.value = url;
+        document.body.appendChild(tmp);
+        tmp.select();
+        document.execCommand('copy');
+        document.body.removeChild(tmp);
+        onCopied();
+      } catch (_) {}
+    }
+  });
+
+  shareModalCloseX.addEventListener('click', closeShareModal);
+  shareModalClose.addEventListener('click', closeShareModal);
+  shareModal.addEventListener('click', function (e) {
+    if (e.target === shareModal) closeShareModal();
+  });
+  document.addEventListener('keydown', function (e) {
+    if (e.key === 'Escape' && shareModal.classList.contains('is-visible')) {
+      closeShareModal();
+    }
+    
+    // Global Ctrl+F / Cmd+F interception
+    if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'f') {
+      e.preventDefault();
+      openFindReplaceModal();
+      const findInput = document.getElementById('find-replace-input');
+      if (findInput) {
+        findInput.focus();
+        findInput.select();
+      }
+      return;
+    }
+    
+    // Global Ctrl+H / Cmd+H interception
+    if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'h') {
+      e.preventDefault();
+      openFindReplaceModal();
+      const replaceInput = document.getElementById('find-replace-with');
+      if (replaceInput) {
+        replaceInput.focus();
+        replaceInput.select();
+      }
+      return;
+    }
+    
+    // Global Escape dismissal for find-replace panel
+    if (e.key === 'Escape' && isFindModalOpen) {
+      e.preventDefault();
+      closeFindReplaceModal();
+      return;
+    }
+  });
+
+  shareButton.addEventListener('click', openShareModal);
+  mobileShareButton.addEventListener('click', openShareModal);
+
+  function loadFromShareHash() {
+    // PERF-002: Lazy-load pako when loading shared URL content
+    if (typeof pako === 'undefined') {
+      const hash = window.location.hash;
+      if (!hash.startsWith('#share=')) return;
+      loadScript(CDN.pako).then(function() {
+        loadFromShareHash();
+      }).catch(function(e) {
+        console.error('Failed to load pako for shared URL:', e);
+      });
+      return;
+    }
+    const hash = window.location.hash;
+    if (!hash.startsWith('#share=')) return;
+
+    // Parse encoded content and optional &edit=1 flag.
+    // Hash format: #share=<encoded>  or  #share=<encoded>&edit=1
+    const rest = hash.slice('#share='.length);
+    const ampIdx = rest.indexOf('&');
+    const encoded = ampIdx === -1 ? rest : rest.slice(0, ampIdx);
+    const params = ampIdx === -1 ? '' : rest.slice(ampIdx + 1);
+    const isEdit = params.split('&').includes('edit=1');
+
+    if (!encoded) return;
+    try {
+      const decoded = decodeMarkdownFromShare(encoded);
+      markdownEditor.value = decoded;
+      renderMarkdown({ reason: 'document-load', showSkeleton: true });
+      saveCurrentTabState();
+      // Apply the correct view mode: edit=1 → split, default → preview only
+      setViewMode(isEdit ? 'split' : 'preview');
+    } catch (e) {
+      console.error("Failed to load shared content:", e);
+      alert("The shared URL could not be decoded. It may be corrupted or incomplete.");
+    }
+  }
+
+  loadFromShareHash();
+
+  // Full-window drag-and-drop: track nesting level for reliable enter/leave detection
+  let dragDepth = 0;
+
+  document.addEventListener("dragenter", function(e) {
+    if (e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.includes("Files")) {
+      e.preventDefault();
+      dragDepth++;
+      dragOverlay.classList.add("active");
+      dragOverlay.setAttribute("aria-hidden", "false");
+    }
+  }, false);
+
+  document.addEventListener("dragover", function(e) {
+    if (e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.includes("Files")) {
+      e.preventDefault();
+    }
+  }, false);
+
+  document.addEventListener("dragleave", function(e) {
+    if (e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.includes("Files")) {
+      dragDepth--;
+      if (dragDepth <= 0) {
+        dragDepth = 0;
+        dragOverlay.classList.remove("active");
+        dragOverlay.setAttribute("aria-hidden", "true");
+      }
+    }
+  }, false);
+
+  document.addEventListener("drop", function(e) {
+    e.preventDefault();
+    dragDepth = 0;
+    dragOverlay.classList.remove("active");
+    dragOverlay.setAttribute("aria-hidden", "true");
+    handleDrop(e);
+  }, false);
+
+  function handleDrop(e) {
+    const dt = e.dataTransfer;
+    const files = dt.files;
+    if (files.length) {
+      const file = files[0];
+      const isMarkdownFile =
+        file.type === "text/markdown" ||
+        /\.(md|markdown)$/i.test(file.name || "");
+      if (isMarkdownFile) {
+        importMarkdownFile(file);
+      } else {
+        alert("Please upload a Markdown file (.md or .markdown)");
+      }
+    }
+  }
+
+  document.addEventListener("keydown", function (e) {
+    if (document.activeElement === markdownEditor) {
+      const isCmdOrCtrl = e.ctrlKey || e.metaKey;
+      if (isCmdOrCtrl && !e.shiftKey && e.key.toLowerCase() === 'z') {
+        e.preventDefault();
+        executeUndo();
+        return;
+      } else if ((isCmdOrCtrl && e.shiftKey && e.key.toLowerCase() === 'z') || (isCmdOrCtrl && e.key.toLowerCase() === 'y')) {
+        e.preventDefault();
+        executeRedo();
+        return;
+      }
+    }
+
+    if ((e.ctrlKey || e.metaKey) && e.key === "s") {
+      e.preventDefault();
+      exportMd.click();
+    }
+    if ((e.ctrlKey || e.metaKey) && e.key === "c") {
+      const activeEl = document.activeElement;
+      const isTextControl = activeEl && (activeEl.tagName === "TEXTAREA" || activeEl.tagName === "INPUT");
+      const hasSelection = window.getSelection && window.getSelection().toString().trim().length > 0;
+      const editorHasSelection = markdownEditor.selectionStart !== markdownEditor.selectionEnd;
+      if (!isTextControl && !hasSelection && !editorHasSelection) {
+        e.preventDefault();
+        copyMarkdownButton.click();
+      }
+    }
+    // Story 1.2: Only allow sync toggle shortcut when in split view
+    if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === "s") {
+      e.preventDefault();
+      if (currentViewMode === 'split') {
+        toggleSyncScrolling();
+      }
+    }
+    const isDesktop = typeof Neutralino !== 'undefined';
+    // New tab (Ctrl+T on desktop, Alt+Shift+T on web/desktop)
+    if ((isDesktop && (e.ctrlKey || e.metaKey) && e.key === "t") || (e.altKey && e.shiftKey && e.key.toLowerCase() === "t")) {
+      e.preventDefault();
+      newTab();
+    }
+    // Close tab (Ctrl+W on desktop, Alt+Shift+W on web/desktop)
+    if ((isDesktop && (e.ctrlKey || e.metaKey) && e.key === "w") || (e.altKey && e.shiftKey && e.key.toLowerCase() === "w")) {
+      e.preventDefault();
+      closeTab(activeTabId);
+    }
+    // Close Mermaid zoom modal with Escape
+    if (e.key === "Escape") {
+      closeTabMenus();
+      closeMermaidModal();
+    }
+  });
+
+  document.getElementById('tab-reset-btn').addEventListener('click', function() {
+    resetAllTabs();
+  });
+
+  // ========================================
+  // MERMAID DIAGRAM TOOLBAR
+  // ========================================
+
+  /**
+   * Serialises an SVG element to a data URL suitable for use as an image source.
+   * Inline styles and dimensions are preserved so the PNG matches the rendered diagram.
+   */
+  function svgToDataUrl(svgEl) {
+    const clone = svgEl.cloneNode(true);
+    // Ensure explicit width/height so the canvas has the right dimensions
+    const bbox = svgEl.getBoundingClientRect();
+    if (!clone.getAttribute('width'))  clone.setAttribute('width',  Math.round(bbox.width));
+    if (!clone.getAttribute('height')) clone.setAttribute('height', Math.round(bbox.height));
+    const serialized = new XMLSerializer().serializeToString(clone);
+    return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(serialized);
+  }
+
+  /**
+   * Renders an SVG element onto a canvas and resolves with the canvas.
+   */
+  function svgToCanvas(svgEl) {
+    return new Promise((resolve, reject) => {
+      const bbox = svgEl.getBoundingClientRect();
+      const scale = window.devicePixelRatio || 1;
+      const width  = Math.max(Math.round(bbox.width),  1);
+      const height = Math.max(Math.round(bbox.height), 1);
+
+      const canvas = document.createElement('canvas');
+      canvas.width  = width  * scale;
+      canvas.height = height * scale;
+      const ctx = canvas.getContext('2d');
+      ctx.scale(scale, scale);
+
+      // Fill background matching current theme using the CSS variable value
+      const bgColor = getComputedStyle(document.documentElement)
+        .getPropertyValue('--bg-color').trim() || '#ffffff';
+      ctx.fillStyle = bgColor;
+      ctx.fillRect(0, 0, width, height);
+
+      const img = new Image();
+      img.onload  = () => { ctx.drawImage(img, 0, 0, width, height); resolve(canvas); };
+      img.onerror = reject;
+      img.src = svgToDataUrl(svgEl);
+    });
+  }
+
+  /** Downloads the diagram in the given container as a PNG file. */
+  async function downloadMermaidPng(container, btn) {
+    const svgEl = container.querySelector('svg');
+    if (!svgEl) return;
+    const original = btn.innerHTML;
+    btn.innerHTML = '<i class="bi bi-hourglass-split"></i>';
+    try {
+      const canvas = await svgToCanvas(svgEl);
+      canvas.toBlob(blob => {
+        const url = URL.createObjectURL(blob);
+        const a = document.createElement('a');
+        a.href = url;
+        a.download = `diagram-${Date.now()}.png`;
+        a.click();
+        URL.revokeObjectURL(url);
+        btn.innerHTML = '<i class="bi bi-check-lg"></i>';
+        setTimeout(() => { btn.innerHTML = original; }, 1500);
+      }, 'image/png');
+    } catch (e) {
+      console.error('Mermaid PNG export failed:', e);
+      btn.innerHTML = original;
+    }
+  }
+
+  /** Copies the diagram in the given container as a PNG image to the clipboard. */
+  async function copyMermaidImage(container, btn) {
+    const svgEl = container.querySelector('svg');
+    if (!svgEl) return;
+    const original = btn.innerHTML;
+    btn.innerHTML = '<i class="bi bi-hourglass-split"></i>';
+    try {
+      const canvas = await svgToCanvas(svgEl);
+      canvas.toBlob(async blob => {
+        try {
+          await navigator.clipboard.write([
+            new ClipboardItem({ 'image/png': blob })
+          ]);
+          btn.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
+        } catch (clipErr) {
+          console.error('Clipboard write failed:', clipErr);
+          btn.innerHTML = '<i class="bi bi-x-lg"></i>';
+        }
+        setTimeout(() => { btn.innerHTML = original; }, 1800);
+      }, 'image/png');
+    } catch (e) {
+      console.error('Mermaid copy failed:', e);
+      btn.innerHTML = original;
+    }
+  }
+
+  /** Downloads the SVG source of a diagram. */
+  function downloadMermaidSvg(container, btn) {
+    const svgEl = container.querySelector('svg');
+    if (!svgEl) return;
+    const clone = svgEl.cloneNode(true);
+    const serialized = new XMLSerializer().serializeToString(clone);
+    const blob = new Blob([serialized], { type: 'image/svg+xml' });
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = `diagram-${Date.now()}.svg`;
+    a.click();
+    URL.revokeObjectURL(url);
+    const original = btn.innerHTML;
+    btn.innerHTML = '<i class="bi bi-check-lg"></i>';
+    setTimeout(() => { btn.innerHTML = original; }, 1500);
+  }
+
+  // ---- Zoom modal state ----
+  let modalZoomScale = 1;
+  let modalPanX = 0;
+  let modalPanY = 0;
+  let modalIsDragging = false;
+  let modalDragStart = { x: 0, y: 0 };
+  let modalCurrentSvgEl = null;
+
+  const mermaidZoomModal   = document.getElementById('mermaid-zoom-modal');
+  const mermaidModalDiagram = document.getElementById('mermaid-modal-diagram');
+
+  function applyModalTransform() {
+    if (modalCurrentSvgEl) {
+      modalCurrentSvgEl.style.transform =
+        `translate(${modalPanX}px, ${modalPanY}px) scale(${modalZoomScale})`;
+    }
+  }
+
+  function closeMermaidModal() {
+    if (!mermaidZoomModal.classList.contains('active')) return;
+    mermaidZoomModal.classList.remove('active');
+    // PERF-007: Clear elements using textContent
+    mermaidModalDiagram.textContent = '';
+    modalCurrentSvgEl = null;
+    modalZoomScale = 1;
+    modalPanX = 0;
+    modalPanY = 0;
+  }
+
+  /** Opens the zoom modal with the SVG from the given container. */
+  function openMermaidZoomModal(container) {
+    const svgEl = container.querySelector('svg');
+    if (!svgEl) return;
+
+    // PERF-007: Clear elements using textContent
+    mermaidModalDiagram.textContent = '';
+    modalZoomScale = 1;
+    modalPanX = 0;
+    modalPanY = 0;
+
+    const svgClone = svgEl.cloneNode(true);
+    // Remove fixed dimensions so it sizes naturally inside the modal
+    svgClone.removeAttribute('width');
+    svgClone.removeAttribute('height');
+    svgClone.style.width  = 'auto';
+    svgClone.style.height = 'auto';
+    svgClone.style.maxWidth  = '80vw';
+    svgClone.style.maxHeight = '60vh';
+    svgClone.style.transformOrigin = 'center';
+    mermaidModalDiagram.appendChild(svgClone);
+    modalCurrentSvgEl = svgClone;
+
+    mermaidZoomModal.classList.add('active');
+  }
+
+  // Modal close button
+  document.getElementById('mermaid-modal-close').addEventListener('click', closeMermaidModal);
+  // Click backdrop to close
+  mermaidZoomModal.addEventListener('click', function(e) {
+    if (e.target === mermaidZoomModal) closeMermaidModal();
+  });
+
+  // Zoom controls
+  document.getElementById('mermaid-modal-zoom-in').addEventListener('click', () => {
+    modalZoomScale = Math.min(modalZoomScale + 0.25, 10);
+    applyModalTransform();
+  });
+  document.getElementById('mermaid-modal-zoom-out').addEventListener('click', () => {
+    modalZoomScale = Math.max(modalZoomScale - 0.25, 0.1);
+    applyModalTransform();
+  });
+  document.getElementById('mermaid-modal-zoom-reset').addEventListener('click', () => {
+    modalZoomScale = 1; modalPanX = 0; modalPanY = 0;
+    applyModalTransform();
+  });
+
+  // Mouse-wheel zoom inside modal
+  mermaidModalDiagram.addEventListener('wheel', function(e) {
+    e.preventDefault();
+    const delta = e.deltaY < 0 ? 0.15 : -0.15;
+    modalZoomScale = Math.min(Math.max(modalZoomScale + delta, 0.1), 10);
+    applyModalTransform();
+  }, { passive: false });
+
+  // Drag to pan inside modal
+  mermaidModalDiagram.addEventListener('mousedown', function(e) {
+    modalIsDragging = true;
+    modalDragStart = { x: e.clientX - modalPanX, y: e.clientY - modalPanY };
+    mermaidModalDiagram.classList.add('dragging');
+  });
+  document.addEventListener('mousemove', function(e) {
+    if (!modalIsDragging) return;
+    modalPanX = e.clientX - modalDragStart.x;
+    modalPanY = e.clientY - modalDragStart.y;
+    applyModalTransform();
+  });
+  document.addEventListener('mouseup', function() {
+    if (modalIsDragging) {
+      modalIsDragging = false;
+      mermaidModalDiagram.classList.remove('dragging');
+    }
+  });
+
+  // Modal download buttons (operate on the currently displayed SVG)
+  document.getElementById('mermaid-modal-download-png').addEventListener('click', async function() {
+    if (!modalCurrentSvgEl) return;
+    const btn = this;
+    const original = btn.innerHTML;
+    btn.innerHTML = '<i class="bi bi-hourglass-split"></i>';
+    try {
+      // Use the original SVG (with dimensions) for proper PNG rendering
+      const canvas = await svgToCanvas(modalCurrentSvgEl);
+      canvas.toBlob(blob => {
+        const url = URL.createObjectURL(blob);
+        const a = document.createElement('a');
+        a.href = url; a.download = `diagram-${Date.now()}.png`; a.click();
+        URL.revokeObjectURL(url);
+        btn.innerHTML = '<i class="bi bi-check-lg"></i>';
+        setTimeout(() => { btn.innerHTML = original; }, 1500);
+      }, 'image/png');
+    } catch (e) {
+      console.error('Modal PNG export failed:', e);
+      btn.innerHTML = original;
+    }
+  });
+
+  document.getElementById('mermaid-modal-copy').addEventListener('click', async function() {
+    if (!modalCurrentSvgEl) return;
+    const btn = this;
+    const original = btn.innerHTML;
+    btn.innerHTML = '<i class="bi bi-hourglass-split"></i>';
+    try {
+      const canvas = await svgToCanvas(modalCurrentSvgEl);
+      canvas.toBlob(async blob => {
+        try {
+          await navigator.clipboard.write([
+            new ClipboardItem({ 'image/png': blob })
+          ]);
+          btn.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
+        } catch (clipErr) {
+          console.error('Clipboard write failed:', clipErr);
+          btn.innerHTML = '<i class="bi bi-x-lg"></i>';
+        }
+        setTimeout(() => { btn.innerHTML = original; }, 1800);
+      }, 'image/png');
+    } catch (e) {
+      console.error('Modal copy failed:', e);
+      btn.innerHTML = original;
+    }
+  });
+
+  document.getElementById('mermaid-modal-download-svg').addEventListener('click', function() {
+    if (!modalCurrentSvgEl) return;
+    const serialized = new XMLSerializer().serializeToString(modalCurrentSvgEl);
+    const blob = new Blob([serialized], { type: 'image/svg+xml' });
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url; a.download = `diagram-${Date.now()}.svg`; a.click();
+    URL.revokeObjectURL(url);
+  });
+
+  /**
+   * Adds the hover toolbar to every rendered Mermaid container.
+   * Safe to call multiple times – existing toolbars are not duplicated.
+   */
+  function addMermaidToolbars() {
+    markdownPreview.querySelectorAll('.mermaid-container').forEach(container => {
+      if (container.querySelector('.mermaid-toolbar')) return; // already added
+      const svgEl = container.querySelector('svg');
+      if (!svgEl) return; // diagram not yet rendered
+
+      const toolbar = document.createElement('div');
+      toolbar.className = 'mermaid-toolbar';
+      toolbar.setAttribute('aria-label', 'Diagram actions');
+
+      const btnZoom = document.createElement('button');
+      btnZoom.className = 'mermaid-toolbar-btn';
+      btnZoom.title = 'Zoom diagram';
+      btnZoom.setAttribute('aria-label', 'Zoom diagram');
+      btnZoom.innerHTML = '<i class="bi bi-arrows-fullscreen"></i>';
+      btnZoom.addEventListener('click', () => openMermaidZoomModal(container));
+
+      const btnPng = document.createElement('button');
+      btnPng.className = 'mermaid-toolbar-btn';
+      btnPng.title = 'Download PNG';
+      btnPng.setAttribute('aria-label', 'Download PNG');
+      btnPng.innerHTML = '<i class="bi bi-file-image"></i> PNG';
+      btnPng.addEventListener('click', () => downloadMermaidPng(container, btnPng));
+
+      const btnCopy = document.createElement('button');
+      btnCopy.className = 'mermaid-toolbar-btn';
+      btnCopy.title = 'Copy image to clipboard';
+      btnCopy.setAttribute('aria-label', 'Copy image to clipboard');
+      btnCopy.innerHTML = '<i class="bi bi-clipboard-image"></i> Copy';
+      btnCopy.addEventListener('click', () => copyMermaidImage(container, btnCopy));
+
+      const btnSvg = document.createElement('button');
+      btnSvg.className = 'mermaid-toolbar-btn';
+      btnSvg.title = 'Download SVG';
+      btnSvg.setAttribute('aria-label', 'Download SVG');
+      btnSvg.innerHTML = '<i class="bi bi-filetype-svg"></i> SVG';
+      btnSvg.addEventListener('click', () => downloadMermaidSvg(container, btnSvg));
+
+      toolbar.appendChild(btnZoom);
+      toolbar.appendChild(btnCopy);
+      toolbar.appendChild(btnPng);
+      toolbar.appendChild(btnSvg);
+      container.appendChild(toolbar);
+    });
+  }
+
+  // ==========================================================================
+  // Aegis SEO agency Multilingual & Internationalization (i18n) engine
+  // ==========================================================================
+  const I18N_DICTS = {
+    en: {
+      title: "Markdown Viewer",
+      syncOff: "Sync Off",
+      syncOn: "Sync On",
+      import: "Import",
+      importFile: "From files",
+      importGithub: "From GitHub",
+      export: "Export",
+      exportMd: "Markdown (.md)",
+      exportHtml: "HTML",
+      exportPdf: "PDF",
+      copy: "Copy",
+      copied: "Copied!",
+      share: "Share",
+      reset: "Reset",
+      editor: "Editor",
+      split: "Split",
+      preview: "Preview",
+      minRead: "Min Read",
+      words: "Words",
+      chars: "Chars",
+      switchRtl: "Switch to RTL",
+      switchLtr: "Switch to LTR",
+      darkMode: "Dark Mode",
+      lightMode: "Light Mode",
+      helpTitle: "Markdown Viewer Help",
+      aboutTitle: "About Markdown",
+      shareTitle: "Share Document",
+      renameTitle: "Rename file",
+      insertLink: "Insert link",
+      insertRef: "Insert reference",
+      insertImg: "Insert image",
+      insertTable: "Insert table",
+      findReplace: "Find & Replace",
+      placeholder: "Type your markdown here...",
+      loadingEmojis: "Loading emojis...",
+      loadingFiles: "Fetching file tree..."
+    },
+    zh: {
+      title: "Markdown 阅读器",
+      syncOff: "同步关闭",
+      syncOn: "同步开启",
+      import: "导入",
+      importFile: "本地文件导入",
+      importGithub: "从 GitHub 导入",
+      export: "导出",
+      exportMd: "导出 Markdown (.md)",
+      exportHtml: "导出 HTML",
+      exportPdf: "导出 PDF",
+      copy: "复制",
+      copied: "已复制!",
+      share: "分享",
+      reset: "重置",
+      editor: "编辑器",
+      split: "分栏预览",
+      preview: "纯预览",
+      minRead: "分钟阅读",
+      words: "字数",
+      chars: "字符数",
+      switchRtl: "切换为右至左布局",
+      switchLtr: "切换为左至右布局",
+      darkMode: "深色模式",
+      lightMode: "浅色模式",
+      helpTitle: "Markdown 阅读器帮助说明",
+      aboutTitle: "关于 Markdown 阅读器",
+      shareTitle: "分享当前文档",
+      renameTitle: "重命名文件",
+      insertLink: "插入超链接",
+      insertRef: "插入脚注引用",
+      insertImg: "插入图片",
+      insertTable: "插入表格",
+      findReplace: "查找与替换",
+      placeholder: "在此输入您的 Markdown 文本...",
+      loadingEmojis: "正在加载表情...",
+      loadingFiles: "正在获取文件树..."
+    },
+    ja: {
+      title: "Markdown ビューア",
+      syncOff: "同期オフ",
+      syncOn: "同期オン",
+      import: "インポート",
+      importFile: "ファイルから",
+      importGithub: "GitHub から",
+      export: "エクスポート",
+      exportMd: "Markdown (.md)",
+      exportHtml: "HTML",
+      exportPdf: "PDF",
+      copy: "コピー",
+      copied: "コピー完了!",
+      share: "共有",
+      reset: "リセット",
+      editor: "エディタ",
+      split: "分割表示",
+      preview: "プレビュー",
+      minRead: "分 読了",
+      words: "単語数",
+      chars: "文字数",
+      switchRtl: "RTL表示に切替",
+      switchLtr: "LTR表示に切替",
+      darkMode: "ダークモード",
+      lightMode: "ライトモード",
+      helpTitle: "Markdown ビューア ヘルプ",
+      aboutTitle: "Markdown について",
+      shareTitle: "ドキュメントの共有",
+      renameTitle: "ファイル名の変更",
+      insertLink: "リンクの挿入",
+      insertRef: "参照の挿入",
+      insertImg: "画像の挿入",
+      insertTable: "テーブルの挿入",
+      findReplace: "検索と置換",
+      placeholder: "ここにMarkdownを入力してください...",
+      loadingEmojis: "絵文字を読み込んでいます...",
+      loadingFiles: "ファイルツリーを取得しています..."
+    },
+    ko: {
+      title: "마크다운 뷰어",
+      syncOff: "동기화 끄기",
+      syncOn: "동기화 켜기",
+      import: "가져오기",
+      importFile: "로컬 파일에서",
+      importGithub: "GitHub에서",
+      export: "내보내기",
+      exportMd: "마크다운 (.md)",
+      exportHtml: "HTML로 내보내기",
+      exportPdf: "PDF로 내보내기",
+      copy: "복사",
+      copied: "복사됨!",
+      share: "공유",
+      reset: "초기화",
+      editor: "편집기",
+      split: "분할 보기",
+      preview: "미리보기",
+      minRead: "분 읽기",
+      words: "단어 수",
+      chars: "글자 수",
+      switchRtl: "우측 정렬로 전환",
+      switchLtr: "좌측 정렬로 전환",
+      darkMode: "다크 모드",
+      lightMode: "라이트 모드",
+      helpTitle: "마크다운 뷰어 도움말",
+      aboutTitle: "마크다운 정보",
+      shareTitle: "문서 공유",
+      renameTitle: "파일 이름 바꾸기",
+      insertLink: "링크 삽입",
+      insertRef: "참조 삽입",
+      insertImg: "이미지 삽입",
+      insertTable: "표 삽입",
+      findReplace: "찾기 및 바꾸기",
+      placeholder: "여기에 마크다운 내용을 입력하세요...",
+      loadingEmojis: "이모지 로딩 중...",
+      loadingFiles: "파일 트리 가져오는 중..."
+    },
+    pt: {
+      title: "Visualizador de Markdown",
+      syncOff: "Desativar Sincronia",
+      syncOn: "Ativar Sincronia",
+      import: "Importar",
+      importFile: "De arquivos",
+      importGithub: "Do GitHub",
+      export: "Exportar",
+      exportMd: "Markdown (.md)",
+      exportHtml: "HTML",
+      exportPdf: "PDF",
+      copy: "Copiar",
+      copied: "Copiado!",
+      share: "Compartilhar",
+      reset: "Redefinir",
+      editor: "Editor",
+      split: "Dividido",
+      preview: "Visualizar",
+      minRead: "Min de leitura",
+      words: "Palavras",
+      chars: "Caracteres",
+      switchRtl: "Mudar para RTL",
+      switchLtr: "Mudar para LTR",
+      darkMode: "Modo Escuro",
+      lightMode: "Modo Claro",
+      helpTitle: "Ajuda do Visualizador de Markdown",
+      aboutTitle: "Sobre o Markdown",
+      shareTitle: "Compartilhar Documento",
+      renameTitle: "Renomear arquivo",
+      insertLink: "Inserir link",
+      insertRef: "Inserir referência",
+      insertImg: "Inserir imagem",
+      insertTable: "Inserir tabela",
+      findReplace: "Localizar & Substituir",
+      placeholder: "Digite seu markdown aqui...",
+      loadingEmojis: "Carregando emojis...",
+      loadingFiles: "Buscando árvore de arquivos..."
+    },
+    es: {
+      title: "Visualizador de Markdown",
+      syncOff: "Sincronización desactivada",
+      syncOn: "Sincronización activada",
+      import: "Importar",
+      importFile: "Desde archivos",
+      importGithub: "Desde GitHub",
+      export: "Exportar",
+      exportMd: "Markdown (.md)",
+      exportHtml: "HTML",
+      exportPdf: "PDF",
+      copy: "Copiar",
+      copied: "¡Copiado!",
+      share: "Compartir",
+      reset: "Restablecer",
+      editor: "Editor",
+      split: "Dividido",
+      preview: "Previsualizar",
+      minRead: "Min de lectura",
+      words: "Palabras",
+      chars: "Caracteres",
+      switchRtl: "Cambiar a RTL",
+      switchLtr: "Cambiar a LTR",
+      darkMode: "Modo oscuro",
+      lightMode: "Modo claro",
+      helpTitle: "Ayuda del Visualizador de Markdown",
+      aboutTitle: "Acerca de Markdown",
+      shareTitle: "Compartir documento",
+      renameTitle: "Renombrar archivo",
+      insertLink: "Insertar enlace",
+      insertRef: "Insertar referencia",
+      insertImg: "Insertar imagen",
+      insertTable: "Insertar tabla",
+      findReplace: "Buscar y reemplazar",
+      placeholder: "Escribe tu markdown aquí...",
+      loadingEmojis: "Cargando emojis...",
+      loadingFiles: "Obteniendo árbol de archivos..."
+    },
+    fr: {
+      title: "Lecteur Markdown",
+      syncOff: "Désactiver la synchro",
+      syncOn: "Activer la synchro",
+      import: "Importer",
+      importFile: "Depuis des fichiers",
+      importGithub: "Depuis GitHub",
+      export: "Exporter",
+      exportMd: "Markdown (.md)",
+      exportHtml: "HTML",
+      exportPdf: "PDF",
+      copy: "Copier",
+      copied: "Copié !",
+      share: "Partager",
+      reset: "Réinitialiser",
+      editor: "Éditeur",
+      split: "Divisé",
+      preview: "Aperçu",
+      minRead: "Min de lecture",
+      words: "Mots",
+      chars: "Caractères",
+      switchRtl: "Passer à RTL",
+      switchLtr: "Passer à LTR",
+      darkMode: "Mode sombre",
+      lightMode: "Mode clair",
+      helpTitle: "Aide du Lecteur Markdown",
+      aboutTitle: "À propos de Markdown",
+      shareTitle: "Partager le document",
+      renameTitle: "Renommer le fichier",
+      insertLink: "Insérer un lien",
+      insertRef: "Insérer une référence",
+      insertImg: "Insérer une image",
+      insertTable: "Insérer un tableau",
+      findReplace: "Rechercher & remplacer",
+      placeholder: "Saisissez votre markdown ici...",
+      loadingEmojis: "Chargement des émojis...",
+      loadingFiles: "Récupération de l'arborescence des fichiers..."
+    },
+    de: {
+      title: "Markdown Viewer",
+      syncOff: "Synchronisierung aus",
+      syncOn: "Synchronisierung an",
+      import: "Importieren",
+      importFile: "Aus Dateien",
+      importGithub: "Aus GitHub",
+      export: "Exportieren",
+      exportMd: "Markdown (.md)",
+      exportHtml: "HTML",
+      exportPdf: "PDF",
+      copy: "Kopieren",
+      copied: "Kopiert!",
+      share: "Teilen",
+      reset: "Zurücksetzen",
+      editor: "Editor",
+      split: "Geteilt",
+      preview: "Vorschau",
+      minRead: "Min. Lesezeit",
+      words: "Wörter",
+      chars: "Zeichen",
+      switchRtl: "Zu RTL wechseln",
+      switchLtr: "Zu LTR wechseln",
+      darkMode: "Dunkelmodus",
+      lightMode: "Heller Modus",
+      helpTitle: "Markdown Viewer Hilfe",
+      aboutTitle: "Über Markdown",
+      shareTitle: "Dokument teilen",
+      renameTitle: "Datei umbenennen",
+      insertLink: "Link einfügen",
+      insertRef: "Referenz einfügen",
+      insertImg: "Bild einfügen",
+      insertTable: "Tabelle einfügen",
+      findReplace: "Suchen & Ersetzen",
+      placeholder: "Geben Sie hier Ihr Markdown ein...",
+      loadingEmojis: "Emojis werden geladen...",
+      loadingFiles: "Dateibaum wird abgerufen..."
+    },
+    ru: {
+      title: "Просмотрщик Markdown",
+      syncOff: "Синхронизация выкл",
+      syncOn: "Синхронизация вкл",
+      import: "Импорт",
+      importFile: "Из файлов",
+      importGithub: "Из GitHub",
+      export: "Экспорт",
+      exportMd: "Markdown (.md)",
+      exportHtml: "HTML",
+      exportPdf: "PDF",
+      copy: "Копировать",
+      copied: "Скопировано!",
+      share: "Поделиться",
+      reset: "Сбросить",
+      editor: "Редактор",
+      split: "Разделение",
+      preview: "Предпросмотр",
+      minRead: "Мин чтения",
+      words: "Слов",
+      chars: "Символов",
+      switchRtl: "Переключить на RTL",
+      switchLtr: "Переключить на LTR",
+      darkMode: "Темная тема",
+      lightMode: "Светлая тема",
+      helpTitle: "Справка Markdown Viewer",
+      aboutTitle: "О Markdown",
+      shareTitle: "Поделиться документом",
+      renameTitle: "Переименовать файл",
+      insertLink: "Вставить ссылку",
+      insertRef: "Вставить сноску",
+      insertImg: "Вставить изображение",
+      insertTable: "Вставить таблицу",
+      findReplace: "Найти и заменить",
+      placeholder: "Введите здесь ваш markdown...",
+      loadingEmojis: "Загрузка эмодзи...",
+      loadingFiles: "Получение списка файлов..."
+    },
+    it: {
+      title: "Visualizzatore Markdown",
+      syncOff: "Sincronia disattivata",
+      syncOn: "Sincronia attivata",
+      import: "Importa",
+      importFile: "Da file",
+      importGithub: "Da GitHub",
+      export: "Esporta",
+      exportMd: "Markdown (.md)",
+      exportHtml: "HTML",
+      exportPdf: "PDF",
+      copy: "Copia",
+      copied: "Copiato!",
+      share: "Condividi",
+      reset: "Ripristina",
+      editor: "Editor",
+      split: "Diviso",
+      preview: "Anteprima",
+      minRead: "Min di lettura",
+      words: "Parole",
+      chars: "Caratteri",
+      switchRtl: "Passa a RTL",
+      switchLtr: "Passa a LTR",
+      darkMode: "Tema scuro",
+      lightMode: "Tema chiaro",
+      helpTitle: "Aiuto di Visualizzatore Markdown",
+      aboutTitle: "Informazioni su Markdown",
+      shareTitle: "Condividi documento",
+      renameTitle: "Rinomina file",
+      insertLink: "Inserisci link",
+      insertRef: "Inserisci riferimento",
+      insertImg: "Inserisci immagine",
+      insertTable: "Inserisci tabella",
+      findReplace: "Trova e sostituisci",
+      placeholder: "Scrivi il tuo markdown qui...",
+      loadingEmojis: "Caricamento emoji...",
+      loadingFiles: "Recupero albero dei file..."
+    },
+    tr: {
+      title: "Markdown Görüntüleyici",
+      syncOff: "Senkronizasyonu Kapat",
+      syncOn: "Senkronizasyonu Aç",
+      import: "İçe Aktar",
+      importFile: "Dosyalardan",
+      importGithub: "GitHub'dan",
+      export: "Dışa Aktar",
+      exportMd: "Markdown (.md)",
+      exportHtml: "HTML",
+      exportPdf: "PDF",
+      copy: "Kopyala",
+      copied: "Kopyalandı!",
+      share: "Paylaş",
+      reset: "Sıfırla",
+      editor: "Editör",
+      split: "Bölünmüş",
+      preview: "Önizleme",
+      minRead: "Dk Okuma",
+      words: "Kelime",
+      chars: "Karakter",
+      switchRtl: "RTL'ye Geç",
+      switchLtr: "LTR'ye Geç",
+      darkMode: "Karanlık Mod",
+      lightMode: "Aydınlık Mod",
+      helpTitle: "Markdown Görüntüleyici Yardımı",
+      aboutTitle: "Markdown Hakkında",
+      shareTitle: "Belgeyi Paylaş",
+      renameTitle: "Dosyayı yeniden adlandır",
+      insertLink: "Bağlantı ekle",
+      insertRef: "Referans ekle",
+      insertImg: "Resim ekle",
+      insertTable: "Tablo ekle",
+      findReplace: "Bul ve Değiştir",
+      placeholder: "Markdown'ınızı buraya yazın...",
+      loadingEmojis: "Emoji'ler yükleniyor...",
+      loadingFiles: "Dosya ağacı alınıyor..."
+    },
+    pl: {
+      title: "Czytnik Markdown",
+      syncOff: "Wyłącz synchronizację",
+      syncOn: "Włącz synchronizację",
+      import: "Importuj",
+      importFile: "Z plików",
+      importGithub: "Z GitHub",
+      export: "Eksportuj",
+      exportMd: "Markdown (.md)",
+      exportHtml: "HTML",
+      exportPdf: "PDF",
+      copy: "Kopiuj",
+      copied: "Skopiowano!",
+      share: "Udostępnij",
+      reset: "Resetuj",
+      editor: "Edytor",
+      split: "Podzielony",
+      preview: "Podgląd",
+      minRead: "Min. czytania",
+      words: "Słowa",
+      chars: "Znaki",
+      switchRtl: "Przełącz na RTL",
+      switchLtr: "Przełącz na LTR",
+      darkMode: "Tryb ciemny",
+      lightMode: "Tryb jasny",
+      helpTitle: "Pomoc Czytnika Markdown",
+      aboutTitle: "O Markdown",
+      shareTitle: "Udostępnij dokument",
+      renameTitle: "Zmień nazwę pliku",
+      insertLink: "Wstaw link",
+      insertRef: "Wstaw odnośnik",
+      insertImg: "Wstaw obraz",
+      insertTable: "Wstaw tabelę",
+      findReplace: "Znajdź i zamień",
+      placeholder: "Wpisz tutaj swój markdown...",
+      loadingEmojis: "Ładowanie emoji...",
+      loadingFiles: "Pobieranie drzewa plików..."
+    },
+    tw: {
+      title: "Markdown 閱讀器",
+      syncOff: "同步關閉",
+      syncOn: "同步開啟",
+      import: "匯入",
+      importFile: "本地檔案匯入",
+      importGithub: "從 GitHub 匯入",
+      export: "匯出",
+      exportMd: "匯出 Markdown (.md)",
+      exportHtml: "匯出 HTML",
+      exportPdf: "匯出 PDF",
+      copy: "複製",
+      copied: "已複製!",
+      share: "分享",
+      reset: "重置",
+      editor: "編輯器",
+      split: "分欄預覽",
+      preview: "純預覽",
+      minRead: "分鐘閱讀",
+      words: "字數",
+      chars: "字元數",
+      switchRtl: "切換為右至左佈局",
+      switchLtr: "切換為左至右佈局",
+      darkMode: "深色模式",
+      lightMode: "淺色模式",
+      helpTitle: "Markdown 閱讀器說明",
+      aboutTitle: "關於 Markdown 閱讀器",
+      shareTitle: "分享當前文件",
+      renameTitle: "重新命名檔案",
+      insertLink: "插入超連結",
+      insertRef: "插入腳註引用",
+      insertImg: "插入圖片",
+      insertTable: "插入表格",
+      findReplace: "尋找與取代",
+      placeholder: "在此輸入您的 Markdown 文本...",
+      loadingEmojis: "正在載入表情...",
+      loadingFiles: "正在獲取檔案樹..."
+    },
+    uk: {
+      title: "Переглядач Markdown",
+      syncOff: "Вимкнути синхронізацію",
+      syncOn: "Увімкнути синхронізацію",
+      import: "Імпорт",
+      importFile: "З файлів",
+      importGithub: "З GitHub",
+      export: "Експорт",
+      exportMd: "Markdown (.md)",
+      exportHtml: "HTML",
+      exportPdf: "PDF",
+      copy: "Копіювати",
+      copied: "Скопійовано!",
+      share: "Поділитися",
+      reset: "Скинути",
+      editor: "Редактор",
+      split: "Розділений",
+      preview: "Перегляд",
+      minRead: "Хв читання",
+      words: "Слів",
+      chars: "Символів",
+      switchRtl: "Перемкнути на RTL",
+      switchLtr: "Перемкнути на LTR",
+      darkMode: "Темний режим",
+      lightMode: "Світлий режим",
+      helpTitle: "Довідка Markdown Viewer",
+      aboutTitle: "Про Markdown",
+      shareTitle: "Поділитися документом",
+      renameTitle: "Перейменувати файл",
+      insertLink: "Вставити посилання",
+      insertRef: "Вставити витяг",
+      insertImg: "Вставити зображення",
+      insertTable: "Вставити таблицю",
+      findReplace: "Знайти та замінити",
+      placeholder: "Введіть ваш markdown тут...",
+      loadingEmojis: "Завантаження емодзі...",
+      loadingFiles: "Отримання структури файлів..."
+    }
+  };
+
+  let activeLang = 'en';
+
+  function applyTranslations(lang) {
+    activeLang = lang;
+    document.documentElement.setAttribute('lang', lang === 'zh' ? 'zh-Hans' : (lang === 'tw' ? 'zh-Hant' : lang));
+    const dict = I18N_DICTS[lang] || I18N_DICTS.en;
+
+    // Update main logo and header elements
+    const logoEl = document.querySelector('.app-header h1');
+    if (logoEl) logoEl.textContent = dict.title;
+
+    // Update dynamic current language labels in drop menus
+    const labelEl = document.getElementById('current-lang-label');
+    if (labelEl) {
+      const flags = {
+        en: "🇺🇸 English",
+        zh: "🇨🇳 简体中文",
+        ja: "🇯🇵 日本語",
+        ko: "🇰🇷 한국어",
+        pt: "🇧🇷 Português (Brasil)",
+        es: "🇪🇸 Español",
+        fr: "🇫🇷 Français",
+        de: "🇩🇪 Deutsch",
+        ru: "🇷🇺 Русский",
+        it: "🇮🇹 Italiano",
+        tr: "🇹🇷 Türkçe",
+        pl: "🇵🇱 Polski",
+        tw: "🇹🇼 繁體中文",
+        uk: "🇺🇦 Українська"
+      };
+      labelEl.textContent = flags[lang];
+    }
+    const mobileLabelEl = document.getElementById('mobile-current-lang-label');
+    if (mobileLabelEl) {
+      const flags = {
+        en: "us English",
+        zh: "CN 简体中文",
+        ja: "JP 日本語",
+        ko: "KR 한국어",
+        pt: "BR Português (Brasil)",
+        es: "ES Español",
+        fr: "FR Français",
+        de: "DE Deutsch",
+        ru: "RU Русский",
+        it: "IT Italiano",
+        tr: "TR Türkçe",
+        pl: "PL Polski",
+        tw: "TW 繁體中文",
+        uk: "UK Українська"
+      };
+      mobileLabelEl.textContent = flags[lang];
+    }
+
+    // Translate buttons with text content
+    const toggleSyncEl = document.getElementById('toggle-sync');
+    if (toggleSyncEl) {
+      const isSyncActive = toggleSyncEl.classList.contains('sync-active');
+      const textSpan = toggleSyncEl.querySelector('.btn-text');
+      if (textSpan) textSpan.textContent = isSyncActive ? dict.syncOff : dict.syncOn;
+    }
+    const mobileToggleSyncEl = document.getElementById('mobile-toggle-sync');
+    if (mobileToggleSyncEl) {
+      const isSyncActive = mobileToggleSyncEl.classList.contains('sync-active');
+      mobileToggleSyncEl.innerHTML = `<i class="bi bi-link"></i> ${isSyncActive ? dict.syncOff : dict.syncOn}`;
+    }
+
+    // Import buttons
+    const importDropEl = document.getElementById('importDropdown');
+    if (importDropEl) {
+      const importText = importDropEl.querySelector('.btn-text');
+      if (importText) importText.textContent = dict.import;
+    }
+    const importFileEl = document.getElementById('import-from-file');
+    if (importFileEl) importFileEl.innerHTML = `<i class="bi bi-upload me-2"></i>${dict.importFile}`;
+    const importGithubEl = document.getElementById('import-from-github');
+    if (importGithubEl) importGithubEl.innerHTML = `<i class="bi bi-github me-2"></i>${dict.importGithub}`;
+
+    const mImportFileEl = document.getElementById('mobile-import-button');
+    if (mImportFileEl) mImportFileEl.innerHTML = `<i class="bi bi-upload me-2"></i>${dict.importFile}`;
+    const mImportGithubEl = document.getElementById('mobile-import-github-button');
+    if (mImportGithubEl) mImportGithubEl.innerHTML = `<i class="bi bi-github me-2"></i>${dict.importGithub}`;
+
+    // Export buttons
+    const exportDropEl = document.getElementById('exportDropdown');
+    if (exportDropEl) {
+      const exportText = exportDropEl.querySelector('.btn-text');
+      if (exportText) exportText.textContent = dict.export;
+    }
+    const exportMdEl = document.getElementById('export-md');
+    if (exportMdEl) exportMdEl.innerHTML = `<i class="bi bi-file-earmark-text me-2"></i>${dict.exportMd}`;
+    const exportHtmlEl = document.getElementById('export-html');
+    if (exportHtmlEl) exportHtmlEl.innerHTML = `<i class="bi bi-file-earmark-code me-2"></i>${dict.exportHtml}`;
+    const exportPdfEl = document.getElementById('export-pdf');
+    if (exportPdfEl) exportPdfEl.innerHTML = `<i class="bi bi-file-earmark-pdf me-2"></i>${dict.exportPdf}`;
+
+    const mExportMdEl = document.getElementById('mobile-export-md');
+    if (mExportMdEl) mExportMdEl.innerHTML = `<i class="bi bi-file-earmark-text me-2"></i>${dict.exportMd}`;
+    const mExportHtmlEl = document.getElementById('mobile-export-html');
+    if (mExportHtmlEl) mExportHtmlEl.innerHTML = `<i class="bi bi-file-earmark-code me-2"></i>${dict.exportHtml}`;
+    const mExportPdfEl = document.getElementById('mobile-export-pdf');
+    if (mExportPdfEl) mExportPdfEl.innerHTML = `<i class="bi bi-file-earmark-pdf me-2"></i>${dict.exportPdf}`;
+
+    // Copy / Share
+    if (copyMarkdownButton) {
+      const copyButtonText = copyMarkdownButton.querySelector('.btn-text');
+      if (copyButtonText) copyButtonText.textContent = dict.copy;
+    }
+    const mCopyBtn = document.getElementById('mobile-copy-markdown');
+    if (mCopyBtn) mCopyBtn.innerHTML = `<i class="bi bi-clipboard me-2"></i>${dict.copy}`;
+
+    if (shareButton) {
+      const shareButtonText = shareButton.querySelector('.btn-text');
+      if (shareButtonText) shareButtonText.textContent = dict.share;
+    }
+    const mShareBtn = document.getElementById('mobile-share-button');
+    if (mShareBtn) mShareBtn.innerHTML = `<i class="bi bi-share me-2"></i>${dict.share}`;
+
+    // Document Reset
+    const tabResetBtn = document.getElementById('tab-reset-btn');
+    if (tabResetBtn) tabResetBtn.innerHTML = `<i class="bi bi-arrow-counterclockwise"></i> ${dict.reset}`;
+    const mTabResetBtn = document.getElementById('mobile-tab-reset-btn');
+    if (mTabResetBtn) mTabResetBtn.innerHTML = `<i class="bi bi-arrow-counterclockwise"></i> ${dict.reset} all files`;
+
+    // View toggle buttons title tooltips
+    document.querySelectorAll('[data-view-mode="editor"]').forEach(b => b.title = dict.editor);
+    document.querySelectorAll('[data-view-mode="split"]').forEach(b => b.title = dict.split);
+    document.querySelectorAll('[data-view-mode="preview"]').forEach(b => b.title = dict.preview);
+    document.querySelectorAll('.mobile-view-mode-btn[data-mode="editor"] span').forEach(s => s.textContent = dict.editor);
+    document.querySelectorAll('.mobile-view-mode-btn[data-mode="split"] span').forEach(s => s.textContent = dict.split);
+    document.querySelectorAll('.mobile-view-mode-btn[data-mode="preview"] span').forEach(s => s.textContent = dict.preview);
+
+    // Direction Toggle
+    const dirToggle = document.getElementById('direction-toggle');
+    if (dirToggle) {
+      const isRtl = document.body.style.direction === 'rtl';
+      dirToggle.title = isRtl ? dict.switchLtr : dict.switchRtl;
+    }
+
+    // Modal Titles
+    const modalHelpTitle = document.getElementById('help-modal-title');
+    if (modalHelpTitle) modalHelpTitle.textContent = dict.helpTitle;
+    const modalAboutTitle = document.getElementById('about-modal-title');
+    if (modalAboutTitle) modalAboutTitle.textContent = dict.aboutTitle;
+    const modalShareTitle = document.getElementById('share-modal-title');
+    if (modalShareTitle) modalShareTitle.textContent = dict.shareTitle;
+    const modalRenameTitle = document.getElementById('rename-modal-title');
+    if (modalRenameTitle) modalRenameTitle.textContent = dict.renameTitle;
+    const modalLinkTitle = document.getElementById('link-modal-title');
+    if (modalLinkTitle) modalLinkTitle.textContent = dict.insertLink;
+    const modalRefTitle = document.getElementById('reference-modal-title');
+    if (modalRefTitle) modalRefTitle.textContent = dict.insertRef;
+    const modalImgTitle = document.getElementById('image-modal-title');
+    if (modalImgTitle) modalImgTitle.textContent = dict.insertImg;
+    const modalTableTitle = document.getElementById('table-modal-title');
+    if (modalTableTitle) modalTableTitle.textContent = dict.insertTable;
+    const modalFindTitle = document.getElementById('find-replace-title');
+    if (modalFindTitle) modalFindTitle.textContent = dict.findReplace;
+
+    // Theme titles
+    const mThemeToggle = document.getElementById('mobile-theme-toggle');
+    if (mThemeToggle) {
+      const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
+      mThemeToggle.innerHTML = `<i class="bi bi-${currentTheme === 'dark' ? 'sun' : 'moon'} me-2"></i> ${currentTheme === 'dark' ? dict.lightMode : dict.darkMode}`;
+    }
+
+    // Stats Labels
+    const minReadEl = document.getElementById('lbl-min-read');
+    if (minReadEl) minReadEl.textContent = dict.minRead;
+    const wordsEl = document.getElementById('lbl-words');
+    if (wordsEl) wordsEl.textContent = dict.words;
+    const charsEl = document.getElementById('lbl-chars');
+    if (charsEl) charsEl.textContent = dict.chars;
+
+    const mMinReadEl = document.getElementById('lbl-mobile-min-read');
+    if (mMinReadEl) mMinReadEl.textContent = dict.minRead;
+    const mWordsEl = document.getElementById('lbl-mobile-words');
+    if (mWordsEl) mWordsEl.textContent = dict.words;
+    const mCharsEl = document.getElementById('lbl-mobile-chars');
+    if (mCharsEl) mCharsEl.textContent = dict.chars;
+
+    // Placeholder
+    if (markdownEditor) {
+      markdownEditor.placeholder = dict.placeholder;
+    }
+
+    // Trigger state tracking update
+    updateDocumentStats();
+
+    // Mark current selected dropdown items as active
+    document.querySelectorAll('.lang-select-item').forEach(item => {
+      if (item.getAttribute('data-lang') === lang) {
+        item.classList.add('active');
+      } else {
+        item.classList.remove('active');
+      }
+    });
+  }
+
+  function detectAndInitLanguage() {
+    const urlParams = new URLSearchParams(window.location.search);
+    let lang = urlParams.get('lang');
+
+    if (!lang) {
+      const hash = window.location.hash;
+      const hashParams = new URLSearchParams(hash.includes('?') ? hash.split('?')[1] : '');
+      lang = hashParams.get('lang');
+    }
+
+    if (!lang) {
+      lang = localStorage.getItem('app-lang');
+    }
+
+    if (!lang && navigator.language) {
+      const navLang = navigator.language.toLowerCase();
+      if (navLang.startsWith('zh-tw') || navLang.startsWith('zh-hk') || navLang.startsWith('zh-hant')) lang = 'tw';
+      else if (navLang.startsWith('zh')) lang = 'zh';
+      else if (navLang.startsWith('ja')) lang = 'ja';
+      else if (navLang.startsWith('ko')) lang = 'ko';
+      else if (navLang.startsWith('pt')) lang = 'pt';
+      else if (navLang.startsWith('es')) lang = 'es';
+      else if (navLang.startsWith('fr')) lang = 'fr';
+      else if (navLang.startsWith('de')) lang = 'de';
+      else if (navLang.startsWith('ru')) lang = 'ru';
+      else if (navLang.startsWith('it')) lang = 'it';
+      else if (navLang.startsWith('tr')) lang = 'tr';
+      else if (navLang.startsWith('pl')) lang = 'pl';
+      else if (navLang.startsWith('uk')) lang = 'uk';
+    }
+
+    if (!lang || !I18N_DICTS[lang]) {
+      lang = 'en';
+    }
+
+    applyTranslations(lang);
+  }
+
+  // Language selectors click event listeners
+  document.addEventListener('click', function(e) {
+    const item = e.target.closest('.lang-select-item');
+    if (item) {
+      e.preventDefault();
+      const lang = item.getAttribute('data-lang');
+      applyTranslations(lang);
+      localStorage.setItem('app-lang', lang);
+      
+      // Update browser search parameters dynamically without page reload
+      const url = new URL(window.location.href);
+      url.searchParams.set('lang', lang);
+      window.history.replaceState({}, '', url.toString());
+    }
+  });
+
+  // Accessibility dynamic screen reader announcer helper
+  function announceToScreenReader(message) {
+    const announcer = document.getElementById('app-accessibility-announcer');
+    if (!announcer) return;
+    announcer.textContent = '';
+    clearTimeout(announceToScreenReader._timeoutId);
+    announceToScreenReader._timeoutId = setTimeout(() => {
+      announcer.textContent = message;
+    }, 50);
+  }
+
+  // Visual skeleton loader generator for emoji list
+  function renderEmojiSkeletons() {
+    const grid = document.getElementById('emoji-modal-grid');
+    if (!grid) return;
+    // PERF-007: Clear elements using textContent
+    grid.textContent = '';
+    const fragment = document.createDocumentFragment();
+    for (let i = 0; i < 18; i++) {
+      const item = document.createElement('div');
+      item.className = 'emoji-item skeleton-placeholder';
+      item.setAttribute('aria-hidden', 'true');
+      item.style.border = '1px solid var(--border-color)';
+      item.style.borderRadius = '10px';
+      item.style.padding = '10px';
+      item.style.display = 'flex';
+      item.style.flexDirection = 'column';
+      item.style.alignItems = 'center';
+      item.style.gap = '8px';
+
+      const preview = document.createElement('span');
+      preview.className = 'emoji-preview skeleton-circle';
+      item.appendChild(preview);
+
+      const shortcodeRow = document.createElement('div');
+      shortcodeRow.className = 'emoji-shortcode';
+      const code = document.createElement('span');
+      code.className = 'skeleton-text';
+      code.style.width = '60px';
+      shortcodeRow.appendChild(code);
+      item.appendChild(shortcodeRow);
+
+      fragment.appendChild(item);
+    }
+    grid.appendChild(fragment);
+  }
+
+  // Visual skeleton loader generator for GitHub file list tree
+  function renderGitHubImportTreeSkeleton() {
+    if (!githubImportTree) return;
+    // PERF-007: Clear elements using textContent
+    githubImportTree.textContent = '';
+    
+    const wrapper = document.createElement('div');
+    wrapper.className = 'github-import-tree-skeleton';
+    
+    const list = document.createElement('ul');
+    list.style.listStyle = 'none';
+    list.style.paddingLeft = '4px';
+    list.style.margin = '0';
+    
+    for (let i = 0; i < 4; i++) {
+      const folderItem = document.createElement('li');
+      folderItem.style.margin = '6px 0';
+      
+      const folderSpan = document.createElement('span');
+      folderSpan.className = 'skeleton-placeholder skeleton-tree-folder';
+      folderItem.appendChild(folderSpan);
+      
+      const subList = document.createElement('ul');
+      subList.style.listStyle = 'none';
+      subList.style.paddingLeft = '18px';
+      subList.style.margin = '0';
+      
+      for (let j = 0; j < 2; j++) {
+        const fileItem = document.createElement('li');
+        fileItem.style.margin = '4px 0';
+        
+        const fileSpan = document.createElement('span');
+        fileSpan.className = 'skeleton-placeholder skeleton-tree-file';
+        fileItem.appendChild(fileSpan);
+        subList.appendChild(fileItem);
+      }
+      
+      folderItem.appendChild(subList);
+      list.appendChild(folderItem);
+    }
+    
+    wrapper.appendChild(list);
+    githubImportTree.appendChild(wrapper);
+  }
+
+  // Run detection
+  detectAndInitLanguage();
+
+  // Intercept all link clicks in the preview pane to open them securely and prevent page navigation
+  if (markdownPreview) {
+    markdownPreview.addEventListener('click', function(e) {
+      const link = e.target.closest('a');
+      if (link) {
+        const href = link.getAttribute('href');
+        if (href) {
+          if (href.startsWith('#')) {
+            const targetId = decodeURIComponent(href.slice(1));
+            let targetEl = null;
+            if (targetId) {
+              try {
+                targetEl = markdownPreview.querySelector(`[id="${CSS.escape(targetId)}"]`) ||
+                           markdownPreview.querySelector(`[name="${CSS.escape(targetId)}"]`);
+              } catch (err) {
+                targetEl = Array.from(markdownPreview.querySelectorAll('[id], [name]')).find(el => {
+                  return el.getAttribute('id') === targetId || el.getAttribute('name') === targetId;
+                });
+              }
+              
+              if (!targetEl) {
+                const cleanTargetId = targetId.toLowerCase().replace(/[^a-z0-9]/g, '');
+                if (cleanTargetId) {
+                  targetEl = Array.from(markdownPreview.querySelectorAll('h1, h2, h3, h4, h5, h6')).find(heading => {
+                    const cleanText = heading.textContent.toLowerCase().replace(/[^a-z0-9]/g, '');
+                    return cleanText === cleanTargetId;
+                  });
+                }
+              }
+            }
+            if (targetEl) {
+              e.preventDefault();
+              isProgrammaticScrolling = true;
+              
+              // Scroll preview pane to target heading
+              targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
+              
+              // Scroll editor pane to the matching synced position
+              const previewScrollRange = previewPane.scrollHeight - previewPane.clientHeight;
+              const targetRatio = previewScrollRange > 0 ? Math.min(1, Math.max(0, targetEl.offsetTop / previewScrollRange)) : 0;
+              const editorScrollPosition = targetRatio * (markdownEditor.scrollHeight - markdownEditor.clientHeight);
+              
+              markdownEditor.scrollTo({
+                top: editorScrollPosition,
+                behavior: 'smooth'
+              });
+              
+              if (window.programmaticScrollTimeout) {
+                clearTimeout(window.programmaticScrollTimeout);
+              }
+              window.programmaticScrollTimeout = setTimeout(() => {
+                isProgrammaticScrolling = false;
+              }, 1000);
+            }
+            return;
+          }
+          
+          e.preventDefault();
+          // Defense-in-depth: check that the URL protocol is safe
+          let isSafe = false;
+          try {
+            const parsed = new URL(href, window.location.href);
+            isSafe = ['http:', 'https:', 'mailto:', 'tel:', 'blob:'].includes(parsed.protocol);
+          } catch (err) {
+            // If URL constructor fails, it might be a relative path without a base, which is safe to resolve
+            isSafe = !href.trim().toLowerCase().startsWith('javascript:');
+          }
+          
+          if (isSafe) {
+            if (typeof Neutralino !== 'undefined') {
+              Neutralino.os.open(href);
+            } else {
+              window.open(href, '_blank', 'noopener,noreferrer');
+            }
+          } else {
+            console.warn('Blocked opening potentially unsafe URL:', href);
+          }
+        }
+      }
+    });
+  }
+
+  // Register Service Worker for offline capabilities
+  if ('serviceWorker' in navigator) {
+    window.addEventListener('load', function() {
+      navigator.serviceWorker.register('sw.js').then(function(registration) {
+        console.log('ServiceWorker registration successful with scope: ', registration.scope);
+      }, function(err) {
+        console.log('ServiceWorker registration failed: ', err);
+      });
+    });
+  }
+});

+ 24 - 0
sitemap.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+        xmlns:xhtml="http://www.w3.org/1999/xhtml">
+  <url>
+    <loc>https://markdownplusplus.pages.dev/</loc>
+    <xhtml:link rel="alternate" hreflang="x-default" href="https://markdownplusplus.pages.dev/" />
+    <xhtml:link rel="alternate" hreflang="en" href="https://markdownplusplus.pages.dev/?lang=en" />
+    <xhtml:link rel="alternate" hreflang="zh-Hans" href="https://markdownplusplus.pages.dev/?lang=zh" />
+    <xhtml:link rel="alternate" hreflang="ja" href="https://markdownplusplus.pages.dev/?lang=ja" />
+    <xhtml:link rel="alternate" hreflang="ko" href="https://markdownplusplus.pages.dev/?lang=ko" />
+    <xhtml:link rel="alternate" hreflang="pt-BR" href="https://markdownplusplus.pages.dev/?lang=pt" />
+    <xhtml:link rel="alternate" hreflang="es" href="https://markdownplusplus.pages.dev/?lang=es" />
+    <xhtml:link rel="alternate" hreflang="fr" href="https://markdownplusplus.pages.dev/?lang=fr" />
+    <xhtml:link rel="alternate" hreflang="de" href="https://markdownplusplus.pages.dev/?lang=de" />
+    <xhtml:link rel="alternate" hreflang="ru" href="https://markdownplusplus.pages.dev/?lang=ru" />
+    <xhtml:link rel="alternate" hreflang="it" href="https://markdownplusplus.pages.dev/?lang=it" />
+    <xhtml:link rel="alternate" hreflang="tr" href="https://markdownplusplus.pages.dev/?lang=tr" />
+    <xhtml:link rel="alternate" hreflang="pl" href="https://markdownplusplus.pages.dev/?lang=pl" />
+    <xhtml:link rel="alternate" hreflang="zh-Hant" href="https://markdownplusplus.pages.dev/?lang=tw" />
+    <xhtml:link rel="alternate" hreflang="uk" href="https://markdownplusplus.pages.dev/?lang=uk" />
+    <changefreq>weekly</changefreq>
+    <priority>1.0</priority>
+  </url>
+</urlset>

+ 3873 - 0
styles.css

@@ -0,0 +1,3873 @@
+:root {
+  --bg-color: #ffffff;
+  --editor-bg: #f6f8fa;
+  --preview-bg: #ffffff; /* Preview background for light mode */
+  --text-color: #24292e;
+  --text-secondary: #57606a;
+  --font-mono: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
+  --color-danger-fg: #d73a49;
+  --preview-text-color: #24292e; /* Text color for preview in light mode */
+  --border-color: #e1e4e8;
+  --header-bg: #f6f8fa;
+  --button-bg: #f6f8fa;
+  --button-hover: #e1e4e8;
+  --button-active: #d1d5da;
+  --scrollbar-thumb: #c1c1c1;
+  --scrollbar-track: #f1f1f1;
+  --accent-color: #0366d6;
+  --table-bg: #ffffff; /* Table background for light mode */
+  --code-bg: #f6f8fa; /* Code block background for light mode */
+  --skeleton-bg: #e2e8f0;
+  --skeleton-glow: rgba(255, 255, 255, 0.65);
+
+  /* Find & Replace Panel custom properties (PERF-010 consolidated) */
+  --fr-bg: rgba(255, 255, 255, 0.95);
+  --fr-border: #d0d7de;
+  --fr-shadow: 0 8px 24px rgba(140, 149, 159, 0.2);
+  --fr-btn-active: #0969da;
+  --fr-btn-active-bg: #ddf4ff;
+  --fr-match-highlight: #ffdf5d;
+  --fr-match-active: #ff9b30;
+  --fr-match-text-color: #24292e;
+  --fr-match-active-text-color: #24292e;
+  --fr-error-bg: #ffebe9;
+  --fr-error-border: #ff8577;
+  --fr-text-danger: #cf222e;
+}
+
+[data-theme="dark"] {
+  --bg-color: #0d1117;
+  --editor-bg: #161b22;
+  --preview-bg: #0d1117; /* Preview background for dark mode */
+  --text-color: #c9d1d9;
+  --text-secondary: #8b949e;
+  --color-danger-fg: #f85149;
+  --preview-text-color: #c9d1d9; /* Text color for preview in dark mode */
+  --border-color: #30363d;
+  --header-bg: #161b22;
+  --button-bg: #21262d;
+  --button-hover: #30363d;
+  --button-active: #3b434b;
+  --scrollbar-thumb: #484f58;
+  --scrollbar-track: #21262d;
+  --accent-color: #58a6ff;
+  --table-bg: #161b22; /* Table background for dark mode */
+  --code-bg: #161b22; /* Code block background for dark mode */
+  --skeleton-bg: #2d3139;
+  --skeleton-glow: rgba(255, 255, 255, 0.08);
+
+  /* Find & Replace Panel custom properties for dark mode */
+  --fr-bg: rgba(28, 33, 40, 0.98);
+  --fr-border: #444c56;
+  --fr-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
+  --fr-btn-active: #2f81f7;
+  --fr-btn-active-bg: rgba(56, 139, 253, 0.15);
+  --fr-match-highlight: rgba(187, 128, 9, 0.4);
+  --fr-match-active: #ad6200;
+  --fr-match-text-color: #c9d1d9;
+  --fr-match-active-text-color: #ffffff;
+  --fr-error-bg: rgba(248, 81, 73, 0.1);
+  --fr-error-border: rgba(248, 81, 73, 0.4);
+  --fr-text-danger: #ff7b72;
+}
+
+* {
+  box-sizing: border-box;
+  margin: 0;
+  padding: 0;
+}
+
+@media (min-width: 768px) {
+  html,
+  body {
+    height: 100%;
+    overflow: hidden;
+  }
+}
+
+body {
+  background-color: var(--bg-color);
+  color: var(--text-color);
+  /* PERF-021: Removed background-color transition to avoid full-viewport repaint on theme toggle */
+  transition: color 0.15s ease;
+  min-height: 100vh;
+  font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Hiragino Kaku Gothic ProN", Meiryo, "Malgun Gothic", "Apple SD Gothic Neo", "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+}
+
+.app-header {
+  background-color: var(--header-bg);
+  border-bottom: 1px solid var(--border-color);
+  padding: 0.35rem 0.75rem;
+  transition: background-color 0.3s ease;
+  position: relative;
+  z-index: 100;
+  flex-shrink: 0;
+}
+
+.app-container {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.content-container {
+  display: flex;
+  flex: 1;
+  overflow: hidden;
+}
+
+.editor-pane, .preview-pane {
+  flex: 1;
+  padding: 20px;
+  overflow-y: auto;
+  position: relative;
+  /* PERF-025: Shortened transition and scoped to background-color only */
+  transition: background-color 0.15s ease;
+}
+
+.editor-pane {
+  background-color: var(--editor-bg);
+  border-right: 1px solid var(--border-color);
+  padding-right: 0px;
+  --line-number-gutter: 0px;
+}
+
+.preview-pane {
+  background-color: var(--preview-bg); /* Using the new variable for preview background */
+}
+
+/* Custom scrollbar */
+.editor-pane::-webkit-scrollbar,
+.preview-pane::-webkit-scrollbar,
+#markdown-editor::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+}
+
+.editor-pane::-webkit-scrollbar-track,
+.preview-pane::-webkit-scrollbar-track,
+#markdown-editor::-webkit-scrollbar-track {
+  background: var(--scrollbar-track);
+}
+
+.editor-pane::-webkit-scrollbar-thumb,
+.preview-pane::-webkit-scrollbar-thumb,
+#markdown-editor::-webkit-scrollbar-thumb {
+  background: var(--scrollbar-thumb);
+  border-radius: 4px;
+}
+
+.editor-pane::-webkit-scrollbar-thumb:hover,
+.preview-pane::-webkit-scrollbar-thumb:hover,
+#markdown-editor::-webkit-scrollbar-thumb:hover {
+  background: var(--button-active);
+}
+
+#markdown-editor {
+  width: 100%;
+  height: 100%;
+  border: none;
+  background-color: transparent;
+  color: var(--text-color);
+  resize: none;
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+  font-size: 14px;
+  line-height: 1.5;
+  padding: 10px;
+  padding-left: calc(10px + var(--line-number-gutter));
+  transition: background-color 0.3s ease, color 0.3s ease;
+  overflow-y: auto;
+  position: relative;
+  z-index: 3;
+}
+
+#markdown-editor:focus {
+  outline: none;
+}
+
+.preview-pane {
+  padding: 20px;
+}
+
+.markdown-body {
+  padding: 20px;
+  width: 100%;
+  background-color: var(--preview-bg); /* Ensuring the markdown content matches preview background */
+  color: var(--preview-text-color); /* Using specific text color for preview content */
+}
+
+.markdown-body a.reference-link {
+  font-size: 0.75em;
+  letter-spacing: -0.02em;
+  line-height: 1;
+  vertical-align: super;
+  position: relative;
+  top: 0.08em;
+}
+
+/* Style tables in light mode */
+.markdown-body table {
+  background-color: var(--table-bg);
+  border-color: var(--border-color);
+}
+
+.markdown-body table tr {
+  background-color: var(--table-bg);
+  border-top: 1px solid var(--border-color);
+}
+
+.markdown-body table tr:nth-child(2n) {
+  background-color: var(--bg-color);
+}
+
+/* Style code blocks in light mode */
+.markdown-body pre {
+  background-color: var(--code-bg);
+  border-radius: 6px;
+}
+
+.markdown-body code {
+  background-color: var(--code-bg);
+  border-radius: 3px;
+  padding: 0.2em 0.4em;
+}
+
+.markdown-body img.emoji-inline {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.1em;
+}
+
+.markdown-body ul,
+.markdown-body ol {
+  padding-left: 2em;
+  margin: 0.4em 0;
+}
+
+.markdown-body ul ul,
+.markdown-body ul ol,
+.markdown-body ol ul,
+.markdown-body ol ol {
+  margin-top: 0.2em;
+  margin-bottom: 0.2em;
+}
+
+.markdown-body ul.contains-task-list,
+.markdown-body li.task-list-item {
+  list-style: none;
+}
+
+.markdown-body ul.contains-task-list {
+  padding-left: 2em;
+}
+
+.markdown-body li.task-list-item input[type="checkbox"] {
+  margin: 0 0.5em 0.2em 0;
+  vertical-align: middle;
+  pointer-events: none;
+}
+
+.markdown-body li.task-list-item::marker {
+  content: "";
+}
+
+.markdown-body li:has(> input[type="checkbox"]) {
+  list-style: none;
+}
+
+.markdown-body li:has(> input[type="checkbox"])::marker {
+  content: "";
+}
+
+.markdown-body ul:has(> li > input[type="checkbox"]) {
+  list-style: none;
+  padding-left: 2em;
+}
+
+.markdown-body .footnotes {
+  margin-top: 1.5rem;
+  font-size: 0.9em;
+}
+
+.markdown-body .footnotes ol {
+  padding-left: 1.5em;
+}
+
+.markdown-body .footnotes ol > li::marker {
+  content: "[" counter(list-item) "] ";
+  font-weight: 600;
+}
+
+.markdown-body .footnotes li > p {
+  margin: 0.2em 0;
+}
+
+.markdown-body .footnote-ref a,
+.markdown-body .footnote-backref {
+  text-decoration: none;
+}
+
+.markdown-body .footnote-backref {
+  margin-left: 0.4em;
+}
+
+.markdown-body .markdown-alert {
+  padding: 0.5rem 1rem;
+  margin-bottom: 16px;
+  border-left: 0.25em solid;
+  border-radius: 0.375rem;
+}
+
+.markdown-body .markdown-alert > :last-child {
+  margin-bottom: 0;
+}
+
+.markdown-body .markdown-alert-title {
+  margin: 0 0 8px;
+  font-weight: 600;
+  line-height: 1.25;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.markdown-body .markdown-alert-icon {
+  display: inline-flex;
+  width: 16px;
+  height: 16px;
+}
+
+.markdown-body .markdown-alert-icon svg {
+  width: 16px;
+  height: 16px;
+  fill: currentColor;
+}
+
+.markdown-body .markdown-alert-note {
+  color: #0969da;
+  border-left-color: #0969da;
+  background-color: #ddf4ff;
+}
+
+.markdown-body .markdown-alert-tip {
+  color: #1a7f37;
+  border-left-color: #1a7f37;
+  background-color: #dafbe1;
+}
+
+.markdown-body .markdown-alert-important {
+  color: #8250df;
+  border-left-color: #8250df;
+  background-color: #fbefff;
+}
+
+.markdown-body .markdown-alert-warning {
+  color: #9a6700;
+  border-left-color: #9a6700;
+  background-color: #fff8c5;
+}
+
+.markdown-body .markdown-alert-caution {
+  color: #cf222e;
+  border-left-color: #cf222e;
+  background-color: #ffebe9;
+}
+
+.markdown-body .markdown-alert > *:not(.markdown-alert-title) {
+  color: var(--preview-text-color);
+}
+
+[data-theme="dark"] .markdown-body .markdown-alert-note {
+  color: #4493f8;
+  background-color: rgba(31, 111, 235, 0.15);
+  border-left-color: #4493f8;
+}
+
+[data-theme="dark"] .markdown-body .markdown-alert-tip {
+  color: #3fb950;
+  background-color: rgba(35, 134, 54, 0.15);
+  border-left-color: #3fb950;
+}
+
+[data-theme="dark"] .markdown-body .markdown-alert-important {
+  color: #ab7df8;
+  background-color: rgba(137, 87, 229, 0.15);
+  border-left-color: #ab7df8;
+}
+
+[data-theme="dark"] .markdown-body .markdown-alert-warning {
+  color: #d29922;
+  background-color: rgba(210, 153, 34, 0.18);
+  border-left-color: #d29922;
+}
+
+[data-theme="dark"] .markdown-body .markdown-alert-caution {
+  color: #f85149;
+  background-color: rgba(248, 81, 73, 0.18);
+  border-left-color: #f85149;
+}
+
+.toolbar {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.toolbar-group {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.toolbar-divider {
+  width: 1px;
+  height: 20px;
+  background-color: var(--border-color);
+  opacity: 0.7;
+}
+
+.tool-button {
+  background-color: var(--button-bg);
+  border: 1px solid var(--border-color);
+  color: var(--text-color);
+  border-radius: 5px;
+  padding: 4px 8px;
+  font-size: 13px;
+  cursor: pointer;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+  /* PERF-016: Specific transition properties instead of 'all' to avoid animating layout-triggering properties */
+  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
+}
+
+.tool-button:hover {
+  background-color: var(--button-hover);
+}
+
+.tool-button:active {
+  background-color: var(--button-active);
+}
+
+.tool-button:disabled,
+.tool-button[aria-disabled="true"] {
+  cursor: not-allowed;
+  opacity: 0.5;
+}
+
+.tool-button i {
+  font-size: 15px;
+}
+
+.tool-button.is-active,
+.tool-button.is-active:hover {
+  border-color: var(--accent-color);
+  color: var(--accent-color);
+  background-color: rgba(3, 102, 214, 0.08);
+}
+
+.btn-text {
+  display: none;
+}
+
+.toolbar .tool-button {
+  height: 28px;
+  min-width: 28px;
+}
+
+.toolbar .tool-button.sync-active {
+  border-color: var(--accent-color);
+  color: var(--accent-color);
+}
+
+.file-input {
+  display: none;
+}
+
+/* Drag overlay: full-screen drop target shown when user drags a file over the window */
+.drag-overlay {
+  display: none;
+  position: fixed;
+  inset: 0;
+  z-index: 9999;
+  background-color: rgba(0, 0, 0, 0.45);
+  pointer-events: none;
+  align-items: center;
+  justify-content: center;
+}
+
+.drag-overlay.active {
+  display: flex;
+  pointer-events: auto;
+}
+
+.drag-overlay-inner {
+  border: 3px dashed var(--accent-color);
+  border-radius: 12px;
+  padding: 48px 64px;
+  text-align: center;
+  color: #ffffff;
+  background-color: rgba(3, 102, 214, 0.15);
+  animation: overlayPulse 1.4s ease-in-out infinite;
+}
+
+.drag-overlay-icon {
+  display: block;
+  font-size: 3rem;
+  margin-bottom: 12px;
+  color: var(--accent-color);
+}
+
+.drag-overlay-text {
+  font-size: 1.4rem;
+  font-weight: 600;
+  margin-bottom: 4px;
+}
+
+.drag-overlay-sub {
+  font-size: 0.85rem;
+  opacity: 0.75;
+  margin-bottom: 0;
+}
+
+@keyframes overlayPulse {
+  0%, 100% { transform: scale(1); }
+  50% { transform: scale(1.015); }
+}
+
+/* Editor drop hint: subtle text at bottom of editor pane, shown only when empty */
+.drop-hint {
+  position: absolute;
+  bottom: 14px;
+  left: 0;
+  right: 0;
+  text-align: center;
+  font-size: 0.75rem;
+  color: var(--text-color);
+  opacity: 0.35;
+  pointer-events: none;
+  user-select: none;
+  z-index: 3;
+}
+
+.editor-pane:has(#markdown-editor:not(:placeholder-shown)) .drop-hint {
+  display: none;
+}
+
+.line-numbers {
+  position: absolute;
+  top: 20px;
+  bottom: 20px;
+  left: 20px;
+  width: var(--line-number-gutter);
+  padding: 10px 8px 10px 0;
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+  font-size: 14px;
+  line-height: 1.5;
+  text-align: right;
+  color: var(--text-secondary);
+  background-color: var(--editor-bg);
+  border-right: 1px solid var(--border-color);
+  box-sizing: border-box;
+  overflow: hidden;
+  pointer-events: none;
+  user-select: none;
+  z-index: 2;
+  font-variant-numeric: tabular-nums;
+}
+
+.line-numbers .line-number {
+  display: block;
+  height: auto;
+}
+
+.editor-highlight-layer {
+  position: absolute;
+  inset: 20px 0 20px calc(20px + var(--line-number-gutter));
+  padding: 10px;
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+  font-size: 14px;
+  line-height: 1.5;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+  color: transparent;
+  pointer-events: none;
+  overflow: auto;
+  background-color: var(--editor-bg);
+  border-radius: 4px;
+  z-index: 1;
+}
+
+.editor-highlight-layer::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+}
+
+.editor-highlight-layer::-webkit-scrollbar-thumb {
+  background: transparent;
+}
+
+.editor-highlight-layer::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+.find-highlight {
+  background-color: var(--fr-match-highlight, rgba(255, 223, 93, 0.4)) !important;
+  border-radius: 2px;
+  color: transparent !important;
+  padding: 0 !important;
+  margin: 0 !important;
+}
+
+.find-highlight.active {
+  background-color: var(--fr-match-active, #ff9b30) !important;
+  color: transparent !important;
+  padding: 0 !important;
+  margin: 0 !important;
+  outline: 1px solid var(--accent-color, #0366d6) !important;
+  outline-offset: -1px;
+}
+
+.preview-find-highlight {
+  background-color: var(--fr-match-highlight, rgba(255, 223, 93, 0.4)) !important;
+  color: var(--fr-match-text-color, inherit) !important;
+  border-radius: 2px;
+  padding: 0 1px !important;
+  margin: 0 !important;
+}
+
+.preview-find-highlight.active {
+  background-color: var(--fr-match-active, #ff9b30) !important;
+  color: var(--fr-match-active-text-color, inherit) !important;
+  outline: 1px solid var(--accent-color, #0366d6) !important;
+  outline-offset: -1px;
+}
+
+/* Dropdown improvements */
+.dropdown-menu {
+  background-color: var(--bg-color);
+  border-color: var(--border-color);
+}
+
+.dropdown-item {
+  color: var(--text-color);
+}
+
+.dropdown-item:hover, .dropdown-item:focus {
+  background-color: var(--button-hover);
+  color: var(--text-color);
+}
+
+/* Markdown formatting toolbar */
+.markdown-format-toolbar {
+  display: flex;
+  align-items: center;
+  height: 34px;
+  padding: 0 6px;
+  background-color: var(--header-bg);
+  border-bottom: 1px solid var(--border-color);
+  overflow-x: auto;
+  overflow-y: hidden;
+  flex-shrink: 0;
+  scrollbar-width: none;
+  -ms-overflow-style: none;
+}
+
+.markdown-format-toolbar::-webkit-scrollbar {
+  display: none;
+}
+
+.markdown-toolbar-group {
+  display: flex;
+  align-items: center;
+  gap: 2px;
+  height: 100%;
+  padding: 0 6px;
+  border-right: 1px solid var(--border-color);
+  flex-shrink: 0;
+}
+
+.markdown-toolbar-group:first-child {
+  padding-left: 0;
+}
+
+.markdown-toolbar-group:last-child {
+  border-right: none;
+  padding-right: 0;
+}
+
+.markdown-tool-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 26px;
+  height: 26px;
+  border: 1px solid transparent;
+  border-radius: 4px;
+  background: transparent;
+  color: var(--text-color);
+  cursor: pointer;
+  font-size: 14px;
+  line-height: 1;
+  padding: 0;
+  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
+}
+
+.markdown-tool-btn:hover,
+.markdown-tool-btn:focus-visible {
+  background-color: var(--button-hover);
+  border-color: var(--border-color);
+  color: var(--accent-color);
+}
+
+.markdown-tool-btn:active {
+  background-color: var(--button-active);
+}
+
+.markdown-tool-btn:disabled,
+.markdown-tool-btn.disabled {
+  opacity: 0.4;
+  cursor: not-allowed;
+  pointer-events: none;
+}
+
+.markdown-tool-btn i {
+  font-size: 15px;
+}
+
+.markdown-tool-btn[data-md-action="reference"] i::before {
+  content: "[ ]";
+  font-style: normal;
+  font-size: 12px;
+  letter-spacing: -0.12em;
+}
+
+.markdown-tool-btn.text-tool {
+  width: auto;
+  min-width: 26px;
+  padding: 0 5px;
+  font-weight: 600;
+  font-family: Georgia, "Times New Roman", serif;
+}
+
+.heading-group .markdown-tool-btn {
+  min-width: 30px;
+}
+
+
+
+/* Loading indicators */
+.loading {
+  opacity: 0.6;
+  pointer-events: none;
+}
+
+/* Focus outline for accessibility */
+button:focus, 
+a:focus {
+  outline: 2px solid var(--accent-color);
+  outline-offset: 2px;
+}
+
+/* Animation for copied message */
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+/* Tooltip styles */
+.tooltip {
+  position: absolute;
+  background: var(--button-bg);
+  border: 1px solid var(--border-color);
+  padding: 5px 8px;
+  border-radius: 4px;
+  font-size: 12px;
+  z-index: 1000;
+  animation: fadeIn 0.2s ease;
+}
+
+/* Styles for GitHub markdown preview light mode */
+.markdown-body {
+  color-scheme: light;
+  --color-prettylights-syntax-comment: #6a737d;
+  --color-prettylights-syntax-constant: #005cc5;
+  --color-prettylights-syntax-entity: #6f42c1;
+  --color-prettylights-syntax-storage-modifier-import: #24292e;
+  --color-prettylights-syntax-entity-tag: #22863a;
+  --color-prettylights-syntax-keyword: #cf222e;
+  --color-prettylights-syntax-string: #032f62;
+  --color-prettylights-syntax-variable: #e36209;
+  --color-prettylights-syntax-brackethighlighter-unmatched: #b31d28;
+  --color-prettylights-syntax-invalid-illegal-text: #fafbfc;
+  --color-prettylights-syntax-invalid-illegal-bg: #b31d28;
+  --color-prettylights-syntax-carriage-return-text: #fafbfc;
+  --color-prettylights-syntax-carriage-return-bg: #d73a49;
+  --color-prettylights-syntax-string-regexp: #22863a;
+  --color-prettylights-syntax-markup-list: #735c0f;
+  --color-prettylights-syntax-markup-heading: #005cc5;
+  --color-prettylights-syntax-markup-italic: #24292e;
+  --color-prettylights-syntax-markup-bold: #24292e;
+  --color-prettylights-syntax-markup-deleted-text: #b31d28;
+  --color-prettylights-syntax-markup-deleted-bg: #ffeef0;
+  --color-prettylights-syntax-markup-inserted-text: #22863a;
+  --color-prettylights-syntax-markup-inserted-bg: #f0fff4;
+  --color-prettylights-syntax-markup-changed-text: #e36209;
+  --color-prettylights-syntax-markup-changed-bg: #ffebda;
+  --color-prettylights-syntax-markup-ignored-text: #f6f8fa;
+  --color-prettylights-syntax-markup-ignored-bg: #005cc5;
+  --color-prettylights-syntax-meta-diff-range: #6f42c1;
+  --color-prettylights-syntax-brackethighlighter-angle: #586069;
+  --color-prettylights-syntax-sublimelinter-gutter-mark: #e1e4e8;
+  --color-prettylights-syntax-constant-other-reference-link: #032f62;
+  --color-fg-default: #24292e;
+  --color-fg-muted: #586069;
+  --color-fg-subtle: #6a737d;
+  --color-canvas-default: #ffffff;
+  --color-canvas-subtle: #f6f8fa;
+  --color-border-default: #e1e4e8;
+  --color-border-muted: #eaecef;
+  --color-neutral-muted: rgba(175,184,193,0.2);
+  --color-accent-fg: #0366d6;
+  --color-accent-emphasis: #0366d6;
+  --color-attention-subtle: #fff5b1;
+  --color-danger-fg: #d73a49;
+}
+
+/* Styles for GitHub markdown preview dark mode */
+[data-theme="dark"] .markdown-body {
+  color-scheme: dark;
+  --color-prettylights-syntax-comment: #8b949e;
+  --color-prettylights-syntax-constant: #79c0ff;
+  --color-prettylights-syntax-entity: #d2a8ff;
+  --color-prettylights-syntax-storage-modifier-import: #c9d1d9;
+  --color-prettylights-syntax-entity-tag: #7ee787;
+  --color-prettylights-syntax-keyword: #ff7b72;
+  --color-prettylights-syntax-string: #a5d6ff;
+  --color-prettylights-syntax-variable: #ffa657;
+  --color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
+  --color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
+  --color-prettylights-syntax-invalid-illegal-bg: #8e1519;
+  --color-prettylights-syntax-carriage-return-text: #f0f6fc;
+  --color-prettylights-syntax-carriage-return-bg: #b62324;
+  --color-prettylights-syntax-string-regexp: #7ee787;
+  --color-prettylights-syntax-markup-list: #f2cc60;
+  --color-prettylights-syntax-markup-heading: #1f6feb;
+  --color-prettylights-syntax-markup-italic: #c9d1d9;
+  --color-prettylights-syntax-markup-bold: #c9d1d9;
+  --color-prettylights-syntax-markup-deleted-text: #ffdcd7;
+  --color-prettylights-syntax-markup-deleted-bg: #67060c;
+  --color-prettylights-syntax-markup-inserted-text: #aff5b4;
+  --color-prettylights-syntax-markup-inserted-bg: #033a16;
+  --color-prettylights-syntax-markup-changed-text: #ffdfb6;
+  --color-prettylights-syntax-markup-changed-bg: #5a1e02;
+  --color-prettylights-syntax-markup-ignored-text: #c9d1d9;
+  --color-prettylights-syntax-markup-ignored-bg: #1158c7;
+  --color-prettylights-syntax-meta-diff-range: #d2a8ff;
+  --color-prettylights-syntax-brackethighlighter-angle: #8b949e;
+  --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
+  --color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
+  --color-fg-default: #c9d1d9;
+  --color-fg-muted: #8b949e;
+  --color-fg-subtle: #484f58;
+  --color-canvas-default: #0d1117;
+  --color-canvas-subtle: #161b22;
+  --color-border-default: #30363d;
+  --color-border-muted: #21262d;
+  --color-neutral-muted: rgba(110,118,129,0.4);
+  --color-accent-fg: #58a6ff;
+  --color-accent-emphasis: #1f6feb;
+  --color-attention-subtle: rgba(187,128,9,0.15);
+  --color-danger-fg: #f85149;
+}
+
+/* Override specific styles for dark mode tables and code */
+[data-theme="dark"] .markdown-body table tr {
+  background-color: var(--table-bg);
+}
+
+[data-theme="dark"] .markdown-body table tr:nth-child(2n) {
+  background-color: #1c2128; /* Slightly lighter than base dark background */
+}
+
+[data-theme="dark"] .markdown-body pre {
+  background-color: var(--code-bg);
+}
+
+[data-theme="dark"] .markdown-body code {
+  background-color: var(--code-bg);
+}
+
+/* Syntax Highlighting Mapping to GitHub Variables */
+.hljs {
+  color: var(--color-fg-default);
+}
+.hljs-doctag,
+.hljs-keyword,
+.hljs-meta .hljs-keyword,
+.hljs-template-tag,
+.hljs-template-variable,
+.hljs-type,
+.hljs-variable.language_ {
+  color: var(--color-prettylights-syntax-keyword);
+}
+.hljs-title,
+.hljs-title.class_,
+.hljs-title.class_.inherited__,
+.hljs-title.function_ {
+  color: var(--color-prettylights-syntax-entity);
+}
+.hljs-attr,
+.hljs-attribute,
+.hljs-literal,
+.hljs-meta,
+.hljs-number,
+.hljs-operator,
+.hljs-variable,
+.hljs-selector-attr,
+.hljs-selector-class,
+.hljs-selector-id {
+  color: var(--color-prettylights-syntax-constant);
+}
+.hljs-regexp,
+.hljs-string,
+.hljs-meta .hljs-string {
+  color: var(--color-prettylights-syntax-string);
+}
+.hljs-built_in,
+.hljs-symbol {
+  color: var(--color-prettylights-syntax-variable);
+}
+.hljs-comment,
+.hljs-code,
+.hljs-formula {
+  color: var(--color-prettylights-syntax-comment);
+}
+.hljs-name,
+.hljs-quote,
+.hljs-selector-tag,
+.hljs-selector-pseudo {
+  color: var(--color-prettylights-syntax-entity-tag);
+}
+.hljs-subst {
+  color: var(--color-fg-default);
+}
+.hljs-section {
+  color: var(--color-prettylights-syntax-markup-heading);
+  font-weight: bold;
+}
+.hljs-bullet {
+  color: var(--color-prettylights-syntax-constant);
+}
+.hljs-emphasis {
+  color: var(--color-fg-default);
+  font-style: italic;
+}
+.hljs-strong {
+  color: var(--color-fg-default);
+  font-weight: bold;
+}
+.hljs-addition {
+  color: var(--color-prettylights-syntax-markup-inserted-text);
+  background-color: var(--color-prettylights-syntax-markup-inserted-bg);
+}
+.hljs-deletion {
+  color: var(--color-prettylights-syntax-markup-deleted-text);
+  background-color: var(--color-prettylights-syntax-markup-deleted-bg);
+}
+
+.stats-container {
+  font-size: 0.8rem;
+  color: var(--text-color);
+}
+
+.stat-item {
+  align-items: center;
+}
+
+.stat-item i {
+  font-size: 0.9rem;
+  opacity: 0.8;
+}
+
+#importDropdown,
+#exportDropdown,
+#languageDropdown {
+  font-size: 0.8rem;
+}
+
+#importDropdown i,
+#exportDropdown i,
+#languageDropdown i {
+  font-size: 0.9rem;
+}
+
+/* Ensure desktop dropdown menu options match the stats-container font size */
+[aria-labelledby="importDropdown"] .dropdown-item,
+[aria-labelledby="exportDropdown"] .dropdown-item,
+[aria-labelledby="languageDropdown"] .dropdown-item {
+  font-size: 0.8rem;
+}
+
+/* Ensure mobile menu import, export, and language dropdown triggers/options match the mobile stats-container font size */
+#mobile-import-button,
+#mobile-import-github-button,
+#mobile-export-md,
+#mobile-export-html,
+#mobile-export-pdf,
+#mobileLanguageDropdown {
+  font-size: 0.9rem !important;
+}
+
+[aria-labelledby="mobileLanguageDropdown"] .dropdown-item {
+  font-size: 0.9rem;
+}
+
+.editor-pane {
+  overflow: hidden;
+}
+
+/* Mobile Menu Styles */
+.mobile-menu {
+  display: none;
+  position: relative;
+  z-index: 1001;
+}
+
+
+
+/* slide‑in panel */
+.mobile-menu-panel {
+  position: fixed;
+  top: 0;
+  right: -300px;
+  width: 280px;
+  height: 100vh;
+  background-color: var(--bg-color);
+  box-shadow: -2px 0 10px rgba(0, 0, 0, 0.2);
+  transition: right 0.3s ease;
+  overflow-y: auto;
+  padding: 1rem;
+  display: flex;
+  flex-direction: column;
+  z-index: 1002;
+}
+
+.mobile-menu-panel.active {
+  right: 0;
+}
+
+/* translucent overlay behind panel */
+.mobile-menu-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100vh;
+  background-color: rgba(0, 0, 0, 0.5);
+  opacity: 0;
+  visibility: hidden;
+  pointer-events: none;
+  transition: opacity 0.3s ease, visibility 0.3s ease;
+  z-index: 1000;
+}
+
+.mobile-menu-overlay.active {
+  opacity: 1;
+  visibility: visible;
+  pointer-events: auto;
+}
+
+/* header inside mobile menu */
+.mobile-menu-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 1rem;
+}
+
+.mobile-menu-header h5 {
+  margin: 0;
+  font-size: 1.25rem;
+  color: var(--text-color);
+}
+
+/* stats section in mobile menu */
+.mobile-stats-container {
+  border-bottom: 1px solid var(--border-color);
+  padding-bottom: 0.75rem;
+  margin-bottom: 1rem;
+}
+
+.mobile-stats-container .stat-item {
+  font-size: 0.9rem;
+  color: var(--text-color);
+  display: flex;
+  align-items: center;
+}
+
+.mobile-stats-container .stat-item i {
+  margin-right: 0.5em;
+  opacity: 0.8;
+}
+
+/* menu buttons list */
+.mobile-menu-items {
+  display: flex;
+  flex-direction: column;
+  gap: 0.5rem;
+  flex-grow: 1;
+}
+
+/* each menu item */
+.mobile-menu-item {
+  background-color: var(--button-bg);
+  border: 1px solid var(--border-color);
+  color: var(--text-color);
+  border-radius: 6px;
+  padding: 0.6rem 1rem;
+  font-size: 1rem;
+  text-align: left;
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+  transition: background-color 0.2s ease;
+  cursor: pointer;
+}
+
+.mobile-menu-item:hover {
+  background-color: var(--button-hover);
+}
+
+.mobile-menu-item:active {
+  background-color: var(--button-active);
+}
+
+/* close button override */
+#close-mobile-menu.tool-button {
+  padding: 0.25rem 0.5rem;
+  font-size: 1rem;
+}
+
+/* Mobile document tabs section */
+.mobile-tabs-section {
+  border-bottom: 1px solid var(--border-color);
+  padding-bottom: 0.75rem;
+}
+
+.mobile-tabs-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 0.5rem;
+}
+
+.mobile-tabs-label {
+  font-size: 0.85rem;
+  font-weight: 600;
+  color: var(--text-color);
+  opacity: 0.8;
+  text-transform: uppercase;
+  letter-spacing: 0.04em;
+}
+
+.mobile-new-tab-btn {
+  background: none;
+  border: 1px solid var(--border-color);
+  border-radius: 4px;
+  color: var(--text-color);
+  padding: 2px 7px;
+  font-size: 0.9rem;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  transition: background-color 0.15s ease;
+}
+
+.mobile-new-tab-btn:hover {
+  background-color: var(--button-hover);
+}
+
+.mobile-tab-list {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  max-height: 180px;
+  overflow-y: auto;
+}
+
+.mobile-tab-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background-color: var(--button-bg);
+  border: 1px solid var(--border-color);
+  border-radius: 6px;
+  padding: 0.45rem 0.75rem;
+  font-size: 0.9rem;
+  color: var(--text-color);
+  cursor: pointer;
+  transition: background-color 0.15s ease;
+  gap: 0.5rem;
+}
+
+.mobile-tab-item:hover {
+  background-color: var(--button-hover);
+}
+
+.mobile-tab-item.active {
+  border-color: var(--accent-color);
+  color: var(--accent-color);
+  background-color: var(--bg-color);
+}
+
+.mobile-tab-title {
+  flex: 1;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  min-width: 0;
+}
+
+.mobile-tab-item .tab-menu-btn {
+  opacity: 0.6;
+}
+
+.mobile-tab-item:hover .tab-menu-btn,
+.mobile-tab-item.active .tab-menu-btn {
+  opacity: 0.8;
+}
+
+#mobile-tab-reset-btn {
+  margin-left: 0;
+  height: auto;
+  padding: 0.45rem 0.75rem;
+  justify-content: center;
+  font-size: 0.9rem;
+}
+
+/* ==========================================
+   NAVBAR RESPONSIVE BREAKPOINTS
+   >= 1080px : full desktop navbar
+   <  1080px : mobile hamburger + stacked panes
+   ========================================== */
+
+/* Mobile / tablet (< 1080px): switch to hamburger, stack panes */
+@media (max-width: 1079px) {
+  /* Override Bootstrap d-md-flex / d-md-none so the breakpoint is 1080px */
+  .stats-container,
+  .toolbar {
+    display: none !important;
+  }
+
+  /* Expand touch target sizes to meet WCAG mobile guidelines */
+  .markdown-tool-btn {
+    width: 36px !important;
+    height: 36px !important;
+    font-size: 16px !important;
+  }
+  .markdown-format-toolbar {
+    height: 44px !important;
+  }
+  .tab-close-btn {
+    width: 28px !important;
+    height: 28px !important;
+    font-size: 14px !important;
+  }
+  .tab-menu-btn {
+    width: 28px !important;
+    height: 28px !important;
+    font-size: 14px !important;
+  }
+
+  .mobile-menu {
+    display: block !important;
+  }
+
+  /* Stack editor and preview vertically */
+  .content-container {
+    flex-direction: column;
+  }
+
+  .editor-pane,
+  .preview-pane {
+    flex: none;
+    height: 50%;
+    border-right: none;
+  }
+
+  .editor-pane {
+    border-bottom: 1px solid var(--border-color);
+  }
+
+  /* Hide drag-resize divider (touch devices don't use it) */
+  .resize-divider {
+    display: none;
+  }
+
+  /* Single-pane view modes: occupy full height */
+  .content-container.view-editor-only .editor-pane,
+  .content-container.view-preview-only .preview-pane {
+    height: 100%;
+  }
+
+  .content-container.view-split .editor-pane,
+  .content-container.view-split .preview-pane {
+    height: 50%;
+  }
+}
+
+.github-link {
+  color: var(--text-color);
+  text-decoration: none;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: transform 0.2s ease, color 0.2s ease;
+  margin-right: 2rem;
+}
+
+.github-link:hover {
+  color: var(--accent-color);
+  transform: scale(1.1);
+}
+
+.github-link i {
+  font-size: 1.25rem;
+}
+
+/* ========================================
+   HEADER LAYOUT
+   ======================================== */
+.header-container {
+  position: relative;
+  min-height: 30px;
+}
+
+.app-header h1 {
+  font-size: 1.05rem;
+  line-height: 1.1;
+}
+
+.header-left {
+  flex: 1 0 auto;
+  justify-content: flex-start;
+  white-space: nowrap;
+}
+
+.header-right {
+  flex: 1 0 auto;
+  justify-content: flex-end;
+  white-space: nowrap;
+}
+
+/* Pane View States */
+.content-container.view-editor-only .preview-pane {
+  display: none;
+}
+
+.content-container.view-editor-only .editor-pane {
+  flex: 1;
+  border-right: none;
+}
+
+.content-container.view-preview-only .editor-pane {
+  display: none;
+}
+
+.content-container.view-preview-only .preview-pane {
+  flex: 1;
+}
+
+.content-container.view-split .editor-pane,
+.content-container.view-split .preview-pane {
+  flex: 1;
+}
+
+/* Compact desktop (< 1280px): compact toolbar */
+@media (max-width: 1280px) {
+  /* Compact toolbar at medium widths */
+  .toolbar {
+    gap: 4px;
+  }
+}
+
+
+
+/* ========================================
+   RESIZE DIVIDER - Story 1.3
+   ======================================== */
+
+.resize-divider {
+  width: 8px;
+  background-color: transparent;
+  cursor: col-resize;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  position: relative;
+  z-index: 10;
+  transition: background-color 0.2s ease;
+}
+
+.resize-divider:hover {
+  background-color: var(--button-hover);
+}
+
+.resize-divider.dragging {
+  background-color: var(--accent-color);
+}
+
+.resize-divider-handle {
+  width: 2px;
+  height: 40px;
+  background-color: var(--border-color);
+  border-radius: 2px;
+  transition: background-color 0.2s ease, width 0.2s ease;
+}
+
+.resize-divider:hover .resize-divider-handle,
+.resize-divider.dragging .resize-divider-handle {
+  background-color: var(--accent-color);
+  width: 3px;
+}
+
+/* Hide divider in single-pane modes */
+.content-container.view-editor-only .resize-divider,
+.content-container.view-preview-only .resize-divider {
+  display: none;
+}
+
+
+
+/* Prevent text selection during drag */
+.resizing {
+  user-select: none;
+  cursor: col-resize !important;
+}
+
+.resizing * {
+  cursor: col-resize !important;
+}
+
+.resizing #markdown-preview,
+.resizing #markdown-editor,
+.resizing .line-numbers {
+  pointer-events: none !important;
+}
+
+/* ========================================
+   MOBILE VIEW MODE CONTROLS - Story 1.4
+   ======================================== */
+
+.mobile-view-mode-group {
+  display: flex;
+  gap: 0;
+  border-bottom: 1px solid var(--border-color);
+  padding-bottom: 0.75rem;
+}
+
+.mobile-view-mode-btn {
+  flex: 1;
+  background-color: var(--button-bg);
+  border: 1px solid var(--border-color);
+  color: var(--text-color);
+  padding: 8px 12px;
+  font-size: 14px;
+  cursor: pointer;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+  transition: all 0.2s ease;
+}
+
+.mobile-view-mode-btn:first-child {
+  border-radius: 6px 0 0 6px;
+}
+
+.mobile-view-mode-btn:last-child {
+  border-radius: 0 6px 6px 0;
+}
+
+.mobile-view-mode-btn:not(:last-child) {
+  border-right: none;
+}
+
+.mobile-view-mode-btn:hover,
+.mobile-view-mode-btn:active {
+  background-color: var(--button-hover);
+}
+
+.mobile-view-mode-btn.active {
+  background-color: var(--button-bg);
+  border-color: var(--accent-color);
+  color: var(--accent-color);
+  border-width: 2px;
+  padding: 7px 11px;
+}
+
+.mobile-view-mode-btn.active:not(:last-child) {
+  border-right: 2px solid var(--accent-color);
+}
+
+.mobile-view-mode-btn i {
+  font-size: 18px;
+}
+
+.mobile-view-mode-btn span {
+  font-size: 12px;
+}
+
+/* ========================================
+   RESPONSIVE VIEW MODE FIXES - Story 1.5
+   ======================================== */
+
+
+
+/* ========================================
+   PDF EXPORT TABLE FIX - Rowspan/Colspan
+   ======================================== */
+
+/* Fix for html2canvas not properly rendering rowspan/colspan cells.
+   Apply backgrounds to cells instead of rows to prevent row backgrounds
+   from painting over rowspan cells during canvas capture. */
+.pdf-export table tr {
+  background-color: transparent !important;
+}
+
+.pdf-export table th,
+.pdf-export table td {
+  background-color: var(--table-bg, #ffffff);
+  position: relative;
+}
+
+.pdf-export table tr:nth-child(2n) th,
+.pdf-export table tr:nth-child(2n) td {
+  background-color: var(--bg-color, #f6f8fa);
+}
+
+/* Ensure rowspan cells render correctly */
+.pdf-export table th[rowspan],
+.pdf-export table td[rowspan] {
+  vertical-align: middle;
+  background-color: var(--table-bg, #ffffff) !important;
+}
+
+/* Ensure colspan cells render correctly */
+.pdf-export table th[colspan],
+.pdf-export table td[colspan] {
+  text-align: center;
+}
+
+/* Dark mode PDF export table fix */
+[data-theme="dark"] .pdf-export table th,
+[data-theme="dark"] .pdf-export table td {
+  background-color: var(--table-bg, #161b22);
+}
+
+[data-theme="dark"] .pdf-export table tr:nth-child(2n) th,
+[data-theme="dark"] .pdf-export table tr:nth-child(2n) td {
+  background-color: #1c2128;
+}
+
+[data-theme="dark"] .pdf-export table th[rowspan],
+[data-theme="dark"] .pdf-export table td[rowspan] {
+  background-color: var(--table-bg, #161b22) !important;
+}
+
+/* ========================================
+   MERMAID DIAGRAM TOOLBAR
+   ======================================== */
+
+.mermaid-container {
+  position: relative;
+}
+
+.mermaid-toolbar {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  display: flex;
+  gap: 4px;
+  opacity: 0;
+  transition: opacity 0.2s ease;
+  z-index: 10;
+}
+
+.mermaid-container:hover .mermaid-toolbar {
+  opacity: 1;
+}
+
+.mermaid-toolbar-btn {
+  background-color: var(--button-bg);
+  border: 1px solid var(--border-color);
+  color: var(--text-color);
+  border-radius: 4px;
+  padding: 4px 7px;
+  font-size: 13px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 3px;
+  transition: background-color 0.2s ease, color 0.2s ease;
+  white-space: nowrap;
+}
+
+.mermaid-toolbar-btn:hover {
+  background-color: var(--button-hover);
+  color: var(--accent-color);
+}
+
+.mermaid-toolbar-btn:active {
+  background-color: var(--button-active);
+}
+
+.mermaid-toolbar-btn i {
+  font-size: 14px;
+}
+
+/* ========================================
+   MERMAID ZOOM MODAL
+   ======================================== */
+
+#mermaid-zoom-modal {
+  display: none;
+  position: fixed;
+  inset: 0;
+  z-index: 2000;
+  background-color: rgba(0, 0, 0, 0.75);
+  align-items: center;
+  justify-content: center;
+}
+
+#mermaid-zoom-modal.active {
+  display: flex;
+}
+
+.mermaid-modal-content {
+  background-color: var(--bg-color);
+  border: 1px solid var(--border-color);
+  border-radius: 8px;
+  padding: 16px;
+  width: 85vw;
+  height: 85vh;
+  max-width: 85vw;
+  max-height: 85vh;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+@media (max-width: 576px) {
+  .mermaid-modal-content {
+    width: 95vw;
+    height: 90vh;
+    max-width: 95vw;
+    max-height: 90vh;
+    padding: 10px;
+  }
+}
+
+.mermaid-modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.mermaid-modal-header span {
+  font-weight: 600;
+  font-size: 15px;
+  color: var(--text-color);
+}
+
+.mermaid-modal-close {
+  background: none;
+  border: none;
+  color: var(--text-color);
+  font-size: 1.2rem;
+  cursor: pointer;
+  padding: 2px 6px;
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  transition: background-color 0.2s ease;
+}
+
+.mermaid-modal-close:hover {
+  background-color: var(--button-hover);
+}
+
+.mermaid-modal-diagram {
+  overflow: auto;
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 200px;
+  cursor: grab;
+}
+
+.mermaid-modal-diagram.dragging {
+  cursor: grabbing;
+}
+
+.mermaid-modal-diagram svg {
+  transform-origin: center;
+  transition: transform 0.1s ease;
+  max-width: none;
+}
+
+.mermaid-modal-controls {
+  display: flex;
+  justify-content: center;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.mermaid-modal-controls .mermaid-toolbar-btn {
+  opacity: 1;
+}
+
+/* ========================================
+   DOCUMENT TABS & SESSION MANAGEMENT
+   ======================================== */
+
+.tab-bar {
+  display: flex;
+  align-items: center;
+  background-color: var(--header-bg);
+  border-bottom: 1px solid var(--border-color);
+  height: 32px;
+  overflow: visible;  /* ← was: overflow: hidden */
+  flex-shrink: 0;
+  padding: 0 4px;
+  gap: 0;
+  user-select: none;
+  position: relative;
+  z-index: 10;
+}
+
+.tab-list {
+  display: flex;
+  align-items: flex-end;
+  overflow-x: auto;
+  overflow-y: visible;  /* ← was: overflow-y: hidden */
+  flex: 1;
+  height: 100%;
+  scrollbar-width: none;
+  -ms-overflow-style: none;
+}
+
+.tab-list::-webkit-scrollbar {
+  display: none;
+}
+
+.tab-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  height: 32px;
+  padding: 0 8px 0 10px;
+  min-width: 100px;
+  max-width: 180px;
+  background-color: var(--button-bg);
+  border: 1px solid var(--border-color);
+  border-bottom: 1px solid transparent;
+  border-radius: 6px 6px 0 0;
+  cursor: pointer;
+  font-size: 13px;
+  color: var(--text-color);
+  white-space: nowrap;
+  /* overflow: hidden; <-- REMOVE THIS */
+  position: relative;
+  transition: background-color 0.15s ease, color 0.15s ease;
+  flex-shrink: 0;
+  margin-right: 2px;
+  opacity: 0.7;
+}
+
+.tab-item:hover {
+  background-color: var(--button-hover);
+  opacity: 0.9;
+}
+
+.tab-item.active {
+  background-color: var(--bg-color);
+  border-color: var(--border-color);
+  color: var(--accent-color);
+  border-bottom: 1px solid var(--bg-color);
+  opacity: 1;
+  z-index: 2;
+}
+
+.tab-item.unsaved::after {
+  content: '';
+  display: inline-block;
+  width: 6px;
+  height: 6px;
+  background-color: var(--accent-color);
+  border-radius: 50%;
+  flex-shrink: 0;
+  margin-left: 2px;
+}
+
+.tab-title {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  flex: 1;
+  min-width: 0;
+}
+
+.tab-close-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 16px;
+  height: 16px;
+  border-radius: 3px;
+  background: none;
+  border: none;
+  color: var(--text-color);
+  cursor: pointer;
+  padding: 0;
+  font-size: 11px;
+  opacity: 0;
+  flex-shrink: 0;
+  transition: background-color 0.15s ease, opacity 0.15s ease;
+}
+
+.tab-item:hover .tab-close-btn,
+.tab-item.active .tab-close-btn {
+  opacity: 0.6;
+}
+
+.tab-close-btn:hover {
+  background-color: var(--button-active);
+  opacity: 1 !important;
+  color: var(--color-danger-fg, #d73a49);
+}
+
+.tab-new-btn {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  height: 24px;
+  padding: 0 8px;
+  border-radius: 5px;
+  background: none;
+  border: 1px solid var(--border-color);
+  color: var(--text-color);
+  cursor: pointer;
+  font-size: 12px;
+  flex-shrink: 0;
+  margin-left: 6px;
+  align-self: center;
+  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
+}
+
+.tab-new-btn:hover {
+  background-color: rgba(46, 160, 67, 0.1);
+  border-color: var(--accent-color, #2ea043);
+  color: var(--accent-color, #2ea043);
+}
+
+.tab-new-btn:active {
+  background-color: rgba(46, 160, 67, 0.2);
+}
+
+/* Drag-and-drop visual feedback */
+.tab-item.dragging {
+  opacity: 0.4;
+}
+
+.tab-item.drag-over {
+  border-left: 2px solid var(--accent-color);
+}
+
+/* Tab enter animation */
+@keyframes tabSlideIn {
+  from { opacity: 0; transform: translateY(4px); }
+  to   { opacity: 0.7; transform: translateY(0); }
+}
+
+.tab-item {
+  animation: tabSlideIn 0.12s ease forwards;
+}
+
+.tab-item.active {
+  animation: none;
+}
+
+/* Hide tab bar on very small screens — single-file use */
+@media (max-width: 480px) {
+  .tab-bar {
+    display: none;
+  }
+}
+
+/* ========================================
+   TAB OVERFLOW — Scroll Buttons & Fade Indicators
+   ======================================== */
+
+.tab-scroll-btn {
+  display: none;
+  align-items: center;
+  justify-content: center;
+  width: 24px;
+  height: 24px;
+  border-radius: 4px;
+  background: none;
+  border: 1px solid transparent;
+  color: var(--text-color);
+  cursor: pointer;
+  font-size: 14px;
+  flex-shrink: 0;
+  padding: 0;
+  transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
+  z-index: 2;
+  opacity: 0.6;
+}
+
+.tab-scroll-btn:hover {
+  background-color: var(--button-hover);
+  border-color: var(--border-color);
+  opacity: 1;
+}
+
+.tab-scroll-btn:active {
+  background-color: var(--button-active);
+}
+
+/* Show scroll buttons only when overflow exists */
+.tab-bar.has-overflow-left .tab-scroll-left,
+.tab-bar.has-overflow-right .tab-scroll-right {
+  display: flex;
+}
+
+/* Overflow fade indicators — subtle gradient at clipped edges */
+.tab-list::before,
+.tab-list::after {
+  content: '';
+  position: sticky;
+  top: 0;
+  bottom: 0;
+  width: 0;
+  flex-shrink: 0;
+  pointer-events: none;
+  z-index: 3;
+  transition: box-shadow 0.2s ease;
+}
+
+.tab-list::before {
+  left: 0;
+}
+
+.tab-list::after {
+  right: 0;
+}
+
+.tab-bar.has-overflow-left .tab-list::before {
+  box-shadow: 8px 0 12px -4px rgba(0, 0, 0, 0.12);
+}
+
+.tab-bar.has-overflow-right .tab-list::after {
+  box-shadow: -8px 0 12px -4px rgba(0, 0, 0, 0.12);
+}
+
+[data-theme="dark"] .tab-bar.has-overflow-left .tab-list::before {
+  box-shadow: 8px 0 12px -4px rgba(0, 0, 0, 0.35);
+}
+
+[data-theme="dark"] .tab-bar.has-overflow-right .tab-list::after {
+  box-shadow: -8px 0 12px -4px rgba(0, 0, 0, 0.35);
+}
+
+/* ========================================
+   THREE-DOT TAB MENU
+   ======================================== */
+
+.tab-menu-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 22px;
+  height: 22px;
+  border-radius: 3px;
+  background: none;
+  border: none;
+  color: var(--text-color);
+  cursor: pointer;
+  padding: 0;
+  font-size: 14px;
+  font-weight: bold;
+  letter-spacing: 1px;
+  opacity: 0.65;
+  flex-shrink: 0;
+  transition: background-color 0.15s ease, opacity 0.15s ease;
+  position: relative;
+}
+
+/* Touch Hitbox Expansion for Tab Menu Button */
+.tab-menu-btn::before {
+  content: '';
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 48px;
+  height: 48px;
+}
+
+/* Touch-optimized styling for coarser pointers (e.g., smartphones & tablets) */
+@media (pointer: coarse) {
+  .tab-bar {
+    height: 40px !important;
+  }
+  .tab-item {
+    height: 40px !important;
+    font-size: 14px !important;
+    padding: 0 10px 0 12px !important;
+    gap: 8px !important;
+  }
+  .tab-new-btn,
+  .tab-reset-btn {
+    height: 32px !important;
+    font-size: 14px !important;
+    padding: 0 12px !important;
+  }
+  .tab-scroll-btn {
+    width: 32px !important;
+    height: 32px !important;
+    font-size: 18px !important;
+  }
+  .tab-menu-btn {
+    width: 30px !important;
+    height: 30px !important;
+    font-size: 18px !important;
+  }
+  .tab-close-btn {
+    width: 20px !important;
+    height: 20px !important;
+    font-size: 13px !important;
+    opacity: 0.8 !important;
+  }
+}
+
+.tab-item:hover .tab-menu-btn,
+.tab-item.active .tab-menu-btn {
+  opacity: 0.65;
+}
+
+.tab-menu-btn:hover {
+  background-color: var(--button-active);
+  opacity: 1 !important;
+}
+
+.tab-menu-dropdown {
+  display: none;
+  position: fixed;
+  min-width: 130px;
+  background-color: var(--header-bg);
+  border: 1px solid var(--border-color);
+  border-radius: 6px;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+  z-index: 99999;
+  overflow: hidden;
+  flex-direction: column;
+}
+
+.tab-menu-dropdown.open {
+  display: flex;
+}
+
+.tab-menu-item {
+  display: flex;
+  align-items: center;
+  gap: 7px;
+  padding: 7px 12px;
+  background: none;
+  border: none;
+  color: var(--text-color);
+  font-size: 12px;
+  cursor: pointer;
+  text-align: left;
+  transition: background-color 0.12s ease;
+  white-space: nowrap;
+}
+
+.tab-menu-item:hover {
+  background-color: var(--button-hover);
+}
+
+.tab-menu-item-danger {
+  color: var(--color-danger-fg, #d73a49);
+}
+
+.tab-menu-item-danger:hover {
+  background-color: rgba(215, 58, 73, 0.1);
+}
+
+/* ========================================
+   RESET BUTTON
+   ======================================== */
+
+.tab-reset-btn {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  height: 24px;
+  padding: 0 8px;
+  border-radius: 5px;
+  background: none;
+  border: 1px solid var(--border-color);
+  color: var(--text-color);
+  cursor: pointer;
+  font-size: 12px;
+  flex-shrink: 0;
+  margin-left: 6px;
+  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
+}
+
+.tab-reset-btn:hover {
+  background-color: rgba(215, 58, 73, 0.1);
+  border-color: var(--color-danger-fg, #d73a49);
+  color: var(--color-danger-fg, #d73a49);
+}
+
+/* ========================================
+   RESET & RENAME CONFIRMATION MODALS
+   ======================================== */
+
+.reset-modal-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.45);
+  z-index: 2000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.reset-modal-overlay.modal-overlay {
+  opacity: 0;
+  visibility: hidden;
+  transition: opacity 0.2s ease, visibility 0.2s ease;
+}
+
+.reset-modal-overlay.modal-overlay.is-visible {
+  opacity: 1;
+  visibility: visible;
+}
+
+.reset-modal-box {
+  background: var(--header-bg);
+  border: 1px solid var(--border-color);
+  border-radius: 10px;
+  padding: 24px 28px;
+  min-width: 280px;
+  max-width: 360px;
+  box-shadow: 0 8px 32px rgba(0,0,0,0.25);
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.modal-box {
+  max-height: min(85vh, 760px);
+  opacity: 0;
+  transform: translateY(8px);
+  transition: transform 0.2s ease, opacity 0.2s ease;
+}
+
+.reset-modal-overlay.modal-overlay.is-visible .modal-box {
+  opacity: 1;
+  transform: translateY(0);
+}
+
+.modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.modal-header .reset-modal-message {
+  text-align: left;
+  flex: 1;
+}
+
+.modal-close-btn {
+  border: 1px solid var(--border-color);
+  background: var(--button-bg);
+  color: var(--text-color);
+  border-radius: 6px;
+  width: 28px;
+  height: 28px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  transition: background-color 0.15s ease;
+}
+
+.modal-close-btn:hover {
+  background-color: var(--button-hover);
+}
+
+.modal-body {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  max-height: min(60vh, 520px);
+  overflow: auto;
+  padding-right: 4px;
+}
+
+.modal-section {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.modal-section-title {
+  margin: 0;
+  font-size: 0.95rem;
+  font-weight: 600;
+}
+
+.modal-list {
+  margin: 0;
+  padding-left: 1.1rem;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  font-size: 0.85rem;
+}
+
+.modal-list a {
+  color: var(--accent-color);
+  text-decoration: none;
+}
+
+.modal-list a:hover {
+  text-decoration: underline;
+}
+
+.modal-subtext {
+  margin: 0;
+  font-size: 12px;
+  color: var(--text-secondary, #57606a);
+  line-height: 1.4;
+}
+
+.find-replace-meta {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.find-match-count {
+  font-size: 12px;
+  color: var(--text-secondary, #57606a);
+}
+
+.find-replace-nav {
+  display: inline-flex;
+  gap: 6px;
+}
+
+.find-nav-btn {
+  width: 28px;
+  height: 28px;
+  padding: 0;
+}
+
+.about-header {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  flex-wrap: wrap;
+}
+
+.about-logo {
+  width: 64px;
+  height: 64px;
+  border-radius: 12px;
+  border: 1px solid var(--border-color);
+  object-fit: cover;
+}
+
+.about-details {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.about-title {
+  margin: 0;
+  font-size: 1.05rem;
+  font-weight: 600;
+}
+
+.about-description {
+  margin: 0;
+  font-size: 0.85rem;
+  color: var(--text-secondary, #57606a);
+}
+
+.about-meta {
+  margin: 0;
+  font-size: 0.78rem;
+  color: var(--text-secondary, #57606a);
+}
+
+.modal-body kbd {
+  padding: 2px 6px;
+  border-radius: 4px;
+  background-color: var(--button-bg);
+  border: 1px solid var(--border-color);
+  font-size: 0.75rem;
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+}
+
+.reset-modal-box--wide {
+  width: min(92vw, 640px);
+  max-width: 640px;
+}
+
+.reset-modal-message {
+  margin: 0;
+  font-size: 14px;
+  color: var(--text-color);
+  font-weight: 500;
+  text-align: center;
+}
+
+.reset-modal-actions {
+  display: flex;
+  gap: 10px;
+  justify-content: flex-end;
+}
+
+.reset-modal-btn {
+  padding: 6px 16px;
+  border-radius: 6px;
+  border: 1px solid var(--border-color);
+  background: var(--button-bg);
+  color: var(--text-color);
+  font-size: 13px;
+  cursor: pointer;
+  transition: background-color 0.15s ease;
+}
+
+.reset-modal-btn:hover {
+  background-color: var(--button-hover);
+}
+
+.reset-modal-confirm {
+  background-color: var(--color-danger-fg, #d73a49);
+  border-color: var(--color-danger-fg, #d73a49);
+  color: #fff;
+}
+
+.reset-modal-confirm:hover {
+  background-color: #b02a37;
+  border-color: #b02a37;
+}
+
+/* ========================================
+   PDF EXPORT PROGRESS MODAL
+   ======================================== */
+
+.pdf-progress-overlay {
+  position: fixed;
+  inset: 0;
+  z-index: 2600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(0, 0, 0, 0.45);
+  padding: 20px;
+}
+
+.pdf-progress-modal {
+  width: min(92vw, 420px);
+  background: var(--header-bg);
+  border: 1px solid var(--border-color);
+  border-radius: 8px;
+  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.28);
+  color: var(--text-color);
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  padding: 20px;
+}
+
+.pdf-progress-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.pdf-progress-title {
+  margin: 0;
+  font-size: 15px;
+  font-weight: 600;
+}
+
+.pdf-progress-percent {
+  font-size: 28px;
+  font-weight: 600;
+  line-height: 1;
+}
+
+.pdf-progress-track {
+  width: 100%;
+  height: 10px;
+  background: var(--button-bg);
+  border: 1px solid var(--border-color);
+  border-radius: 999px;
+  overflow: hidden;
+}
+
+.pdf-progress-fill {
+  width: 0%;
+  height: 100%;
+  background: var(--accent-color);
+  border-radius: inherit;
+  transition: width 0.18s ease;
+}
+
+.pdf-progress-details {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  font-size: 12px;
+  color: var(--text-secondary);
+}
+
+.pdf-progress-detail {
+  display: flex;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.pdf-progress-detail strong {
+  color: var(--text-color);
+  font-weight: 600;
+}
+
+.pdf-progress-actions {
+  display: flex;
+  justify-content: flex-end;
+}
+
+.tool-button.pdf-export-loading,
+.mobile-menu-item.pdf-export-loading {
+  pointer-events: none;
+}
+/* ========================================
+   RESET MODAL FORM FIELDS
+   ======================================== */
+
+.reset-modal-field {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  text-align: left;
+}
+
+.reset-modal-field-group {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.reset-modal-label {
+  font-size: 12px;
+  color: var(--text-secondary, #57606a);
+  font-weight: 600;
+}
+
+.reset-modal-toggle-group {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.reset-modal-option {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 13px;
+  color: var(--text-color);
+}
+
+.reset-modal-option input {
+  margin: 0;
+}
+
+/* ========================================
+   RENAME MODAL INPUT
+   ======================================== */
+
+.rename-modal-input {
+  width: 100%;
+  padding: 7px 10px;
+  border-radius: 6px;
+  border: 1px solid var(--border-color);
+  background: var(--bg-color);
+  color: var(--text-color);
+  font-size: 13px;
+  outline: none;
+  box-sizing: border-box;
+}
+
+.rename-modal-input:focus {
+  border-color: var(--accent-color);
+}
+
+/* ========================================
+   TOOLBAR POPUP PANELS
+   ======================================== */
+
+.reset-modal-box--xl {
+  width: min(94vw, 980px);
+  max-width: 980px;
+}
+
+.modal-empty {
+  margin: 0;
+  font-size: 12px;
+  color: var(--text-secondary, #57606a);
+  text-align: center;
+}
+
+.emoji-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+  gap: 12px;
+  max-height: min(52vh, 440px);
+  overflow: auto;
+  padding: 4px;
+}
+
+.symbol-grid {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  max-height: min(52vh, 440px);
+  overflow: auto;
+  padding: 4px 2px;
+}
+
+.symbol-section-title {
+  margin: 0;
+  font-size: 11px;
+  text-transform: uppercase;
+  letter-spacing: 0.08em;
+  color: var(--text-secondary, #57606a);
+}
+
+.symbol-section-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+  gap: 12px;
+}
+
+.alert-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+  gap: 12px;
+  max-height: min(45vh, 360px);
+  overflow: auto;
+  padding: 2px;
+}
+
+.emoji-item,
+.symbol-item,
+.alert-option {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  align-items: center;
+  border: 1px solid var(--border-color);
+  border-radius: 10px;
+  padding: 10px;
+  background: var(--bg-color);
+  color: var(--text-color);
+  cursor: pointer;
+  transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
+}
+
+.emoji-item:focus-visible,
+.symbol-item:focus-visible,
+.alert-option:focus-visible {
+  outline: 2px solid var(--accent-color);
+  outline-offset: 2px;
+}
+
+.emoji-item.is-selected,
+.symbol-item.is-selected,
+.alert-option.is-selected {
+  border-color: var(--accent-color);
+  box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.2);
+  background-color: rgba(88, 166, 255, 0.08);
+}
+
+.emoji-preview {
+  width: 36px;
+  height: 36px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.emoji-preview img {
+  width: 32px;
+  height: 32px;
+}
+
+.emoji-shortcode {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  color: var(--text-secondary, #57606a);
+  text-align: center;
+}
+
+.emoji-copy-btn,
+.symbol-copy-btn {
+  border: none;
+  background: transparent;
+  color: var(--text-secondary, #57606a);
+  cursor: pointer;
+  padding: 2px;
+  border-radius: 4px;
+}
+
+.emoji-copy-btn:hover,
+.symbol-copy-btn:hover {
+  color: var(--text-color);
+  background: var(--button-hover);
+}
+
+.emoji-copy-btn.is-copied,
+.symbol-copy-btn.is-copied {
+  color: var(--accent-color);
+}
+
+.symbol-preview {
+  font-size: 28px;
+  line-height: 1;
+}
+
+.symbol-code {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  color: var(--text-secondary, #57606a);
+}
+
+.alert-option {
+  align-items: stretch;
+  text-align: left;
+  padding: 12px;
+}
+
+.alert-preview {
+  margin: 0;
+}
+
+.alert-preview .markdown-alert {
+  padding: 0.5rem 0.9rem;
+  border-left: 0.25em solid;
+  border-radius: 0.375rem;
+}
+
+.alert-preview .markdown-alert-title {
+  margin: 0 0 6px;
+  font-weight: 600;
+  line-height: 1.25;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.alert-preview .markdown-alert-icon {
+  display: inline-flex;
+  width: 16px;
+  height: 16px;
+}
+
+.alert-preview .markdown-alert-icon svg {
+  width: 16px;
+  height: 16px;
+  fill: currentColor;
+}
+
+.alert-preview .markdown-alert > *:not(.markdown-alert-title) {
+  color: var(--text-color);
+}
+
+.alert-preview .markdown-alert-note {
+  color: #0969da;
+  border-left-color: #0969da;
+  background-color: #ddf4ff;
+}
+
+.alert-preview .markdown-alert-tip {
+  color: #1a7f37;
+  border-left-color: #1a7f37;
+  background-color: #dafbe1;
+}
+
+.alert-preview .markdown-alert-important {
+  color: #8250df;
+  border-left-color: #8250df;
+  background-color: #fbefff;
+}
+
+.alert-preview .markdown-alert-warning {
+  color: #9a6700;
+  border-left-color: #9a6700;
+  background-color: #fff8c5;
+}
+
+.alert-preview .markdown-alert-caution {
+  color: #cf222e;
+  border-left-color: #cf222e;
+  background-color: #ffebe9;
+}
+
+[data-theme="dark"] .alert-preview .markdown-alert-note {
+  color: #4493f8;
+  border-left-color: #4493f8;
+  background-color: rgba(31, 111, 235, 0.15);
+}
+
+[data-theme="dark"] .alert-preview .markdown-alert-tip {
+  color: #3fb950;
+  border-left-color: #3fb950;
+  background-color: rgba(35, 134, 54, 0.15);
+}
+
+[data-theme="dark"] .alert-preview .markdown-alert-important {
+  color: #ab7df8;
+  border-left-color: #ab7df8;
+  background-color: rgba(137, 87, 229, 0.15);
+}
+
+[data-theme="dark"] .alert-preview .markdown-alert-warning {
+  color: #d29922;
+  border-left-color: #d29922;
+  background-color: rgba(210, 153, 34, 0.18);
+}
+
+[data-theme="dark"] .alert-preview .markdown-alert-caution {
+  color: #f85149;
+  border-left-color: #f85149;
+  background-color: rgba(248, 81, 73, 0.18);
+}
+
+.github-import-error {
+  margin: 0;
+  font-size: 12px;
+  color: var(--color-danger-fg, #d73a49);
+  text-align: left;
+  line-height: 1.5;
+}
+
+.github-import-error.is-info {
+  color: var(--text-secondary, #57606a);
+}
+
+#github-import-modal .reset-modal-box {
+  width: 60vw;
+  max-width: 60vw;
+  min-width: 340px;
+  padding: 30px 34px;
+  gap: 16px;
+  box-shadow: 0 20px 48px rgba(0, 0, 0, 0.22);
+}
+
+#github-import-modal .reset-modal-message {
+  font-size: 18px;
+  line-height: 1.35;
+  text-align: left;
+}
+
+#github-import-url,
+#github-import-file-select {
+  min-height: 46px;
+  padding: 10px 12px;
+  font-size: 15px;
+}
+
+#github-import-file-select {
+  min-height: 180px;
+}
+
+.github-import-tree {
+  max-height: 420px;
+  overflow: auto;
+  border: 1px solid var(--border-color);
+  border-radius: 10px;
+  padding: 12px;
+  background: var(--bg-color);
+}
+
+.github-import-selection-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 10px 12px;
+  border: 1px solid var(--border-color);
+  border-radius: 8px;
+  background: var(--button-bg);
+}
+
+.github-import-selected-count {
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--text-color);
+}
+
+.github-import-tree ul {
+  list-style: none;
+  margin: 0;
+  padding-left: 18px;
+}
+
+.github-import-tree > ul {
+  padding-left: 4px;
+}
+
+.github-import-tree li {
+  margin: 2px 0;
+}
+
+.github-tree-folder-label {
+  display: inline-block;
+  font-size: 14px;
+  color: var(--text-secondary, #57606a);
+  margin-bottom: 4px;
+}
+
+.github-tree-file-btn {
+  border: 0;
+  background: transparent;
+  color: var(--text-color);
+  cursor: pointer;
+  padding: 6px 8px;
+  border-radius: 6px;
+  text-align: left;
+  width: 100%;
+  font-size: 14px;
+}
+
+.github-tree-file-btn:hover,
+.github-tree-file-btn:focus-visible {
+  background: var(--button-hover);
+  outline: none;
+}
+
+.github-tree-file-btn.is-selected {
+  background: rgba(56, 139, 253, 0.14);
+  color: var(--accent-color);
+}
+
+#github-import-modal .reset-modal-actions {
+  gap: 12px;
+}
+
+#github-import-modal .reset-modal-btn {
+  min-height: 42px;
+  padding: 9px 18px;
+  font-size: 14px;
+}
+
+@media (max-width: 576px) {
+  #github-import-modal .reset-modal-box {
+    width: 95vw;
+    max-width: 95vw;
+    min-width: 0;
+    padding: 20px;
+    gap: 14px;
+  }
+
+  .github-import-selection-toolbar {
+    flex-direction: column;
+    align-items: stretch;
+  }
+
+  #github-import-modal .reset-modal-message {
+    font-size: 16px;
+  }
+
+  #github-import-modal .reset-modal-actions {
+    flex-direction: column-reverse;
+  }
+
+  #github-import-modal .reset-modal-btn {
+    width: 100%;
+  }
+}
+
+.frontmatter-table {
+  border-collapse: collapse;
+  margin-bottom: 1.5em;
+  font-size: 0.9em;
+  width: auto;
+  max-width: 100%;
+}
+
+.frontmatter-table th,
+.frontmatter-table td {
+  border: 1px solid var(--border-color);
+  padding: 6px 13px;
+  vertical-align: top;
+  color: var(--text-color);
+}
+
+.frontmatter-table tr:nth-child(odd) th,
+.frontmatter-table tr:nth-child(odd) td {
+  background-color: var(--table-bg);
+}
+
+.frontmatter-table tr:nth-child(even) th,
+.frontmatter-table tr:nth-child(even) td {
+  background-color: var(--editor-bg);
+}
+
+.frontmatter-table th {
+  font-weight: 600;
+  text-align: right;
+  white-space: nowrap;
+  vertical-align: middle;
+}
+
+.frontmatter-table td {
+  text-align: left;
+}
+
+.fm-complex {
+  margin: 0;
+  padding: 4px 6px;
+  font-size: 0.8em;
+  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
+  white-space: pre-wrap;
+  word-break: break-word;
+  background: transparent;
+  border: none;
+  color: var(--text-color);
+}
+
+.fm-tag {
+  display: inline-block;
+  padding: 2px 8px;
+  margin: 2px 3px 2px 0;
+  border: 1px solid var(--border-color);
+  border-radius: 2em;
+  font-size: 0.8em;
+  font-weight: 500;
+  color: var(--accent-color);
+  background-color: var(--button-bg);
+  white-space: nowrap;
+}
+
+/* ========================================
+   RTL SUPPORT
+   ======================================== */
+
+[dir="rtl"] body {
+  direction: rtl;
+}
+
+[dir="rtl"] .editor-pane {
+  padding-left: 0px;
+  padding-right: 20px;
+  border-right: none;
+  border-left: 1px solid var(--border-color);
+}
+
+[dir="rtl"] #markdown-editor,
+[dir="rtl"] .markdown-body {
+  direction: rtl;
+  text-align: right;
+}
+
+[dir="rtl"] .markdown-body pre,
+[dir="rtl"] .markdown-body code,
+[dir="rtl"] .fm-complex {
+  direction: ltr;
+  text-align: left;
+}
+
+[dir="rtl"] .line-numbers {
+  left: auto;
+  right: 20px;
+  padding: 10px 0 10px 8px;
+  text-align: left;
+  border-right: none;
+  border-left: 1px solid var(--border-color);
+}
+
+[dir="rtl"] #markdown-editor {
+  padding-left: 10px;
+  padding-right: calc(10px + var(--line-number-gutter));
+}
+
+[dir="rtl"] .editor-highlight-layer {
+  inset: 20px calc(20px + var(--line-number-gutter)) 20px 0;
+}
+
+[dir="rtl"] .mobile-menu-item,
+[dir="rtl"] .tab-menu-item,
+[dir="rtl"] .modal-header .reset-modal-message,
+[dir="rtl"] .reset-modal-field,
+[dir="rtl"] .alert-option,
+[dir="rtl"] .github-import-error,
+[dir="rtl"] #github-import-modal .reset-modal-message,
+[dir="rtl"] .github-tree-file-btn,
+[dir="rtl"] .frontmatter-table td {
+  text-align: right;
+}
+
+[dir="rtl"] .github-import-tree ul {
+  padding-left: 0;
+  padding-right: 18px;
+}
+
+[dir="rtl"] .github-import-tree > ul {
+  padding-right: 4px;
+}
+
+[dir="rtl"] .markdown-body .markdown-alert,
+[dir="rtl"] .alert-preview .markdown-alert {
+  border-left: 0;
+  border-right: 0.25em solid currentColor;
+}
+
+/* ============================================
+   SHARE MODAL
+   ============================================ */
+
+.share-modal-description {
+  font-size: 13px;
+  color: var(--text-secondary, #57606a);
+  margin: 0;
+}
+
+.share-mode-cards {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.share-mode-card {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 12px 14px;
+  border-radius: 8px;
+  border: 1px solid var(--border-color);
+  background: var(--bg-color);
+  cursor: pointer;
+  transition: border-color 0.15s ease, background-color 0.15s ease;
+  user-select: none;
+}
+
+.share-mode-card:hover {
+  border-color: var(--accent-color);
+  background: var(--button-hover);
+}
+
+.share-mode-card.is-selected {
+  border-color: var(--accent-color);
+  background: color-mix(in srgb, var(--accent-color) 8%, transparent);
+}
+
+.share-mode-card input[type="radio"] {
+  display: none;
+}
+
+.share-card-icon {
+  font-size: 18px;
+  width: 28px;
+  text-align: center;
+  color: var(--text-secondary, #57606a);
+  flex-shrink: 0;
+}
+
+.share-mode-card.is-selected .share-card-icon {
+  color: var(--accent-color);
+}
+
+.share-card-body {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  flex: 1;
+  min-width: 0;
+}
+
+.share-card-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--text-color);
+}
+
+.share-card-desc {
+  font-size: 12px;
+  color: var(--text-secondary, #57606a);
+}
+
+.share-card-check {
+  width: 18px;
+  text-align: center;
+  color: var(--accent-color);
+  opacity: 0;
+  transition: opacity 0.15s ease;
+  flex-shrink: 0;
+}
+
+.share-mode-card.is-selected .share-card-check {
+  opacity: 1;
+}
+
+.share-url-row {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.share-url-input {
+  flex: 1;
+  font-size: 12px;
+  font-family: var(--font-mono, monospace);
+  color: var(--text-secondary, #57606a);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.share-copy-btn {
+  flex-shrink: 0;
+  padding: 6px 10px;
+}
+
+.share-copy-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.share-modal-notice {
+  font-size: 11px;
+  color: var(--text-secondary, #57606a);
+  margin: 0;
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+
+/* ==========================================================================
+   Multilingual & CJK Optimization styles added by Aegis SEO agency
+   ========================================================================== */
+.lang-select-item {
+  display: flex !important;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  transition: background-color 0.2s ease, padding-left 0.2s ease;
+}
+
+.lang-select-item:hover {
+  padding-left: 12px;
+}
+
+.lang-select-item.active {
+  background-color: var(--accent-color) !important;
+  color: #ffffff !important;
+  font-weight: 600;
+}
+
+/* Adjust CJK text layout for maximum readability inside the preview pane */
+html[lang="zh"] .markdown-body,
+html[lang="ja"] .markdown-body,
+html[lang="ko"] .markdown-body {
+  line-height: 1.75 !important;
+  letter-spacing: 0.03em;
+  word-break: keep-all;
+  overflow-wrap: break-word;
+  text-align: justify;
+}
+
+/* Specific heading spacing improvements for CJK characters */
+html[lang="zh"] .markdown-body h1, html[lang="zh"] .markdown-body h2, html[lang="zh"] .markdown-body h3,
+html[lang="ja"] .markdown-body h1, html[lang="ja"] .markdown-body h2, html[lang="ja"] .markdown-body h3,
+html[lang="ko"] .markdown-body h1, html[lang="ko"] .markdown-body h2, html[lang="ko"] .markdown-body h3 {
+  font-weight: 700;
+  letter-spacing: 0.02em;
+  margin-top: 1.4em;
+  margin-bottom: 0.6em;
+}
+
+/* Smooth fade and scale transition for dropdown active states */
+.dropdown-menu {
+  opacity: 0;
+  transform: translateY(8px) scale(0.98);
+  display: block;
+  visibility: hidden;
+  transition: opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1), transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), visibility 0.2s;
+}
+
+.dropdown-menu.show {
+  opacity: 1;
+  transform: translateY(0) scale(1);
+  visibility: visible;
+}
+
+/* ========================================
+   FIND & REPLACE FLOATING PANEL DESIGN
+   ======================================== */
+
+.find-replace-panel {
+  position: fixed;
+  top: 100px;
+  right: 20px;
+  width: 340px;
+  background-color: var(--fr-bg);
+  border: 1px solid var(--fr-border);
+  border-radius: 12px;
+  box-shadow: var(--fr-shadow);
+  z-index: 1050;
+  display: flex;
+  flex-direction: column;
+  backdrop-filter: blur(10px);
+  -webkit-backdrop-filter: blur(10px);
+  transition: opacity 0.2s ease, transform 0.2s ease;
+  user-select: none;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
+}
+
+.find-replace-panel.docked {
+  position: relative;
+  top: 0 !important;
+  right: 0 !important;
+  height: 100%;
+  border-radius: 0;
+  border-top: none;
+  border-bottom: none;
+  border-right: none;
+  border-left: 1px solid var(--fr-border);
+  box-shadow: none;
+  backdrop-filter: none;
+  z-index: 10;
+  flex-shrink: 0;
+}
+
+.find-replace-panel.docked #find-replace-reset,
+.find-replace-panel.docked #find-replace-reset-footer {
+  display: none !important;
+}
+
+.find-replace-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 8px 12px;
+  border-bottom: 1px solid var(--fr-border);
+  cursor: move;
+  background-color: var(--header-bg);
+  border-top-left-radius: 11px;
+  border-top-right-radius: 11px;
+}
+
+.find-replace-panel.docked .find-replace-header {
+  cursor: default;
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+
+.find-replace-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--text-color);
+}
+
+.find-replace-header-actions {
+  display: flex;
+  gap: 4px;
+}
+
+.panel-icon-btn {
+  background: none;
+  border: none;
+  color: var(--text-color);
+  font-size: 12px;
+  cursor: pointer;
+  padding: 2px 6px;
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: background-color 0.15s ease;
+}
+
+.panel-icon-btn:hover {
+  background-color: var(--button-hover);
+}
+
+.find-replace-body {
+  padding: 12px;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.find-replace-field-row {
+  display: flex;
+  flex-direction: column;
+  position: relative;
+}
+
+.find-input-container, .replace-input-container {
+  display: flex;
+  align-items: center;
+  border: 1px solid var(--fr-border);
+  border-radius: 6px;
+  background-color: var(--bg-color);
+  padding: 2px 4px;
+  width: 100%;
+}
+
+.find-input-container:focus-within, .replace-input-container:focus-within {
+  border-color: var(--accent-color);
+  box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.15);
+}
+
+.find-input-field {
+  flex: 1;
+  border: none;
+  background: transparent;
+  color: var(--text-color);
+  font-size: 13px;
+  padding: 4px 6px;
+  outline: none;
+  width: 50%;
+}
+
+.find-options-group {
+  display: flex;
+  gap: 2px;
+}
+
+.find-option-btn {
+  background: none;
+  border: none;
+  color: var(--text-secondary);
+  font-size: 11px;
+  font-weight: 600;
+  cursor: pointer;
+  padding: 2px 5px;
+  border-radius: 4px;
+  transition: background-color 0.12s ease, color 0.12s ease;
+  min-width: 22px;
+  height: 22px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.find-option-btn:hover {
+  background-color: var(--button-hover);
+  color: var(--text-color);
+}
+
+.find-option-btn.active {
+  background-color: var(--fr-btn-active-bg);
+  color: var(--fr-btn-active);
+}
+
+.find-error-drawer {
+  background-color: var(--fr-error-bg);
+  border: 1px solid var(--fr-error-border);
+  border-radius: 6px;
+  padding: 6px 10px;
+  font-size: 11px;
+  color: var(--fr-text-danger);
+  line-height: 1.3;
+}
+
+.find-replace-meta-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 2px 4px;
+}
+
+.find-match-count {
+  font-size: 12px;
+  color: var(--text-secondary);
+}
+
+.find-nav-group {
+  display: flex;
+  gap: 4px;
+}
+
+.find-nav-arrow-btn {
+  background: none;
+  border: 1px solid var(--fr-border);
+  color: var(--text-color);
+  font-size: 12px;
+  cursor: pointer;
+  padding: 2px 8px;
+  border-radius: 4px;
+  transition: background-color 0.15s ease;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.find-nav-arrow-btn:hover:not(:disabled) {
+  background-color: var(--button-hover);
+}
+
+.find-nav-arrow-btn:disabled {
+  opacity: 0.4;
+  cursor: not-allowed;
+}
+
+.find-drawer-toggle-row {
+  border-top: 1px solid var(--fr-border);
+  margin-top: 4px;
+  padding-top: 6px;
+}
+
+.drawer-toggle-btn {
+  background: none;
+  border: none;
+  color: var(--text-secondary);
+  font-size: 12px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  padding: 2px 4px;
+  border-radius: 4px;
+  transition: color 0.15s ease;
+}
+
+.drawer-toggle-btn:hover {
+  color: var(--text-color);
+}
+
+.find-replace-drawer-content {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  padding: 4px 6px;
+  border-top: 1px dashed var(--fr-border);
+  margin-top: 2px;
+  padding-top: 8px;
+}
+
+.drawer-field {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.drawer-field.check-field {
+  flex-direction: row;
+  align-items: center;
+  gap: 6px;
+  padding: 2px 0;
+}
+
+.drawer-label {
+  font-size: 11px;
+  font-weight: 600;
+  color: var(--text-secondary);
+}
+
+.drawer-select {
+  width: 100%;
+  padding: 4px 6px;
+  border-radius: 4px;
+  border: 1px solid var(--fr-border);
+  background-color: var(--bg-color);
+  color: var(--text-color);
+  font-size: 12px;
+  outline: none;
+}
+
+.drawer-checkbox {
+  margin: 0;
+  cursor: pointer;
+}
+
+.drawer-label-checkbox {
+  font-size: 12px;
+  color: var(--text-color);
+  cursor: pointer;
+}
+
+.find-replace-actions-footer {
+  display: flex;
+  gap: 6px;
+  padding: 8px 12px 12px 12px;
+  border-top: 1px solid var(--fr-border);
+  background-color: var(--header-bg);
+  border-bottom-left-radius: 11px;
+  border-bottom-right-radius: 11px;
+}
+
+.find-replace-panel.docked .find-replace-actions-footer {
+  border-bottom-left-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+.fr-action-btn {
+  flex: 1;
+  padding: 6px 8px;
+  font-size: 12px;
+  font-weight: 500;
+  border-radius: 6px;
+  border: 1px solid var(--fr-border);
+  background-color: var(--button-bg);
+  color: var(--text-color);
+  cursor: pointer;
+  transition: background-color 0.15s ease, border-color 0.15s ease;
+  text-align: center;
+}
+
+.fr-action-btn:hover:not(:disabled) {
+  background-color: var(--button-hover);
+}
+
+.fr-action-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.fr-action-btn.secondary {
+  max-width: 60px;
+}
+
+/* ========================================
+   DIFF PREVIEW CONTAINER
+   ======================================== */
+.diff-preview-body {
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.diff-container {
+  border: 1px solid var(--fr-border);
+  border-radius: 8px;
+  background-color: var(--bg-color);
+  max-height: 400px;
+  overflow: auto;
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+  font-size: 12px;
+  line-height: 1.5;
+}
+
+.diff-line {
+  display: flex;
+  padding: 1px 8px;
+}
+
+.diff-line.addition {
+  background-color: rgba(46, 160, 67, 0.15);
+  color: #3fb950;
+}
+
+.diff-line.deletion {
+  background-color: rgba(248, 81, 73, 0.15);
+  color: #f85149;
+}
+
+.diff-line.context {
+  color: var(--text-secondary);
+}
+
+.diff-line-num {
+  width: 40px;
+  flex-shrink: 0;
+  text-align: right;
+  padding-right: 12px;
+  border-right: 1px solid var(--fr-border);
+  user-select: none;
+  opacity: 0.5;
+}
+
+.diff-line-content {
+  padding-left: 12px;
+  white-space: pre-wrap;
+  word-break: break-all;
+}
+
+/* ========================================
+   DOCK LAYOUT CODES
+   ======================================== */
+.content-container {
+  display: flex;
+  flex: 1;
+  overflow: hidden;
+  position: relative;
+}
+
+.editor-dock-wrapper {
+  display: flex;
+  flex: 1;
+  overflow: hidden;
+  position: relative;
+}
+
+.editor-pane-inner {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  position: relative;
+  overflow: hidden;
+}
+
+/* ========================================
+   MOBILE & TABLET FIND PANEL RESPONSIVE FIXES
+   ======================================== */
+@media (max-width: 1079px) {
+  #find-replace-dock {
+    display: none !important;
+  }
+}
+
+@media (max-width: 768px) {
+  /* Prevent full screen expansion of floating panel on small mobile viewports */
+  .find-replace-panel {
+    width: calc(100% - 24px) !important;
+    right: 12px !important;
+    left: 12px !important;
+    top: 80px !important;
+  }
+}
+
+/* ========================================
+   SKELETON LOADING SHIMMER SYSTEM
+   ======================================== */
+.skeleton-placeholder {
+  display: block;
+  background-color: var(--skeleton-bg);
+  border-radius: 6px;
+  position: relative;
+  overflow: hidden;
+  /* PERF-017: Removed skeleton-pulse; shimmer-only is sufficient and halves GPU compositing layers */
+  /* animation: skeleton-pulse 2.2s cubic-bezier(0.4, 0, 0.2, 1) infinite alternate; */
+}
+
+.skeleton-placeholder::after {
+  content: "";
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  transform: translateX(-100%);
+  background-image: linear-gradient(
+    90deg,
+    rgba(255, 255, 255, 0) 0%,
+    var(--skeleton-glow) 50%,
+    rgba(255, 255, 255, 0) 100%
+  );
+  animation: skeleton-shimmer 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
+}
+
+@keyframes skeleton-shimmer {
+  100% {
+    transform: translateX(100%);
+  }
+}
+
+@keyframes skeleton-pulse {
+  0%, 100% {
+    opacity: 1;
+  }
+  50% {
+    opacity: 0.82;
+  }
+}
+
+.skeleton-circle {
+  width: 32px;
+  height: 32px;
+  border-radius: 50%;
+  margin: 0 auto;
+}
+
+.skeleton-text {
+  height: 12px;
+  width: 80%;
+  margin: 4px auto;
+  border-radius: 3px;
+}
+
+.skeleton-tree-folder {
+  height: 16px;
+  width: 140px;
+  margin: 6px 0;
+  display: inline-block;
+}
+
+.skeleton-tree-file {
+  height: 14px;
+  width: 180px;
+  margin: 4px 0 4px 12px;
+  display: inline-block;
+}
+
+/* Screen reader accessibility utility */
+.visually-hidden {
+  position: absolute !important;
+  width: 1px !important;
+  height: 1px !important;
+  padding: 0 !important;
+  margin: -1px !important;
+  overflow: hidden !important;
+  clip: rect(0, 0, 0, 0) !important;
+  clip-path: inset(50%) !important;
+  white-space: nowrap !important;
+  border: 0 !important;
+}
+
+/* Article skeleton layout structures */
+.skeleton-title {
+  height: 28px;
+  width: 35%;
+  margin-bottom: 24px;
+  border-radius: 8px;
+}
+
+.skeleton-subtitle {
+  height: 20px;
+  width: 20%;
+  margin-bottom: 18px;
+  margin-top: 32px;
+  border-radius: 6px;
+}
+
+.skeleton-line {
+  height: 14px;
+  margin-bottom: 12px;
+  border-radius: 6px;
+}
+
+/* Symmetrical dynamic widths */
+.skeleton-w90 { width: 90%; }
+.skeleton-w92 { width: 92%; }
+.skeleton-w88 { width: 88%; }
+.skeleton-w85 { width: 85%; }
+.skeleton-w60 { width: 60%; }
+.skeleton-w45 { width: 45%; }
+
+/* Editor pane skeleton overlay */
+.editor-skeleton {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 30px 24px 24px calc(24px + var(--line-number-gutter));
+  z-index: 10;
+  pointer-events: none;
+  background: var(--editor-bg);
+  box-sizing: border-box;
+  overflow: hidden;
+  transition: opacity 0.3s ease;
+}
+
+.editor-pane:not(.is-loading) .editor-skeleton {
+  display: none;
+}
+
+.editor-pane.is-loading textarea {
+  opacity: 0; /* Completely hide editor content while initial bootstrap skeleton runs */
+}
+
+/* Preview pane skeleton container */
+.skeleton-preview-container {
+  display: block;
+  width: 100%;
+  box-sizing: border-box;
+  padding: 10px 4px;
+  background: transparent;
+  transition: opacity 0.3s ease;
+}
+
+/* Mermaid compilation loading states */
+.mermaid-container.is-loading {
+  min-height: 180px;
+  background-color: var(--skeleton-bg);
+  border-radius: 8px;
+  border: 1px solid var(--border-color);
+  position: relative;
+  overflow: hidden;
+  animation: skeleton-pulse 2.2s cubic-bezier(0.4, 0, 0.2, 1) infinite alternate;
+}
+
+.mermaid-container.is-loading .mermaid {
+  opacity: 0; /* Hide raw chart source code during compile */
+}
+
+.mermaid-container.is-loading::after {
+  content: "";
+  position: absolute;
+  inset: 0;
+  transform: translateX(-100%);
+  background-image: linear-gradient(
+    90deg,
+    rgba(255, 255, 255, 0) 0%,
+    var(--skeleton-glow) 50%,
+    rgba(255, 255, 255, 0) 100%
+  );
+  animation: skeleton-shimmer 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
+}
+
+/* Accessibility: respect user's motion preferences */
+@media (prefers-reduced-motion: reduce) {
+  .skeleton-placeholder,
+  .skeleton-placeholder::after,
+  .mermaid-container.is-loading,
+  .mermaid-container.is-loading::after {
+    animation: none;
+  }
+  .drag-overlay-inner {
+    animation: none;
+  }
+  .tab-item-new {
+    animation: none;
+  }
+  body,
+  .app-header,
+  .editor-pane,
+  .preview-pane,
+  .tool-button,
+  .markdown-tool-btn {
+    transition: none;
+  }
+}
+

+ 116 - 0
sw.js

@@ -0,0 +1,116 @@
+const CACHE_NAME = 'markdown-viewer-cache-v3.7.4';
+
+// PERF-011: Split precache into critical (local files) and lazy (CDN libraries)
+// Critical assets are precached during SW install for instant offline startup
+const CRITICAL_ASSETS = [
+  './',
+  './index.html',
+  './script.js',
+  './preview-worker.js',
+  './styles.css',
+  './sample.md',
+  './assets/icon.jpg',
+  './manifest.json'
+];
+
+// CDN assets are cached lazily on first use via runtime cache-first strategy
+// This prevents the SW install from downloading ~5.4 MB of CDN resources upfront
+const CDN_ORIGINS = [
+  'cdnjs.cloudflare.com',
+  'cdn.jsdelivr.net'
+];
+
+const NETWORK_FIRST_LOCAL_PATHS = new Set([
+  '/',
+  '/index.html',
+  '/script.js',
+  '/preview-worker.js',
+  '/styles.css',
+  '/sw.js'
+]);
+
+self.addEventListener('install', event => {
+  event.waitUntil(
+    caches.open(CACHE_NAME)
+      .then(cache => cache.addAll(CRITICAL_ASSETS))
+      .then(() => self.skipWaiting())
+  );
+});
+
+self.addEventListener('activate', event => {
+  event.waitUntil(
+    caches.keys().then(keys => Promise.all(
+      keys.map(key => {
+        if (key !== CACHE_NAME) {
+          return caches.delete(key);
+        }
+      })
+    )).then(() => self.clients.claim())
+  );
+});
+
+self.addEventListener('fetch', event => {
+  const url = new URL(event.request.url);
+  const isLocal = url.origin === self.location.origin;
+  const isCDN = CDN_ORIGINS.some(origin => url.hostname.includes(origin));
+
+  if (isLocal) {
+    const localPath = url.pathname.endsWith('/') ? '/' : url.pathname;
+    const shouldUseNetworkFirst =
+      event.request.mode === 'navigate' || NETWORK_FIRST_LOCAL_PATHS.has(localPath);
+
+    if (shouldUseNetworkFirst) {
+      event.respondWith(
+        caches.open(CACHE_NAME).then(cache => {
+          return fetch(event.request).then(networkResponse => {
+            if (networkResponse && networkResponse.status === 200) {
+              cache.put(event.request, networkResponse.clone());
+            }
+            return networkResponse;
+          }).catch(() => cache.match(event.request));
+        })
+      );
+      return;
+    }
+
+    // Stale-While-Revalidate strategy for non-code local assets
+    event.respondWith(
+      caches.open(CACHE_NAME).then(cache => {
+        return cache.match(event.request).then(cachedResponse => {
+          const fetchPromise = fetch(event.request).then(networkResponse => {
+            if (networkResponse && networkResponse.status === 200) {
+              cache.put(event.request, networkResponse.clone());
+            }
+            return networkResponse;
+          }).catch(err => {
+            console.warn('Background fetch failed for:', event.request.url, err);
+          });
+          return cachedResponse || fetchPromise;
+        });
+      })
+    );
+  } else if (isCDN) {
+    // Cache-First strategy for stable third-party CDN libraries
+    // PERF-011: CDN resources are cached on first use (lazy) rather than precached
+    event.respondWith(
+      caches.match(event.request)
+        .then(cachedResponse => {
+          if (cachedResponse) {
+            return cachedResponse;
+          }
+          return fetch(event.request).then(response => {
+            if (response && response.status === 200) {
+              const responseToCache = response.clone();
+              caches.open(CACHE_NAME).then(cache => {
+                cache.put(event.request, responseToCache);
+              });
+            }
+            return response;
+          });
+        })
+    );
+  } else {
+    // Network-only for non-CDN external requests
+    event.respondWith(fetch(event.request));
+  }
+});

+ 194 - 0
wiki/Configuration.md

@@ -0,0 +1,194 @@
+# System & Build Configuration
+
+This page details the configuration variables, local storage structures, container files, and desktop configuration parameters for **Markdown Viewer** (v3.7.4).
+
+---
+
+## Table of Contents
+
+- [Client-Side LocalStorage Keys](#client-side-localstorage-keys)
+- [CDN Library Integrations](#cdn-library-integrations)
+- [Nginx Container Configuration](#nginx-container-configuration)
+- [Docker Compose Service Schema](#docker-compose-service-schema)
+- [NeutralinoJS Desktop Configuration](#neutralinojs-desktop-configuration)
+- [Desktop App Package Scripts](#desktop-app-package-scripts)
+- [GitHub Actions Workflows](#github-actions-workflows)
+
+---
+
+## Client-Side LocalStorage Keys
+
+The web application stores user preferences and document states directly in the browser's `localStorage`.
+
+| LocalStorage Key | Type | Default Value | Description |
+| :--- | :--- | :--- | :--- |
+| `theme` | `"light"` \| `"dark"` | System preference | Renders light or dark colors. |
+| `syncScroll` | `"true"` \| `"false"` | `"true"` | Controls editor-preview scroll sync. |
+| `viewMode` | `"split"` \| `"editor"` \| `"preview"` | `"split"` | Sets the default editing layout. |
+| `markdown_tabs` | `JSON string` | Sample tab schema | Array of document objects (ID, title, content, scroll position, view mode). |
+
+---
+
+## CDN Library Integrations
+
+Markdown Viewer loads large dependencies from CDNs. To update a library or pin it to a specific version, modify the script or link tag in the head of `index.html`:
+
+| Library Name | Version | CDN Service Provider | Core URL |
+| :--- | :--- | :--- | :--- |
+| **Marked.js** | `9.1.6` | jsDelivr | `https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js` |
+| **Highlight.js** | `11.9.0` | cdnjs | `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js` |
+| **DOMPurify** | `3.0.9` | jsDelivr | `https://cdn.jsdelivr.net/npm/dompurify@3.0.9/dist/purify.min.js` |
+| **MathJax** | `3.2.2` | jsDelivr | `https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-mml-chtml.js` |
+| **Mermaid** | `11.15.0` | jsDelivr | `https://cdn.jsdelivr.net/npm/mermaid@11.15.0/dist/mermaid.min.js` |
+| **jsPDF** | `2.5.1` | cdnjs | `https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js` |
+| **html2canvas** | `1.4.1` | cdnjs | `https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js` |
+| **Pako** | `2.1.0` | cdnjs | `https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js` |
+| **js-yaml** | `4.1.0` | cdnjs | `https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js` |
+| **FileSaver.js** | `2.0.5` | cdnjs | `https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js` |
+| **Bootstrap** | `5.3.2` | jsDelivr | `https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.js` |
+
+---
+
+## Nginx Container Configuration
+
+The container image is built using `nginx:alpine`. Static web files are served from `/usr/share/nginx/html/`.
+
+### Embedded Nginx Configuration (`/etc/nginx/conf.d/default.conf`)
+```nginx
+server {
+    listen 80;
+    server_name localhost;
+    root /usr/share/nginx/html;
+    index index.html;
+
+    # SPA Routing Fallback
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+
+    # Static Assets Caching (1 year)
+    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
+        expires 1y;
+        add_header Cache-Control "public, immutable";
+    }
+
+    # Security Headers
+    add_header X-Frame-Options "SAMEORIGIN" always;
+    add_header X-Content-Type-Options "nosniff" always;
+    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+}
+```
+
+---
+
+## Docker Compose Service Schema
+
+The `docker-compose.yml` file defines how the container runs locally:
+
+```yaml
+services:
+  markdown-viewer:
+    image: ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+    container_name: markdown-viewer
+    ports:
+      - "8080:80"
+    restart: unless-stopped
+```
+
+To build and run a local image instead of pulling the pre-built container from the registry, replace the `image` key with `build: .`:
+```yaml
+services:
+  markdown-viewer:
+    build: .
+    container_name: markdown-viewer
+    ports:
+      - "8080:80"
+    restart: unless-stopped
+```
+
+---
+
+## NeutralinoJS Desktop Configuration
+
+The desktop application uses the NeutralinoJS framework, which is configured in `desktop-app/neutralino.config.json`.
+
+```json
+{
+  "applicationId": "js.neutralino.markdownviewer",
+  "version": "1.2.0",
+  "defaultMode": "window",
+  "enableServer": true,
+  "enableNativeAPI": true,
+  "tokenSecurity": "one-time",
+  "logging": {
+    "enabled": true,
+    "writeToLogFile": true
+  },
+  "nativeAllowList": [
+    "app.*",
+    "os.*",
+    "debug.*",
+    "filesystem.*"
+  ],
+  "window": {
+    "title": "Markdown Viewer",
+    "width": 1000,
+    "minWidth": 800,
+    "height": 700,
+    "minHeight": 500,
+    "resizable": true,
+    "maximize": false,
+    "center": true
+  },
+  "modes": {
+    "window": {
+      "index": "/index.html",
+      "icon": "/resources/assets/icon.jpg"
+    }
+  },
+  "cli": {
+    "binaryName": "markdown-viewer",
+    "resourcesPath": "/resources/",
+    "extensionsPath": "/extensions/",
+    "clientLibrary": "/resources/js/neutralino.js",
+    "binaryVersion": "6.5.0",
+    "clientVersion": "11.7.0"
+  }
+}
+```
+
+### Key Window Settings
+*   `width` / `height`: The default launch window size in pixels ($1000 \times 700$).
+*   `minWidth` / `minHeight`: Restricts window scaling below $800 \times 500$ to prevent UI layout issues.
+*   `nativeAllowList`: Grants the application permission to access OS and filesystem APIs (e.g. `filesystem.*` is required for loading and saving local files).
+
+---
+
+## Desktop App Package Scripts
+
+The `desktop-app/package.json` file contains scripts for development, packaging, and dependency updates:
+
+| Script Name | Command | Description |
+| :--- | :--- | :--- |
+| `setup` | `node setup-binaries.js` | Downloads Neutralino platform-specific runtimes. |
+| `dev` | `npx @neutralinojs/neu run` | Starts the desktop app in hot-reload development mode. |
+| `prepare` | `node prepare.js` | Copies core files from the project root into the desktop build directory. |
+| `build` | `node build-windows.js` | Compiles a single-file executable for Windows. |
+| `build:portable` | `npx @neutralinojs/neu build --release` | Packages the application into zip files for Windows, Linux, and macOS. |
+
+---
+
+## GitHub Actions Workflows
+
+The repository uses two GitHub Actions workflows:
+
+### 1. Docker Build & Publish (`docker-publish.yml`)
+*   **Triggers:** Pushes to the `main` branch, or pull requests.
+*   **Registry:** GitHub Container Registry (`ghcr.io`).
+*   **Target Architectures:** `linux/amd64` and `linux/arm64` (multi-arch build).
+*   **Tags:** `latest` for main branch releases, and commit-sha tags for development runs.
+
+### 2. Desktop Compiler (`desktop-build.yml`)
+*   **Triggers:** Pushing Git tags that match the pattern `desktop-v*` (e.g. `desktop-v1.2.0`).
+*   **Action:** Runs on Node LTS, runs the `setup`, `prepare`, and `build:portable` scripts, and generates checksums.
+*   **Release:** Automatically creates a draft release on GitHub and uploads the compiled binaries as assets.

+ 179 - 0
wiki/Contributing.md

@@ -0,0 +1,179 @@
+# Contributing Guidelines
+
+Thank you for your interest in contributing to **Markdown Viewer**! We welcome contributions, including bug reports, feature requests, documentation improvements, and code updates.
+
+---
+
+## Table of Contents
+
+- [Code of Conduct](#code-of-conduct)
+- [Reporting Bugs & Issues](#reporting-bugs--issues)
+- [Security Disclosures](#security-disclosures)
+- [Development Setup](#development-setup)
+  - [Web Application](#web-application)
+  - [Desktop Application](#desktop-application)
+- [Code Style Guidelines](#code-style-guidelines)
+  - [HTML Style](#html-style)
+  - [CSS Style](#css-style)
+  - [JavaScript Style](#javascript-style)
+- [Commit Message Conventions](#commit-message-conventions)
+- [Pull Request & Code Review Process](#pull-request--code-review-process)
+- [Repository Structure](#repository-structure)
+
+---
+
+## Code of Conduct
+
+By participating in this project, you agree to maintain a respectful, professional, and inclusive environment. Please be kind and constructive in all communication and code reviews.
+
+---
+
+## Reporting Bugs & Issues
+
+1.  Search the [GitHub Issue Tracker](https://github.com/ThisIs-Developer/Markdown-Viewer/issues) to verify the bug has not already been reported.
+2.  If it is a new bug, open an issue including:
+    *   A clear, descriptive title.
+    *   Detailed steps to reproduce the bug.
+    *   Expected vs. actual behavior.
+    *   Your operating system and browser versions.
+    *   Any relevant screenshots or console error messages.
+
+---
+
+## Security Disclosures
+
+If you discover a security vulnerability, please report it responsibly:
+*   Do not open a public issue.
+*   Submit a private advisory via GitHub Security Advisories if enabled for the repository.
+*   Alternatively, contact the project maintainers directly with details of the vulnerability and a minimal reproduction.
+
+---
+
+## Development Setup
+
+### Web Application
+The core web app requires no build step. To serve `index.html` on localhost (`127.0.0.1`) and run it locally:
+
+1.  Clone the repository:
+    ```bash
+    git clone https://github.com/ThisIs-Developer/Markdown-Viewer.git
+    cd Markdown-Viewer
+    ```
+2.  Serve the root directory using a local web server:
+    ```bash
+    # Serve with Python (built-in, no dependencies)
+    python3 -m http.server 8080
+    # or
+    # Serve with Node.js serve
+    npx serve . -p 8080
+    ```
+3.  Open **http://localhost:8080** or **http://127.0.0.1:8080** in your browser and edit files like `index.html`, `script.js`, or `styles.css`.
+
+### Desktop Application
+To set up the NeutralinoJS desktop environment:
+
+1.  Navigate to the desktop directory:
+    ```bash
+    cd Markdown-Viewer/desktop-app
+    ```
+2.  Install dependencies:
+    ```bash
+    npm install
+    ```
+3.  Download the Neutralino runtime binaries:
+    ```bash
+    node setup-binaries.js
+    ```
+4.  Copy the latest frontend assets into the desktop directory:
+    ```bash
+    node prepare.js
+    ```
+5.  Start the app in development mode with hot-reload:
+    ```bash
+    npm run dev
+    ```
+
+---
+
+## Code Style Guidelines
+
+### HTML Style
+*   Use **2-space indentation**.
+*   Write semantic HTML5 tags (`<header>`, `<main>`, `<section>`, etc.) and ensure they are nested correctly.
+*   Include `aria-*` attributes and roles to maintain accessibility.
+*   Do not write inline CSS styles; place all styles in `styles.css`.
+
+### CSS Style
+*   Use **2-space indentation**.
+*   Use CSS variables (custom properties) on `:root` and `[data-theme="dark"]` to manage colors, borders, and margins.
+*   Scope transitions to specific properties (e.g. `transition: background-color 0.2s`) to avoid repainting the entire viewport during theme switches.
+*   Group style sheets logically using clear comments (e.g. `/* --- Editor Layout --- */`).
+
+### JavaScript Style
+*   Use **2-space indentation** and insert semicolons.
+*   Use Vanilla ES6 JavaScript without external frameworks.
+*   Use `const` for constant references and `let` for variables. Do not use `var`.
+*   Offload CPU-intensive parsing or formatting logic to the Web Worker (`preview-worker.js`) to keep the main UI thread responsive.
+*   Debounce event handlers that trigger rendering or layout calculations.
+
+---
+
+## Commit Message Conventions
+
+We use the **Conventional Commits** standard to organize project changes. Commit messages must use the following format:
+
+```
+<type>(<scope>): <description>
+
+[body]
+[footer]
+```
+
+### Commit Types:
+*   `feat`: A new user-facing feature.
+*   `fix`: A bug fix.
+*   `docs`: Changes to documentation files (such as the wiki or README).
+*   `style`: Formatting updates (whitespace, semicolons) that do not affect code logic.
+*   `refactor`: Code restructuring that neither fixes a bug nor adds a feature.
+*   `perf`: Performance-related optimizations.
+*   `chore`: Tasks like updating dependencies, build configurations, or CI files.
+
+### Commit Examples:
+*   `feat(editor): add keyboard shortcut for fullscreen mode`
+*   `fix(pdf): correct page breaks in long tables`
+*   `docs(wiki): add Mermaid diagrams formatting guide`
+*   `chore(deps): update marked.js version to 9.1.6`
+
+---
+
+## Pull Request & Code Review Process
+
+1.  Fork the repository and create a new feature branch from `main`:
+    ```bash
+    git checkout -b feature/my-feature-name
+    ```
+2.  Make your changes, verify your code style, and test them across modern browsers (Chrome, Firefox, Safari, Edge).
+3.  Commit your updates following the [Commit Message Conventions](#commit-message-conventions).
+4.  Rebase your branch to ensure it is up to date with the upstream `main` branch before submitting:
+    ```bash
+    git fetch upstream
+    git rebase upstream/main
+    ```
+5.  Open a Pull Request pointing to the upstream repository's `main` branch. Complete the pull request template with details of the changes and any related issues.
+6.  A project maintainer will review your pull request. Please address any review comments before the code is merged. Once approved, the pull request will be squash-merged.
+
+---
+
+## Repository Structure
+
+Below is an overview of the key folders and files in the repository:
+
+*   `index.html`: The entry-point HTML page.
+*   `script.js`: The main controller and UI interaction script.
+*   `preview-worker.js`: The background Web Worker script that compiles Markdown.
+*   `styles.css`: CSS styles and themes.
+*   `sw.js`: The Service Worker cache proxy script.
+*   `Dockerfile` / `docker-compose.yml`: Docker configuration files.
+*   `assets/`: Image assets and diagrams.
+*   `wiki/`: Document source files for this wiki.
+*   `desktop-app/`: NeutralinoJS application source files.

+ 179 - 0
wiki/Desktop-App.md

@@ -0,0 +1,179 @@
+# Desktop Application Guide
+
+This page describes the architecture, development setup, build options, and platform installation procedures for the desktop version of **Markdown Viewer** (v3.7.4), powered by the **Neutralinojs** runtime framework.
+
+---
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Workspace Directory Structure](#workspace-directory-structure)
+- [Prerequisites](#prerequisites)
+- [Local Development Setup](#local-development-setup)
+- [Running in Hot-Reload Dev Mode](#running-in-hot-reload-dev-mode)
+- [Building the Application](#building-the-application)
+- [Build Output Configurations](#build-output-configurations)
+- [Building with Docker Containerization](#building-with-docker-containerization)
+- [Platform-Specific Installation Guidelines](#platform-specific-installation-guidelines)
+  - [Windows](#windows)
+  - [Linux](#linux)
+  - [macOS](#macos)
+
+---
+
+## Overview
+
+The desktop version of Markdown Viewer wraps the core web application (HTML, CSS, JS) inside a lightweight native OS webview container using the **Neutralinojs** framework. 
+
+### Why Neutralinojs?
+*   **Minimal Footprint:** Unlike Electron, which bundles a full Chromium browser and Node.js instance, Neutralinojs uses the system's built-in webview. This results in an executable size of less than 15 MB (compared to 150+ MB for Electron).
+*   **Low Resource Usage:** Idle memory usage is typically under 50 MB.
+*   **Shared Codebase:** The desktop app uses the exact same core files (`script.js`, `styles.css`, `assets/`) as the web application.
+
+---
+
+## Workspace Directory Structure
+
+The `desktop-app` directory contains the configuration files and build scripts for the desktop version:
+
+```
+desktop-app/
+├── package.json              # Contains npm build scripts
+├── neutralino.config.json    # Configures Neutralino window size, titles, and API permissions
+├── setup-binaries.js         # Script to download Neutralino binaries for target platforms
+├── prepare.js                # Copies core files from the root folder into the resources folder
+└── resources/                # Assets packaged into the desktop application
+    ├── index.html            # Compiled template page
+    ├── styles.css            # Stylesheet copied from the root folder
+    ├── js/
+    │   ├── main.js           # Handles desktop lifecycle events and tray menus
+    │   ├── script.js         # Copied from the root folder
+    │   └── neutralino.js     # Neutralino client API library
+    └── assets/               # Image assets copied from the root folder
+```
+
+---
+
+## Prerequisites
+
+To compile the desktop application from source, you will need:
+*   **Node.js** (v16.0.0 or later) and **npm** (installed automatically with Node).
+*   **An active internet connection** (only required during the initial setup to download Neutralino runtimes).
+
+---
+
+## Local Development Setup
+
+To set up the desktop project directory locally:
+
+1.  Open your terminal and navigate to the `desktop-app` folder:
+    ```bash
+    cd Markdown-Viewer/desktop-app
+    ```
+2.  Install dependencies:
+    ```bash
+    npm install
+    ```
+3.  Download the required Neutralino framework binaries for Windows, Linux, and macOS:
+    ```bash
+    node setup-binaries.js
+    ```
+4.  Copy the latest frontend code from the repository root into the desktop build resources folder:
+    ```bash
+    node prepare.js
+    ```
+
+---
+
+## Running in Hot-Reload Dev Mode
+
+To run the application locally in development mode:
+
+```bash
+npm run dev
+```
+
+This starts the desktop application in a new window. It enables a development server with **hot-reload**: edits to files in the `resources/` folder will immediately update the running application without requiring a manual rebuild.
+
+---
+
+## Building the Application
+
+You can package the application in three ways:
+
+### 1. Embedded Binary (Recommended for Windows)
+This builds a single, self-contained Windows executable file that has all application resources embedded inside the binary:
+```bash
+npm run build
+```
+
+### 2. Portable Distribution
+This builds the application with the binary and a separate `resources.neu` file. This is standard for multi-platform distributions:
+```bash
+npm run build:portable
+```
+
+### 3. Build All Formats
+This compiles both the portable distribution ZIP file and the embedded Windows executable:
+```bash
+npm run build:all
+```
+
+---
+
+## Build Output Configurations
+
+Compiled files are placed in the `desktop-app/dist/` directory:
+
+```
+dist/
+├── markdown-viewer/                         # Contains portable binaries
+├── markdown-viewer-release.zip              # Compressed portable distribution
+└── windows-embedded/
+    └── markdown-viewer/
+        └── markdown-viewer-win_x64.exe      # Single-file Windows binary
+```
+
+---
+
+## Building with Docker Containerization
+
+To package the application without installing Node.js locally, use the provided Docker Compose configuration to compile the binaries inside a container:
+
+```bash
+# Run from the desktop-app folder
+docker compose up --build
+```
+
+The container downloads the required binaries, runs the build scripts, and saves the output to the host system's `dist/` directory using a volume mount.
+
+---
+
+## Platform-Specific Installation Guidelines
+
+### Windows
+*   **Installation:** No installer is needed. Run `markdown-viewer-win_x64.exe` directly.
+*   **SmartScreen Warning:** Because the executable is unsigned, Windows SmartScreen may display a warning on first launch. Click **More info**, then select **Run anyway** to launch the application.
+
+### Linux
+Make the binary executable before running it:
+```bash
+chmod +x markdown-viewer-linux_x64
+./markdown-viewer-linux_x64
+```
+
+### macOS
+macOS blocks unsigned binaries by default. To run the app, clear the quarantine flag and make the file executable:
+
+```bash
+# Remove the macOS quarantine flag
+xattr -d com.apple.quarantine markdown-viewer-mac_universal
+
+# Grant execution permissions
+chmod +x markdown-viewer-mac_universal
+
+# Run the app
+./markdown-viewer-mac_universal
+```
+
+Alternatively, right-click the app binary in Finder, click **Open**, and select **Open** in the confirmation dialog.

+ 88 - 0
wiki/Development-Journey.md

@@ -0,0 +1,88 @@
+# Project Development Journey
+
+This page documents the development history, design decisions, and evolution of **Markdown Viewer** from its initial prototype to the current production release (v3.7.4).
+
+---
+
+## Chronological Project Evolution
+
+Markdown Viewer was built to address a common need: a fast, privacy-focused Markdown editor that renders rich formatting, math equations, and diagrams client-side without relying on external databases.
+
+```
++------------------------------------+
+|  Phase 1: Basic Renderer (V0.1)     |
+|  - Simple textarea & Marked parser |
+|  - High typing lag on large files  |
++-----------------+------------------+
+                  |
+                  v
++------------------------------------+
+|  Phase 2: Off-Thread Parser (V0.5) |
+|  - Shifted parsing to Web Worker   |
+|  - Added basic highlight.js syntax |
++-----------------+------------------+
+                  |
+                  v
++------------------------------------+
+|  Phase 3: Patching & Features (V1.0)|
+|  - Incremental DOM patching        |
+|  - MathJax & Mermaid integrations  |
+|  - Multi-document tab bar added    |
++-----------------+------------------+
+                  |
+                  v
++------------------------------------+
+|  Phase 4: Release & Desktop (V3.7.4)|
+|  - Cascade PDF layout pagination   |
+|  - Neutralinojs desktop app wrap   |
+|  - Service Worker offline cache    |
++------------------------------------+
+```
+
+---
+
+## Detailed Version Comparison
+
+To see the differences in user interface design, rendering performance, and feature set, you can compare the original prototype with the current production build:
+
+| Target Area | Initial Prototype (Original V0.1) | Current Production Release (v3.7.4) |
+| :--- | :--- | :--- |
+| **Hosting Link** | [a1b91221.markdownviewer.pages.dev](https://a1b91221.markdownviewer.pages.dev/) | [markdownviewer.pages.dev](https://markdownviewer.pages.dev/) |
+| **Parsing Thread** | Main UI Thread (blocked typing) | Dedicated Background Web Worker |
+| **DOM Rendering** | Full `innerHTML` write (slow reflow) | Incremental FNV-1a Hash DOM Patching |
+| **Scroll Sync** | Basic scroll mapping (caused loops) | Locked `requestAnimationFrame` Sync |
+| **Diagram Support** | None (code text only) | Interactive Mermaid SVGs with Zoom & Pan |
+| **Math Typesetting** | None | LaTeX MathJax rendering with accessibility cleanup |
+| **Tabbed Sessions** | Single document | Drag-and-drop tab bar with localStorage autosave |
+| **Export Formats** | Raw Markdown only | Markdown, Standalone HTML, and sandboxed PDF |
+| **App Wrapper** | Browser only | NeutralinoJS Desktop Application shell |
+| **Offline Mode** | No (required online refresh) | PWA Service Worker caching (Cache-First) |
+| **File Import** | None | Drag & drop file parser and GitHub repo importer |
+
+---
+
+## Key Development Milestones
+
+### 1. Moving to Background Web Workers
+Early testing with files larger than 10 KB showed noticeable typing lag because the main thread had to compile Markdown and highlight syntax on every keystroke. 
+Moving the parser to `preview-worker.js` resolved this. The main thread now only processes user input and patches the DOM, while the background worker handles document compilation and syntax highlighting.
+
+### 2. Upgrading to Incremental DOM Patching
+Using `element.innerHTML` on every keystroke caused the browser to rebuild the entire preview pane. This reset scroll positions, cleared focus states, and collapsed elements like `<details>` blocks. 
+To fix this, we implemented an FNV-1a hash matching system. The app now compares hashes of incoming blocks and only replaces the DOM nodes that have changed, preserving the overall page state.
+
+### 3. Preventing Scroll Synchronization Loops
+Initially, syncing scrolls between the editor and preview created an infinite feedback loop. 
+We resolved this by adding scroll locks (`isEditorScrolling` and `isPreviewScrolling`) combined with scheduling scroll updates inside `requestAnimationFrame`. This ensures scroll sync is smooth and loop-free.
+
+### 4. Implementing Cascade PDF Layout Pagination
+Default PDF exports often slice images, diagrams, and text lines across page breaks. 
+We addressed this by building a cascade pagination engine in an off-screen sandbox. It converts SVG diagrams to rasters, pushes headings below page breaks, splits tables row-by-row while duplicating the headers, and downscales oversized elements to fit the page.
+
+---
+
+## Transparency & Data Policies
+
+*   **100% Client-Side Processing:** All text parsing, diagram generation, mathematical typesetting, and file exports happen inside the client browser. No document data is ever sent to an external server.
+*   **Encrypted Hash Links:** The URL sharing feature uses local zlib/deflate compression via `Pako.js`. The compressed document is stored entirely in the URL hash fragment. Because hash fragments are not sent to servers in HTTP requests, your shared content remains private.
+*   **Security Auditing:** External dependencies (DOMPurify, Marked.js, etc.) are loaded with subresource integrity (SRI) hashes to prevent loading modified scripts.

+ 214 - 0
wiki/Docker-Deployment.md

@@ -0,0 +1,214 @@
+# Docker Deployment Guide
+
+This page provides comprehensive documentation for containerizing, running, and proxying **Markdown Viewer** (v3.7.4) using Docker and Docker Compose.
+
+---
+
+## Table of Contents
+
+- [Quick Start Command](#quick-start-command)
+- [Docker Run Command Reference](#docker-run-command-reference)
+- [Docker Compose Integration](#docker-compose-integration)
+- [Building the Image Locally](#building-the-image-locally)
+- [Production Nginx Settings & Security Headers](#production-nginx-settings--security-headers)
+- [Configuring the App at a Custom Sub-Path](#configuring-the-app-at-a-custom-sub-path)
+- [Reverse Proxy Server Integrations](#reverse-proxy-server-integrations)
+  - [Nginx Configuration](#nginx-configuration)
+  - [Caddy Configuration](#caddy-configuration)
+  - [Traefik Compose Labels](#traefik-compose-labels)
+- [CI/CD Deployment Automation](#cicd-deployment-automation)
+
+---
+
+## Quick Start Command
+
+To spin up a local instance immediately:
+
+```bash
+docker pull ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+docker run -d \
+  --name markdown-viewer \
+  -p 8080:80 \
+  --restart unless-stopped \
+  ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+```
+
+Open **http://localhost:8080** in your browser.
+
+---
+
+## Docker Run Command Reference
+
+### Custom Port Mapping
+To map the container to a different host port (such as `8081`):
+```bash
+docker pull ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+docker run -d --name markdown-viewer -p 8081:80 ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+```
+
+### Specifying Version Tags
+You can pin the container to a specific commit or release by replacing `latest` with the appropriate tag:
+```bash
+docker run -d --name markdown-viewer -p 8080:80 ghcr.io/thisis-developer/markdown-viewer:main
+```
+
+Available image tags:
+*   `latest`: The latest stable build from the main branch.
+*   `main`: The most recent commit pushed to the main branch.
+*   `<commit-sha>` (e.g. `e3d7a1b`): A build pinned to a specific commit.
+
+---
+
+## Docker Compose Integration
+
+The repository includes a default `docker-compose.yml` file for Compose-based deployments:
+
+### Starting the Services
+```bash
+docker compose up -d
+```
+
+### Stopping the Services
+```bash
+docker compose down
+```
+
+### Rebuilding Containers
+To rebuild the image using local source code changes:
+```bash
+docker compose up -d --build
+```
+
+### Default `docker-compose.yml` Configuration
+```yaml
+services:
+  markdown-viewer:
+    image: ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+    container_name: markdown-viewer
+    ports:
+      - "8080:80"
+    restart: unless-stopped
+```
+
+---
+
+## Building the Image Locally
+
+To build a custom Docker image from the source code:
+
+1.  Clone the repository and navigate to the project directory:
+    ```bash
+    git clone https://github.com/ThisIs-Developer/Markdown-Viewer.git
+    cd Markdown-Viewer
+    ```
+2.  Build the Docker image:
+    ```bash
+    docker build -t markdown-viewer:local .
+    ```
+3.  Run the local container:
+    ```bash
+    docker run -d --name markdown-viewer-local -p 8080:80 markdown-viewer:local
+    ```
+
+---
+
+## Production Nginx Settings & Security Headers
+
+The Docker image uses `nginx:alpine` and includes custom configurations to optimize caching and security:
+
+### Core Configurations
+*   **Single-Page App (SPA) Routing:** Requests are redirected to `/index.html` to support client-side routing.
+*   **Static Asset Caching:** Cache-control headers are set to one year (`max-age=31536000`) for static files (JS, CSS, images, fonts).
+*   **Security Headers:** To protect the app from typical web vulnerabilities, Nginx is configured to inject the following headers:
+
+```nginx
+# Prevents the app from being embedded in iframes on other domains (XSS and Clickjacking protection)
+add_header X-Frame-Options "SAMEORIGIN" always;
+
+# Blocks browsers from MIME-type sniffing
+add_header X-Content-Type-Options "nosniff" always;
+
+# Restricts referrer information sent with outbound links
+add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+```
+
+---
+
+## Configuring the App at a Custom Sub-Path
+
+To serve Markdown Viewer from a sub-path (e.g. `example.com/editor/`) instead of the root directory, update the Nginx configuration inside the `Dockerfile`:
+
+Modify the root location block in the Nginx config to use an alias for the sub-path:
+```nginx
+server {
+    listen 80;
+    root /usr/share/nginx/html;
+
+    location /editor/ {
+        alias /usr/share/nginx/html/;
+        try_files $uri $uri/ /editor/index.html;
+    }
+}
+```
+
+Rebuild the Docker image after applying these changes.
+
+---
+
+## Reverse Proxy Server Integrations
+
+### Nginx Configuration
+To set up a reverse proxy with Nginx, use this server block to forward incoming requests to the Markdown Viewer container (running on port `8080`):
+
+```nginx
+server {
+    listen 443 ssl;
+    server_name markdown.example.com;
+
+    ssl_certificate     /etc/ssl/certs/markdown.pem;
+    ssl_certificate_key /etc/ssl/private/markdown.key;
+
+    location / {
+        proxy_pass http://127.0.0.1:8080;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+}
+```
+
+### Caddy Configuration
+Caddy automatically handles HTTPS and reverse proxy configurations. Update your `Caddyfile` with the following rule:
+
+```caddyfile
+markdown.example.com {
+    reverse_proxy localhost:8080
+}
+```
+
+### Traefik Compose Labels
+To use Traefik to route traffic to the Markdown Viewer container, add these labels to the service definition in your `docker-compose.yml` file:
+
+```yaml
+services:
+  markdown-viewer:
+    image: ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+    container_name: markdown-viewer
+    labels:
+      - "traefik.enable=true"
+      - "traefik.http.routers.mdviewer.rule=Host(`markdown.example.com`)"
+      - "traefik.http.routers.mdviewer.entrypoints=websecure"
+      - "traefik.http.routers.mdviewer.tls.certresolver=letsencrypt"
+      - "traefik.http.services.mdviewer.loadbalancer.server.port=80"
+    restart: unless-stopped
+```
+
+---
+
+## CI/CD Deployment Automation
+
+The repository includes a GitHub Actions workflow (`.github/workflows/docker-publish.yml`) that automates container building and publishing:
+
+*   **Pushes to `main`:** Builds and pushes multi-architecture images (`linux/amd64` and `linux/arm64`) to the GitHub Container Registry (`ghcr.io`).
+*   **Pull Requests:** Automatically builds the image to verify compile status and check for errors, without publishing it to the registry.

+ 99 - 0
wiki/FAQ.md

@@ -0,0 +1,99 @@
+# Frequently Asked Questions (FAQ)
+
+This FAQ answers common questions about using, deploying, and troubleshooting **Markdown Viewer** (v3.7.4).
+
+---
+
+## Table of Contents
+
+- [General & Privacy](#general--privacy)
+- [Editor & Preview Features](#editor--preview-features)
+- [Installation & Deployment](#installation--deployment)
+- [Troubleshooting](#troubleshooting)
+
+---
+
+## General & Privacy
+
+### What is Markdown Viewer?
+Markdown Viewer is a client-side Markdown editing suite and live preview tool. It features off-thread Web Worker parsing, incremental DOM patching, responsive split views, interactive diagrams (Mermaid), and mathematical equation formatting (LaTeX).
+
+### Is Markdown Viewer free to use?
+Yes. It is free and open-source software licensed under the **Apache License 2.0**.
+
+### Do I need to create an account?
+No. The application is serverless and does not require registration, login, or subscription.
+
+### Does Markdown Viewer send my content to any servers?
+No. All parsing, rendering, typesetting, and exporting happen entirely on your computer inside the browser. No document text or metadata is uploaded to external servers.
+
+### Do you collect analytics or telemetry data?
+No. The application does not contain tracking scripts, telemetry code, cookies, or advertising pixels.
+
+### What information is stored in my browser?
+The app saves your settings (theme, view mode, scroll synchronization state) and open tab documents to your browser's local storage (`localStorage`). This enables auto-saving and restores your workspace when you reload the page.
+
+### How do I clear all local storage data?
+You can clear this data by using your browser's site settings or developer tools:
+1.  Open the browser console (press `F12`).
+2.  Navigate to the **Application** or **Storage** tab.
+3.  Select **Local Storage** and clear the records for the site.
+4.  Alternatively, click the **Reset** button in the tab bar to clear all documents.
+
+---
+
+## Editor & Preview Features
+
+### Does the app support GitHub-Flavored Markdown (GFM)?
+Yes. It uses the `marked.js` library to support standard GFM features, including tables, task checklists, strikethrough, autolinks, and emoji shortcodes.
+
+### Can I write math formulas and LaTeX equations?
+Yes. The application uses MathJax to format equations. You can write inline equations using single dollar signs (`$E=mc^2$`) or block equations using double dollar signs (`$$...$$`). For more details, see the [Markdown Reference](Markdown-Reference) page.
+
+### Can I draw flowcharts and diagrams?
+Yes. You can write diagrams using Mermaid syntax inside fenced code blocks marked with `mermaid`. The preview pane displays these as interactive SVG diagrams. Double-click any diagram in the preview to open a zoomable, draggable modal.
+
+### Why does the exported PDF look different from the live preview?
+Exporting to PDF uses `html2canvas` and `jsPDF` to capture screenshots of the preview pane page-by-page. While the app uses a sandboxing and pagination engine to adjust page breaks and scale elements, some complex CSS layouts, font styles, or wide code blocks may not render perfectly.
+
+> [!TIP]
+> For the highest PDF quality, use your browser's built-in print command (`Ctrl + P` or `Cmd + P`) and select "Save as PDF".
+
+---
+
+## Installation & Deployment
+
+### Can I run the application offline?
+*   **Web version:** The default build loads libraries (like MathJax and Mermaid) from external CDNs, requiring an active internet connection on first load. Once these files are cached by the Service Worker, the application can run offline.
+*   **Desktop version:** The desktop application requires internet access on first launch to cache CDN assets. To run fully offline from the start, download the CDN dependencies locally, update the script paths in `index.html`, and rebuild the application.
+
+### How do I host Markdown Viewer on my own server?
+Copy the static assets (`index.html`, `script.js`, `preview-worker.js`, `styles.css`, `sw.js`, and `assets/`) and serve them using a static web server (such as Nginx, Apache, or Caddy). 
+
+> [!WARNING]
+> Do not open the `index.html` file directly using the `file://` protocol in your browser. Security policies (CORS) block Web Workers from running from local files, which will break the preview parser.
+
+---
+
+## Troubleshooting
+
+### Why is the preview pane blank or not updating?
+1.  Check that JavaScript is enabled in your browser.
+2.  Open your browser console (`F12`) to check for scripts blocked by security policies or network errors.
+3.  Perform a hard refresh (`Ctrl + Shift + R` or `Cmd + Shift + R`) to clear cached script instances.
+
+### Why are my math equations not formatting?
+MathJax loads dynamically when math markers are detected in your document. Ensure you have an active internet connection to download the library on first use, and check that your LaTeX syntax is correct.
+
+### Why are my Mermaid diagrams showing syntax errors?
+Check that:
+*   The code block tag is written in lowercase as `mermaid`.
+*   Your diagram syntax is correct. You can verify your syntax using the [Mermaid Live Editor](https://mermaid.live).
+
+### Why won't the desktop application launch on macOS?
+macOS blocks unsigned binaries by default. To run the app, clear the quarantine flag using your terminal:
+```bash
+xattr -d com.apple.quarantine markdown-viewer-mac_universal
+chmod +x markdown-viewer-mac_universal
+```
+You can also right-click the binary in Finder, click **Open**, and confirm the launch prompt.

+ 232 - 0
wiki/Features.md

@@ -0,0 +1,232 @@
+# Detailed Features & Implementation Deep Dive
+
+This document details the features of **Markdown Viewer**, focusing on their architectural execution, performance strategies, and code-level configurations for version v3.7.4.
+
+---
+
+## Table of Contents
+
+1.  [Off-Thread Web Worker Parser](#1-off-thread-web-worker-parser)
+2.  [Segmented DOM Patching Engine](#2-segmented-dom-patching-engine)
+3.  [Ratio-Based Proportional Scroll Synchronization](#3-ratio-based-proportional-scroll-synchronization)
+4.  [LaTeX Mathematical Typesetting](#4-latex-mathematical-typesetting)
+5.  [Interactive Mermaid Diagrams with Drag-to-Pan](#5-interactive-mermaid-diagrams-with-drag-to-pan)
+6.  [Cascade PDF Layout Pagination Sandbox](#6-cascade-pdf-layout-pagination-sandbox)
+7.  [Multi-Document Session Persistence & Drag Reordering](#7-multi-document-session-persistence--drag-reordering)
+8.  [Binary Safety Gutter & Multi-File Importer](#8-binary-safety-gutter--multi-file-importer)
+9.  [Serverless Sharing via Compressed Hash Fragments](#9-serverless-sharing-via-compressed-hash-fragments)
+10. [Performance, Security, and UI Variables](#10-performance-security-and-ui-variables)
+
+---
+
+## 1. Off-Thread Web Worker Parser
+
+To prevent typing lag and main-thread blocks, Markdown Viewer offloads compilation to a background Web Worker (`preview-worker.js`).
+
+### Size-Aware Debouncing
+The main thread throttles render requests based on the character length of the active document to conserve CPU cycles:
+*   **Small Documents (<10 KB):** 80ms render debounce.
+*   **Medium Documents (10 KB - 50 KB):** 150ms render debounce.
+*   **Large Documents (>50 KB):** 300ms render debounce.
+
+### Segmented Worker Parsing
+1.  **Block Splitting:** The worker splits incoming markdown strings by double-newlines (`\n\n`) while respecting boundary exclusions (like block math `$$` and code fences ` ``` `).
+2.  **FNV-1a Alphanumeric Hashing:** For each block, the worker computes a 32-bit FNV-1a hash. This is a non-cryptographic hash function designed for speed:
+
+    $$H_i = (H_{i-1} \oplus d_i) \times p$$
+
+    where $p = 16777619$ (FNV prime) and $H_0 = 2166136261$ (offset basis).
+3.  **Selective Compilation:** If the document doesn't use complex global footnotes or reference-style declarations, the worker compiles only changed blocks using `marked.js` and `highlight.js`. It returns an array of compiled HTML strings paired with their FNV-1a hashes.
+
+---
+
+## 2. Segmented DOM Patching Engine
+
+Updating the entire preview pane using `element.innerHTML` causes layout repaints, resets scrollbar offsets, wipes focus states, and collapses open toggle elements (like `<details>`). Markdown Viewer employs a custom patching controller:
+
+### Patching Algorithm
+1.  **Hash Comparison:** The main thread compares the hashes of the incoming HTML block array against the child nodes of the preview pane.
+2.  **Targeted Replacement:**
+    *   If a node's hash matches, it is skipped.
+    *   If a node's hash differs, the script replaces the corresponding child node in place using `replaceWith()`.
+    *   If the new array has more elements, new nodes are appended.
+    *   Extra trailing nodes are pruned.
+3.  **Layout Containment:** Every block is wrapped in a `<section>` container configured with modern CSS rules:
+    ```css
+    content-visibility: auto;
+    contain-intrinsic-size: auto 220px;
+    ```
+    This instructs the browser's layout engine to bypass formatting and rendering of off-screen markdown sections, reducing repaint and reflow overhead.
+
+---
+
+## 3. Ratio-Based Proportional Scroll Synchronization
+
+When editing in **Split View**, scrolling the editor textarea scrolls the HTML preview pane proportionally, and vice versa.
+
+### Math Formula
+Proportional scrolling is mapped using the scroll ratio:
+
+$$R_{\text{scroll}} = \frac{\text{scrollTop}}{\text{scrollHeight} - \text{clientHeight}}$$
+
+The target container's scroll position is then calculated as:
+
+$$\text{Target-scroll-top} = R_{\text{scroll}} \times (\text{Target-scroll-height} - \text{Target-client-height})$$
+
+### Feedback Loop Prevention
+Since scrolling the target pane triggers its own scroll events, this can create an infinite update loop. To prevent this:
+*   The application implements state locks: `isEditorScrolling = true` and `isPreviewScrolling = true`.
+*   Scroll coordinates are updated within `requestAnimationFrame()`.
+*   A 50ms timeout releases the locks after scrolling stops.
+
+---
+
+## 4. LaTeX Mathematical Typesetting
+
+LaTeX parsing uses **MathJax** loaded dynamically from a CDN.
+
+### Scanning and Typesetting
+*   The controller scans inputs using a regex test: `/\$\$|\$[^$]|\\\(|\\\[/`.
+*   If math markers are detected, the MathJax libraries are fetched.
+*   Once loaded, equations are rendered by calling:
+    ```javascript
+    MathJax.typesetPromise([previewElement]);
+    ```
+
+### Accessibility Post-Processing
+By default, MathJax appends assistive MathML containers (`<mjx-assistive-mml>`) with `tabindex="0"`. This interrupts keyboard tab order. A post-processing script runs after typesetting:
+1.  It queries all `<mjx-assistive-mml>` tags in the preview.
+2.  It removes the `tabindex` attribute from each element.
+3.  These assistive nodes are hidden or stripped prior to PDF canvas capture to prevent overlapping text.
+
+---
+
+## 5. Interactive Mermaid Diagrams with Drag-to-Pan
+
+Mermaid code blocks are rendered as SVG diagrams with custom interactive features.
+
+### Floating Action Toolbar
+Every rendered Mermaid diagram is wrapped in a container that appends a floating toolbar with four actions:
+1.  **Zoom modal:** Opens the diagram in a full-screen interactive modal.
+2.  **Download PNG:** Captures the SVG, draws it to a canvas element, and downloads it.
+3.  **Download SVG:** Serializes the SVG XML nodes and triggers a browser download.
+4.  **Copy Image:** Renders the diagram as a PNG blob and copies it to the system clipboard using the asynchronous Clipboard API:
+    ```javascript
+    navigator.clipboard.write([
+        new ClipboardItem({ "image/png": pngBlob })
+    ]);
+    ```
+
+### Drag-to-Pan Mechanics
+Inside the zoom modal, the SVG transform matrix is updated during mouse events:
+*   **Scale:** Computed using mouse-wheel offsets:
+
+    $$\text{scale} = \max(0.1, \min(\text{scale} + \text{delta}, 10))$$
+
+*   **Panning:** Tracks the difference between starting coordinates and current pointer coordinates:
+
+    $$X_{\text{pan}} = X_{\text{current}} - X_{\text{dragStart}}$$
+
+    $$Y_{\text{pan}} = Y_{\text{current}} - Y_{\text{dragStart}}$$
+*   **CSS Transform:** The updates are applied using hardware-accelerated CSS properties:
+    ```javascript
+    svg.style.transform = `translate(${modalPanX}px, ${modalPanY}px) scale(${modalZoomScale})`;
+    ```
+
+---
+
+## 6. Cascade PDF Layout Pagination Sandbox
+
+Exporting long, complex Markdown previews to PDF often leads to sliced text lines and cut-off images. Markdown Viewer uses a custom pagination engine:
+
+### Pagination Pipeline
+1.  **Sandbox Cloning:** The preview DOM is cloned into an off-screen sandbox element (`.pdf-export` class) set to A4 width (210mm).
+2.  **SVG to Raster Conversion:** Because `html2canvas` struggles to render inline SVGs, all Mermaid diagrams are converted to Base64-encoded PNG image elements inside the sandbox.
+3.  **Cascade Pagination Loop:** The pagination engine executes up to 10 passes:
+    *   *Keep-with-Next Headings:* Headings within 70px of a page break are shifted down via margin-top spacers (`.pdf-page-break-spacer`).
+    *   *Table Splitting:* Split rows are shifted, and the table header (`<thead>`) is duplicated onto the subsequent page.
+    *   *Text Alignment:* Lines are shifted downward to align page cuts cleanly between font heights, avoiding sliced characters.
+    *   *Oversized Graphics:* Images exceeding page boundaries are downscaled (minimum scale 0.5) to fit.
+4.  **Compilation:** The stabilized sandbox is captured page-by-page using `html2canvas`, and the resulting canvases are compiled into a PDF via `jsPDF`.
+
+---
+
+## 7. Multi-Document Session Persistence & Drag Reordering
+
+Markdown Viewer supports working with multiple documents simultaneously via a tabbed workspace.
+
+### Tab State Schema
+The workspace is managed in a global `tabs` array:
+```javascript
+{
+  id: "tab_" + Date.now() + "_" + Math.random().toString(36).substring(2, 8),
+  title: "Document Title",
+  content: "# Markdown Content...",
+  scrollPos: 0,
+  viewMode: "split", // split | editor | preview
+  createdAt: 1718042710000
+}
+```
+
+### Save Pipeline
+*   **Debounced Save:** Document changes trigger an auto-save that is debounced by 500ms using a window timer to prevent blocking the UI thread with constant serialization.
+*   **Beforeunload Flush:** To ensure changes are saved when navigating away or closing the page, the state is flushed immediately during `beforeunload` and `visibilitychange` events (when the page is hidden).
+
+### Drag-and-Drop Reordering
+*   Tab elements in the DOM are configured with `draggable="true"`.
+*   Drag events track the moving tab (`draggedTabId`).
+*   Releasing a tab over another swaps their indices in the state array, saves the updated array to `localStorage`, and updates the tab bar.
+
+---
+
+## 8. Binary Safety Gutter & Multi-File Importer
+
+### Binary File Guard
+To prevent importing corrupted binary files, the file reader scans the first 8 KB of any imported file:
+*   If a null byte (`\x00`) is found, the import is aborted, and an error is displayed.
+
+### Multi-File GitHub Importer
+Users can import documents directly from public GitHub repositories:
+1.  **URL Parsing:** The importer resolves repo, branch, folder, or file paths from a pasted URL.
+2.  **API Requests:** It queries public GitHub APIs (`api.github.com/repos/.../contents/...`) to fetch file trees.
+3.  **File Browser Modal:** Users can preview the file tree in a modal, select the files they want, and import them all at once. Selected files are loaded into separate document tabs.
+
+---
+
+## 9. Serverless Sharing via Compressed Hash Fragments
+
+Markdown Viewer lets you share documents via links that contain the entire compressed document content, eliminating the need for a database.
+
+### Encoding Pipeline
+
+$$\text{Markdown Text} \xrightarrow{\text{TextEncoder}} \text{Bytes} \xrightarrow{\text{Pako.deflate (zlib)}} \text{Compressed Bytes} \xrightarrow{\text{Base64-URL Encoding}} \text{URL Hash}$$
+
+1.  The markdown text is encoded to bytes and compressed using `Pako.js` (DEFLATE).
+2.  The compressed bytes are converted to a Base64 string.
+3.  The string is made URL-safe by replacing `+` with `-`, `/` with `_`, and removing trailing `=` padding.
+4.  The hash is appended to the URL as `#share=<encoded_string>`.
+5.  A warning is displayed if the generated link exceeds 32,000 characters.
+
+---
+
+## 10. Performance, Security, and UI Variables
+
+### Repaint & Transition Tuning
+*   Theme changes are managed using CSS variables.
+*   Transition animations are scoped to specific properties (`background-color`, `border-color`) rather than using `transition: all`.
+*   The background transition on the `<body>` element was removed to prevent repainting the entire viewport during theme shifts.
+
+### Resizable Panes
+*   The divider between the editor and preview panes can be dragged horizontally.
+*   It updates the CSS grid layout dynamically using percentage variables.
+*   Drag limits prevent either pane from being scaled below 20% of the viewport width.
+
+### Sanitization and Security
+*   All compiled HTML is sanitized on the main thread using **DOMPurify** before being rendered:
+    ```javascript
+    const cleanHtml = DOMPurify.sanitize(rawHtml, {
+        USE_PROFILES: { html: true },
+        ADD_ATTR: ['target', 'draggable', 'contenteditable']
+    });
+    ```
+*   This strips inline script handlers (e.g. `onload`, `onclick`) and `<script>` elements to prevent Cross-Site Scripting (XSS) when importing or loading external markdown files.

+ 75 - 0
wiki/Home.md

@@ -0,0 +1,75 @@
+# Welcome to the Markdown Viewer Wiki
+
+Welcome to the official technical documentation and user wiki for the **Markdown Viewer** application (v3.7.4). This repository contains detailed configuration guides, architecture documents, installation instructions, and user manuals to help you customize, deploy, and contribute to this client-side Markdown editing suite.
+
+---
+
+## 🚀 Quick Start Portal
+
+To deploy or run a local instance of the application immediately, execute the corresponding command for your environment:
+
+| Deployment Method | Command | Default Access URL |
+| :--- | :--- | :--- |
+| **Docker (Pre-built)** | `docker pull ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0 && docker run -d -p 8080:80 ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0` | `http://localhost:8080` / `http://127.0.0.1:8080` |
+| **Docker Compose** | `git clone https://github.com/ThisIs-Developer/Markdown-Viewer.git && cd Markdown-Viewer && docker compose up -d` | `http://localhost:8080` / `http://127.0.0.1:8080` |
+| **Python Static Server** | `python3 -m http.server 8080` (Run inside repository root to serve `index.html` on localhost/`127.0.0.1`) | `http://localhost:8080` / `http://127.0.0.1:8080` |
+| **Node.js Static Server** | `npx serve . -p 8080` (Run inside repository root to serve `index.html` on localhost/`127.0.0.1`) | `http://localhost:8080` / `http://127.0.0.1:8080` |
+| **Desktop Application** | Download execution package from [GitHub Releases](https://github.com/ThisIs-Developer/Markdown-Viewer/releases) | Launch native binary |
+
+---
+
+## 🗺️ Wiki Table of Contents
+
+Use the navigation map below to explore the technical documentation sections of this wiki:
+
+| Document / Section | Scope & Contents |
+| :--- | :--- |
+| **[Features](Features)** | Under-the-hood engineering deep-dive on compilation workers, DOM patching, and proportional scroll synchronization. |
+| **[Installation](Installation)** | Detailed multi-platform setup instructions for Docker, Docker Compose, static web servers, and Neutralinojs compile pipelines. |
+| **[Usage Guide](Usage-Guide)** | Standard operations manual detailing tab workspaces, file importing rules, and complete keyboard shortcuts mapping. |
+| **[Configuration](Configuration)** | Analysis of localStorage schemas, CDN assets links, Docker Nginx blocks, and Neutralino desktop runtime settings. |
+| **[Docker Deployment](Docker-Deployment)** | Production Docker customization guide containing security headers, custom context paths, and reverse proxy files. |
+| **[Desktop App](Desktop-App)** | Build workflow documentation for packaging native desktop executable wrappers on Windows, Linux, and macOS. |
+| **[Development Journey](Development-Journey)** | Evolution milestones of the project, comparison matrices between early prototype and v3.7.4, and design history. |
+| **[Markdown Reference](Markdown-Reference)** | Exhaustive writing template guide for GFM extensions, MathJax LaTeX equations, and Mermaid charts. |
+| **[FAQ](FAQ)** | Frequently Asked Questions on local privacy, memory utilization, export troubleshooting, and desktop security warnings. |
+| **[Contributing](Contributing)** | Development environment configuration guides, 2-space styling parameters, conventional commits, and PR reviews. |
+
+---
+
+## 🛡️ Core Architectural Principles
+
+Markdown Viewer is designed with four fundamental principles in mind:
+
+1.  **Zero-Server Privacy:** 100% client-side execution. The application has no tracking telemetry, cookie banners, analytical beacons, or external database integrations. Your content never leaves the browser.
+2.  **Off-Thread Processing:** Offloads intensive compiling and syntax coloring jobs from the main execution thread to dedicated Web Workers to ensure a 60fps typing experience even on files exceeding 100 KB.
+3.  **Visual Synchronization:** Renders layout and styles identical to GitHub's native Markdown representations, optimized dynamically for desktop, tablet, and mobile views.
+4.  **Local-First Persistence:** Integrates HTML5 Service Workers to intercept asset queries, serving core library script archives from the local browser storage to enable offline functionality.
+
+---
+
+## 🛠️ Global Technology Stack
+
+| Dependency Library | Version | Caching Tier | Role & Features |
+| :--- | :--- | :--- | :--- |
+| **Marked.js** | `9.1.6` | Precached (Main/Worker) | Markdown syntax parser and GFM compiler. |
+| **Highlight.js** | `11.9.0` | Precached (Worker Thread) | Syntactical color parsing for 190+ programming languages. |
+| **DOMPurify** | `3.0.9` | Precached (Main Thread) | DOM tree cleaning for XSS injection vulnerability blocks. |
+| **MathJax** | `3.2.2` | Lazy Cached (Dynamic) | LaTeX typesetting rendering engine. |
+| **Mermaid.js** | `11.15.0` | Lazy Cached (Dynamic) | Flowcharts, sequences, and architectural diagram blocks builder. |
+| **jsPDF** | `2.5.1` | Lazy Cached (Dynamic) | Client-side paginated PDF document builder. |
+| **html2canvas** | `1.4.1` | Lazy Cached (Dynamic) | Compiles CSS element layouts to canvas raster grids. |
+| **Pako.js** | `2.1.0` | Lazy Cached (Dynamic) | zlib DEFLATE compressor for sharing link generation. |
+| **js-yaml** | `4.1.0` | Precached (Main Thread) | Frontmatter config metadata header parser. |
+| **FileSaver.js** | `2.0.5` | Precached (Main Thread) | Controls download streams from the browser sandbox. |
+| **Bootstrap** | `5.3.2` | Precached (CDN CSS/JS) | Controls general layouts, modal forms, and UI toggles. |
+| **Neutralinojs** | `6.5.0` | Native Bridge | Desktop operating system framework wrapper shell. |
+
+---
+
+## 🔗 Project Resources
+
+*   💻 **[GitHub Source Repository](https://github.com/ThisIs-Developer/Markdown-Viewer)**
+*   📦 **[GitHub Releases & Executables](https://github.com/ThisIs-Developer/Markdown-Viewer/releases)**
+*   🐳 **[GitHub Package Container Registry](https://github.com/ThisIs-Developer/Markdown-Viewer/pkgs/container/markdown-viewer)**
+*   📜 **[Apache 2.0 Project License](https://github.com/ThisIs-Developer/Markdown-Viewer/blob/main/LICENSE)**

+ 206 - 0
wiki/Installation.md

@@ -0,0 +1,206 @@
+# Installation & Deployment Guide
+
+This page provides detailed installation, setup, and deployment guides for **Markdown Viewer** across all supported platforms.
+
+---
+
+## Table of Contents
+
+- [System Requirements](#system-requirements)
+- [Option 1: Docker Container (Recommended)](#option-1-docker-container-recommended)
+- [Option 2: Docker Compose Setup](#option-2-docker-compose-setup)
+- [Option 3: Self-Hosted Static Web Server](#option-3-self-hosted-static-web-server)
+- [Option 4: Neutralinojs Desktop Application](#option-4-neutralinojs-desktop-application)
+- [Air-Gapped & Offline Isolation Configuration](#air-gapped--offline-isolation-configuration)
+
+---
+
+## System Requirements
+
+### Web & Container Deployments
+*   **Modern Web Browsers:** Google Chrome 90+, Mozilla Firefox 90+, Microsoft Edge 90+, Apple Safari 15+. Note that PWA and Service Worker features require HTTPS or a `localhost` origin for security enforcement.
+*   **Docker Daemon:** Docker Engine 20.10+ (for Docker container deployment).
+*   **System Resources:** Minimum 512 MB RAM, 100 MB disk space.
+
+### Desktop Application Compilation
+*   **Operating Systems:** Windows 10+ (x64), Ubuntu 20.04+ (x64 / ARM64), macOS 11+ (Universal Apple Silicon/Intel).
+*   **Node.js Runtime Environment:** v16.0.0 or later (includes npm package manager).
+*   **System Resources:** Minimum 256 MB RAM, 50 MB disk space.
+
+---
+
+## Option 1: Docker Container (Recommended)
+
+Deploy the application using the official Docker image hosted on the GitHub Container Registry (GHCR).
+
+### Running the Container
+Execute the following command to start a detached container that redirects host port `8080` to container port `80`:
+
+```bash
+docker pull ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+docker run -d \
+  --name markdown-viewer \
+  -p 8080:80 \
+  --restart unless-stopped \
+  ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+```
+
+Open **http://localhost:8080** in your browser.
+
+### Adjusting Ports
+To map the application to a different port (such as `9000`), modify the left side of the `-p` parameter:
+
+```bash
+docker pull ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+docker run -d \
+  --name markdown-viewer \
+  -p 9000:80 \
+  --restart unless-stopped \
+  ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+```
+
+### Image Tags
+
+| Tag Name | Production Ready | Target Source Branch | Target Architecture |
+| :--- | :--- | :--- | :--- |
+| `latest` | Yes (Stable Release) | `main` branch (Release tag) | `linux/amd64`, `linux/arm64` |
+| `main` | No (Development) | `main` branch (Commit updates) | `linux/amd64`, `linux/arm64` |
+| `<commit-sha>` | Pinned | Specific Git commit hash | `linux/amd64`, `linux/arm64` |
+
+---
+
+## Option 2: Docker Compose Setup
+
+For local deployments and multi-container environments, use Docker Compose.
+
+### 1. Clone the Codebase
+```bash
+git clone https://github.com/ThisIs-Developer/Markdown-Viewer.git
+cd Markdown-Viewer
+```
+
+### 2. Launch the Application
+Start the container using Compose:
+```bash
+docker compose up -d
+```
+
+### 3. Verify Container Status
+Confirm the container is running:
+```bash
+docker compose ps
+```
+
+### 4. Stop the Container
+```bash
+docker compose down
+```
+
+### Default `docker-compose.yml` Configuration
+```yaml
+services:
+  markdown-viewer:
+    image: ghcr.io/thisis-developer/markdown-viewer:sha-15eafb0
+    container_name: markdown-viewer
+    ports:
+      - "8080:80"
+    restart: unless-stopped
+```
+
+To build a local image instead of pulling the published container, update the `image` field with a `build` directive:
+```yaml
+services:
+  markdown-viewer:
+    build: .
+    container_name: markdown-viewer
+    ports:
+      - "8080:80"
+    restart: unless-stopped
+```
+
+---
+
+## Option 3: Self-Hosted Static Web Server
+
+Because Markdown Viewer is a fully client-side application, you can serve it from any static web server to serve `index.html` on localhost (`127.0.0.1`) by copying `index.html`, `script.js`, `preview-worker.js`, `styles.css`, `sw.js`, and the `assets/` folder.
+
+### Clone the Repository
+```bash
+git clone https://github.com/ThisIs-Developer/Markdown-Viewer.git
+cd Markdown-Viewer
+```
+
+### Serving with Python (Built-in Web Server)
+Run the following command in the repository root to serve the project on localhost:
+```bash
+python3 -m http.server 8080
+```
+
+### Serving with Node.js
+Run the following command in the repository root to serve the project on localhost:
+```bash
+npx serve . -p 8080
+```
+Once started, the application is accessible at **http://localhost:8080** or **http://127.0.0.1:8080**.
+
+### Serving with Nginx
+Copy the project assets to Nginx's HTML folder:
+```bash
+cp -r . /usr/share/nginx/html/
+```
+
+> [!WARNING]
+> Opening the `index.html` file directly in a browser via the `file://` protocol may fail due to browser security restrictions (CORS) that block Web Workers and Service Workers from running locally. Always serve the files using a local web server.
+
+---
+
+## Option 4: Neutralinojs Desktop Application
+
+Markdown Viewer can also run as a native desktop application powered by **Neutralinojs**.
+
+### Downloading Pre-Built Executables
+Download the binary for your platform from the [GitHub Releases Page](https://github.com/ThisIs-Developer/Markdown-Viewer/releases):
+*   **Windows:** `markdown-viewer-win_x64.exe`
+*   **Linux:** `markdown-viewer-linux_x64`
+*   **Linux ARM (Raspberry Pi):** `markdown-viewer-linux_arm64`
+*   **macOS (Apple Silicon/Intel):** `markdown-viewer-mac_universal`
+
+### Building the Desktop Executable from Source
+Follow these steps to build the binaries locally:
+
+1.  Navigate to the desktop application folder:
+    ```bash
+    cd desktop-app
+    ```
+2.  Install dependencies:
+    ```bash
+    npm install
+    ```
+3.  Download Neutralino framework binaries:
+    ```bash
+    node setup-binaries.js
+    ```
+4.  Copy files from the project root into the desktop resource folder:
+    ```bash
+    node prepare.js
+    ```
+5.  Compile the executable:
+    ```bash
+    # Build single-file embedded Windows binary
+    npm run build
+    
+    # Build portable distribution zip files for all platforms
+    npm run build:portable
+    ```
+
+---
+
+## Air-Gapped & Offline Isolation Configuration
+
+By default, the application loads large rendering dependencies (like MathJax and Mermaid) from external CDNs. In secure, offline, or air-gapped environments, these remote scripts will fail to load.
+
+### Setting Up a Fully Offline Build:
+1.  Download the required JavaScript dependencies (see [Configuration](Configuration) for URLs) and save them to a local directory (e.g., `js/libs/`).
+2.  Update the `<script>` and `<link>` tags in `index.html` to reference your local asset paths.
+3.  Synchronize the desktop app folder by running `node prepare.js`.
+4.  Rebuild your Docker image or desktop application.

+ 340 - 0
wiki/Markdown-Reference.md

@@ -0,0 +1,340 @@
+# Markdown Syntax Reference
+
+This page provides a comprehensive guide to writing GitHub-Flavored Markdown (GFM), LaTeX math formulas, and Mermaid diagrams in **Markdown Viewer** (v3.7.4).
+
+---
+
+## Table of Contents
+
+- [Headings](#headings)
+- [Paragraphs & Line Breaks](#paragraphs--line-breaks)
+- [Emphasis & Text Formatting](#emphasis--text-formatting)
+- [Blockquotes](#blockquotes)
+- [Lists & Task Lists](#lists--task-lists)
+- [Code Elements](#code-elements)
+- [Horizontal Rules](#horizontal-rules)
+- [Links & Images](#links--images)
+- [Tables](#tables)
+- [Footnotes](#footnotes)
+- [HTML Sanitized Formatting](#html-sanitized-formatting)
+- [LaTeX Mathematical Formulas](#latex-mathematical-formulas)
+- [Mermaid Diagrams](#mermaid-diagrams)
+- [Emoji & Alerts](#emoji--alerts)
+
+---
+
+## Headings
+
+You can write headings using hash symbols (`#`) at the beginning of a line:
+
+```markdown
+# Heading 1 (Document Title)
+## Heading 2 (Main Section)
+### Heading 3 (Sub-Section)
+#### Heading 4
+##### Heading 5
+###### Heading 6
+```
+
+Alternative Setext syntax for Heading 1 and Heading 2:
+```markdown
+Heading 1
+=========
+
+Heading 2
+---------
+```
+
+---
+
+## Paragraphs & Line Breaks
+
+*   **Paragraphs:** Separate paragraphs with a blank line.
+*   **Line Breaks:** Insert two spaces at the end of a line, or end the line with a backslash (`\`), to start a new line within the same paragraph.
+
+```markdown
+This is paragraph one.
+
+This is paragraph two.
+
+Line one (ended with a backslash)\
+Line two inside the same paragraph.
+
+Line one (ended with two spaces)  
+Line two inside the same paragraph.
+```
+
+---
+
+## Emphasis & Text Formatting
+
+| Style | Syntax | Output |
+| :--- | :--- | :--- |
+| **Italic** | `*text*` or `_text_` | *Italic* |
+| **Bold** | `**text**` or `__text__` | **Bold** |
+| **Bold Italic** | `***text***` | ***Bold Italic*** |
+| **Strikethrough** | `~~text~~` | ~~Strikethrough~~ |
+| **Underline (HTML)** | `<u>text</u>` | <u>Underline</u> |
+| **Highlight (HTML)** | `<mark>text</mark>` | <mark>Highlight</mark> |
+
+---
+
+## Blockquotes
+
+Use the `>` symbol to indent blockquotes:
+
+```markdown
+> This is a blockquote.
+>
+> You can write multiple paragraphs inside a blockquote.
+>
+>> This is a nested blockquote level 2.
+```
+
+---
+
+## Lists & Task Lists
+
+### Unordered Lists
+Use `-`, `*`, or `+` to create bulleted lists:
+```markdown
+- Item A
+- Item B
+  - Nested Item B1
+  - Nested Item B2
+```
+
+### Ordered Lists
+Use numbers followed by a period:
+```markdown
+1. First item
+2. Second item
+   1. Nested ordered item
+```
+
+### GFM Task Checklists
+```markdown
+- [x] Completed task item
+- [ ] Incomplete task item
+- [x] Another completed task
+```
+
+---
+
+## Code Elements
+
+### Inline Code
+Wrap inline code code segments in single backticks:
+```markdown
+Use the `const api = "/v1"` configuration variable to target the endpoint.
+```
+
+### Code Blocks with Syntax Highlighting
+Wrap code blocks in triple backticks and specify the programming language (e.g. `javascript`, `python`, `html`, `css`, `bash`, `yaml`) to enable syntax highlighting:
+
+````markdown
+```javascript
+const greet = (name) => {
+    return `Hello, ${name}!`;
+};
+console.log(greet("Developer"));
+```
+````
+
+---
+
+## Horizontal Rules
+
+Insert three or more hyphens, asterisks, or underscores on a line by themselves to create a divider:
+
+```markdown
+---
+***
+___
+```
+
+---
+
+## Links & Images
+
+### Links
+```markdown
+# Inline link
+[GitHub](https://github.com)
+
+# Link with a title tooltip
+[GitHub](https://github.com "GitHub Homepage")
+
+# Reference link
+[GitHub][github-link]
+
+[github-link]: https://github.com
+```
+
+### Images
+```markdown
+# Standard image
+![Alt text](assets/icon.jpg)
+
+# Image with a title tooltip
+![Alt text](assets/icon.jpg "Logo")
+
+# Reference-style image
+![Alt text][logo-image]
+
+[logo-image]: assets/icon.jpg
+```
+
+---
+
+## Tables
+
+Use vertical bars `|` to separate columns and hyphens `-` to create the header divider. You can align columns using colons `:`:
+
+```markdown
+| Product Name | Quantity | Price |
+| :--- | :---: | ---: |
+| Markdown Editor | 1 | $0.00 |
+| PDF Exporter | 5 | $0.00 |
+| Dynamic Diagrams | 2 | $0.00 |
+```
+
+*   `:---`: Left-aligned (default).
+*   `:---:`: Center-aligned.
+*   `---:`: Right-aligned.
+
+---
+
+## Footnotes
+
+Add footnotes using carets `[^]`:
+
+```markdown
+Here is a sentence with a footnote citation.[^1]
+
+[^1]: This is the text of the footnote displayed at the bottom of the page.
+```
+
+---
+
+## HTML Sanitized Formatting
+
+You can write HTML tags directly in your Markdown files. The application sanitizes HTML using **DOMPurify** to block unsafe code (like `<script>` elements and inline event handlers):
+
+```html
+<details>
+  <summary>Click to expand additional configurations</summary>
+  <p>Here is some additional content tucked inside an HTML details tag.</p>
+</details>
+
+<div class="alert alert-info">
+  This is a custom alert box using Bootstrap styles.
+</div>
+```
+
+---
+
+## LaTeX Mathematical Formulas
+
+LaTeX mathematical equations are typeset in real time using MathJax.
+
+### Inline Math
+Wrap formulas in single dollar signs (`$`):
+```markdown
+The quadratic formula is defined as $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$ when solving equations.
+```
+
+### Block Math
+Wrap formulas in double dollar signs (`$$`):
+```markdown
+$$
+\int_{a}^{b} f(x) \,dx = F(b) - F(a)
+$$
+```
+
+### LaTeX Examples
+
+| Description | Formula Syntax |
+| :--- | :--- |
+| **Fractions** | `\frac{numerator}{denominator}` |
+| **Integrals** | `\int_{lower}^{upper} x^2 \,dx` |
+| **Summations** | `\sum_{i=1}^{n} a_i` |
+| **Matrices** | `\begin{matrix} a & b \\ c & d \end{matrix}` |
+| **Greek Letters** | `\alpha, \beta, \gamma, \theta, \lambda` |
+| **Roots** | `\sqrt{x^2 + y^2}` |
+
+---
+
+## Mermaid Diagrams
+
+Wrap Mermaid syntax in a fenced code block with the `mermaid` language tag:
+
+### Flowchart
+````markdown
+```mermaid
+flowchart TD
+    A[Start Node] --> B{Is Authentication Valid?}
+    B -- Yes --> C[Access granted]
+    B -- No --> D[Access denied]
+```
+````
+
+### Sequence Diagram
+````markdown
+```mermaid
+sequenceDiagram
+    participant Client
+    participant API Worker
+    participant Storage
+    
+    Client->>API Worker: HTTP POST Request (markdown content)
+    API Worker->>Storage: Save file state
+    Storage-->>API Worker: State saved confirmation
+    API Worker-->>Client: HTTP 200 OK Response
+```
+````
+
+### Gantt Chart
+````markdown
+```mermaid
+gantt
+    title Development Sprint Tasks
+    dateFormat YYYY-MM-DD
+    section Parsing Core
+    Web Worker Integration :a1, 2026-06-01, 10d
+    DOM Patching Engine :after a1, 5d
+    section Desktop wrapper
+    Neutralino Shell Integration :2026-06-15, 8d
+```
+````
+
+---
+
+## Emoji & Alerts
+
+### Emoji
+Use emoji shortcodes to insert icons:
+```markdown
+:rocket: :tada: :sparkles: :warning: :memo:
+```
+*   Renders as: 🚀 🎉 ✨ ⚠️ 📝
+
+### GitHub Alerts
+GitHub-style alert callouts are rendered with corresponding styling:
+
+```markdown
+> [!NOTE]
+> This is a general note callout box.
+
+> [!TIP]
+> This is a helpful tip callout box.
+
+> [!IMPORTANT]
+> This is a critical important callout box.
+
+> [!WARNING]
+> This is a warning callout box.
+
+> [!CAUTION]
+> This is a caution callout box.
+```

+ 177 - 0
wiki/Usage-Guide.md

@@ -0,0 +1,177 @@
+# User Operations & Usage Guide
+
+This guide details how to work with the editing workspace, importing flows, exporting tools, and serverless sharing mechanisms in **Markdown Viewer** (v3.7.4).
+
+---
+
+## Table of Contents
+
+- [User Interface Layout](#user-interface-layout)
+- [Workspace Tab Management](#workspace-tab-management)
+- [Importing Documents](#importing-documents)
+- [Exporting & Compiling Documents](#exporting--compiling-documents)
+- [View Modes and Layout Control](#view-modes-and-layout-control)
+- [Theme Configurations](#theme-configurations)
+- [Proportional Scroll Sync](#proportional-scroll-sync)
+- [Content Analytics & Metrics](#content-analytics--metrics)
+- [Serverless URL Hash Sharing](#serverless-url-hash-sharing)
+- [Keyboard Shortcuts Reference](#keyboard-shortcuts-reference)
+
+---
+
+## User Interface Layout
+
+The interface is structured into a header section, formatting toolbar, document tabs, and a resizable workspace.
+
+```
++-----------------------------------------------------------------------+
+|  App Header (.app-header)                                             |
+|  [Tab 1] [Tab 2] [Tab 3]                   [View Modes] [Settings]    |
++-----------------------------------------------------------------------+
+|  Formatting Toolbar (.markdown-format-toolbar)                        |
+|  [Bold] [Italic] [Link] [Image] [Table] [Mermaid] [Math]              |
++-----------------------------------------------------------------------+
+|                                  |                                    |
+|  Editor Pane (.editor-pane)      |  Preview Pane (.preview-pane)      |
+|                                  |                                    |
+|  [Gutter]  # Welcome...          |  # Welcome...                      |
+|    1       This is markdown      |  This is markdown.                 |
+|    2                             |                                    |
+|                                  |                                    |
++-----------------------------------------------------------------------+
+|  Status Bar                                                           |
+|  Words: 4   Chars: 21                          Reading Time: 1 min    |
++-----------------------------------------------------------------------+
+```
+
+*   **Editor Pane (Left):** Monospace editing workspace with a line gutter showing line numbers.
+*   **Preview Pane (Right):** Sandbox layout area rendering GitHub-Flavored Markdown outputs.
+*   **Splitter Bar (Center):** Resizable divider allowing width adjustments. Drag boundaries clamp either pane from scaling below 20% width.
+
+---
+
+## Workspace Tab Management
+
+Markdown Viewer allows you to open and edit multiple documents concurrently.
+
+*   **Create Tabs:** Click the **`+`** button in the tab header bar.
+*   **Rename Tabs:** Double-click a tab label or open the tab menu to change the name.
+*   **Reorder Tabs:** Drag and drop tab components horizontally to reorganize them.
+*   **Duplicate Tabs:** Choose **Duplicate** from the tab's context dropdown to clone the document.
+*   **Delete Tabs:** Click the **`x`** icon on the tab. Deleting the last tab clears editor content and resets state.
+*   **Auto-Save Cache:** Tabs are auto-saved to `localStorage` every 500ms when typing, and flushed immediately during `beforeunload` or `visibilitychange` lifecycle states.
+
+---
+
+## Importing Documents
+
+Load files into the workspace using three different paths:
+
+### 1. Drag & Drop
+Drag a `.md` or `.markdown` text file from your file system and drop it directly onto the editor pane. The contents will overwrite the active tab. A binary safety guard scans the first 8 KB of the file for null bytes (`\x00`) to block corrupted or binary files.
+
+### 2. File Picker
+Click **Import** in the toolbar, select **From files**, and choose one or more Markdown files from your system.
+
+### 3. GitHub Importer
+1.  Click **Import** and select **From GitHub**.
+2.  Paste a public GitHub URL (e.g., repository main page, subdirectory, or direct blob file link):
+    *   `https://github.com/owner/repository`
+    *   `https://github.com/owner/repository/tree/main/src/docs`
+    *   `https://github.com/owner/repository/blob/main/README.md`
+3.  The importer fetches the file tree from GitHub's API.
+4.  In the file selection modal, choose the files you want to import. You can select all files or clear the selection.
+5.  Click **Import Selected** to load the selected files into separate document tabs.
+
+---
+
+## Exporting & Compiling Documents
+
+Export options are available in the **Export** dropdown in the toolbar:
+
+### 1. Raw Markdown (`.md`)
+Saves the raw text buffer of the active tab.
+
+### 2. Standalone HTML (`.html`)
+Generates a self-contained HTML file. It bundles the compiled Markdown content, highlights code blocks, renders diagrams, and inlines GitHub-markdown styles so the document displays correctly offline.
+
+### 3. Compiled PDF (`.pdf`)
+Generates a PDF using `jsPDF` and `html2canvas` via an off-screen sandbox. It converts SVG diagrams to rasters, scales oversized elements, and runs a cascade pagination loop to keep headings with their sections and prevent text lines from being cut in half.
+
+> [!TIP]
+> For the highest PDF rendering quality, use your browser's built-in Print functionality (`Ctrl+P` or `Cmd+P`) and select "Save as PDF".
+
+---
+
+## View Modes and Layout Control
+
+Configure the workspace layout to fit your current writing context:
+
+| View Mode | Toolbar Icon | Layout Description |
+| :--- | :---: | :--- |
+| **Split View** | `⬜⬜` | Dual-pane side-by-side editing and previewing (Default desktop view). |
+| **Editor Only** | `⬜` | Single pane showing only the editor for distraction-free writing. |
+| **Preview Only** | `◼` | Single pane showing only the compiled HTML output for reading. |
+
+*   **Mobile Layout Auto-Collapse:** On viewports below 768px wide, split mode is disabled. The application displays either the editor or the preview, toggling via the view mode icons.
+
+---
+
+## Theme Configurations
+
+Switch themes using the toggle icon in the toolbar:
+*   **Light Theme:** White background matching standard GitHub styling.
+*   **Dark Theme:** Dark theme (`#0d1117`) matching GitHub Dark.
+
+Theme variables default to system preferences and are written to the document root class attribute.
+
+---
+
+## Proportional Scroll Sync
+
+When working in Split View, scrolling the editor or preview will automatically update the opposite pane:
+*   **Scroll Sync Toggle:** Enable or disable synchronization via the scroll lock toggle in the toolbar.
+*   **Ratio-Based Calculations:** Computes relative scroll offsets based on total scrollable heights to keep text and headers aligned.
+*   **Feedback Mitigation:** Employs scroll event locks and frame scheduling via `requestAnimationFrame` to prevent circular updates.
+
+---
+
+## Content Analytics & Metrics
+
+Toggle the **📊 Stats** view in the toolbar to display:
+*   **Word Count:** Total count of space-separated strings.
+*   **Character Count:** Total count of character bytes, excluding whitespace.
+*   **Lines:** Total line count of the document.
+*   **Estimated Reading Time:** Calculated at an average speed of 200 words per minute.
+
+---
+
+## Serverless URL Hash Sharing
+
+Create serverless, database-free sharing links using the **Share** button in the toolbar:
+1.  Markdown text is compressed using `Pako.js` (zlib DEFLATE).
+2.  Data is converted to a URL-safe Base64 string (replacing `+` with `-`, `/` with `_`, and removing trailing `=` padding).
+3.  The string is appended as a URL hash fragment: `http://domain/#share=<payload>`.
+4.  Copy and share this link. Opening it decodes and decompresses the hash to load the document in the editor.
+5.  A warning is displayed if the generated URL exceeds 32,000 characters.
+
+---
+
+## Keyboard Shortcuts Reference
+
+The following shortcut keys are active inside the editor:
+
+| Action | Windows / Linux | macOS |
+| :--- | :--- | :--- |
+| **Export raw Markdown** | `Ctrl + S` | `⌘ + S` |
+| **Copy Rich HTML** | `Ctrl + C` (with no text selected) | `⌘ + C` (with no text selected) |
+| **Toggle Scroll Sync** | `Ctrl + Shift + S` | `⌘ + Shift + S` |
+| **Open a New Tab** | `Ctrl + T` | `⌘ + T` |
+| **Close the Active Tab** | `Ctrl + W` | `⌘ + W` |
+| **Open Find & Replace** | `Ctrl + F` | `⌘ + F` |
+| **Undo Last Edit** | `Ctrl + Z` | `⌘ + Z` |
+| **Redo Last Edit** | `Ctrl + Shift + Z` / `Ctrl + Y` | `⌘ + Shift + Z` / `⌘ + Y` |
+| **Insert Code Block** | `Ctrl + Shift + C` | `⌘ + Shift + C` |
+| **Toggle Fullscreen Editor** | `F11` | `F11` |
+| **Insert 2-space Indent** | `Tab` | `Tab` |
+| **Outdent Line** | `Shift + Tab` | `Shift + Tab` |