# Monospace Font Embedding Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Guarantee ASCII alignment in MarkdownConverter's preview **and** every supported export format (PDF, DOCX, HTML, EPUB, LaTeX, ODT, RTF) by bundling JetBrains Mono + Fira Code TTFs inside the app and embedding them in every output. No OS font dependency, no internet required. **Architecture:** `MonospaceFontConfig` is the single source of truth for the active monospace family + weight. It maps to bundled TTF paths (dev vs packaged/asar.unpacked). `PdfFontHeader`, `DocxFontEmbedder`, `EpubFontEmbedder`, and `ExportCss` are the four per-format adapters. Body-class toggles in the renderer flip a CSS custom property consumed by `styles-modern.css` and friends. **Tech Stack:** Electron (main + renderer + preload), CommonJS, vanilla JS. `jszip ^3.10.1` (already a dep) for DOCX/EPUB surgery. `fontspec` via xelatex for PDF. No new NPM dependencies. **Spec:** `docs/superpowers/specs/2026-06-30-monospace-font-embedding-design.md` --- ## File Structure **New files (8 + 3 font assets):** ``` src/main/MonospaceFontConfig.js # active family → TTF path resolver src/main/PdfFontHeader.js # builds xelatex fontspec header src/main/DocxFontEmbedder.js # embeds TTF into pandoc DOCX output src/main/EpubFontEmbedder.js # wraps pandoc --epub-embed-font + manifest patch src/main/ExportCss.js # @font-face with base64-woff2, self-contained src/main/settings/monospaceSettings.js # shared read/default of monospace settings tests/monospace-font-config.test.js tests/pdf-font-header.test.js tests/docx-font-embedder.test.js tests/epub-font-embedder.test.js tests/export-css.test.js tests/monospace-settings.test.js assets/fonts/FiraCode-Regular.ttf # new asset assets/fonts/FiraCode-Bold.ttf # new asset assets/fonts/FiraCode-LICENSE.txt # new asset ``` **Modified files:** ``` src/fonts.css # add Fira Code @font-face src/styles/tokens.css # add --font-mono-active + --font-mono-feature tokens src/styles-concreteinfo.css # class-driven token values src/styles-modern.css # .editor-textarea, .preview-content code/pre use new tokens src/ascii-generator.html # swap Google Fonts CDN for local fonts.css src/print-preview.js # embed ExportCss in iframe srcdoc src/main.js # 5 export pipelines + asar config + pandoc version flag detection src/renderer.js # body-class toggle on settings change src/preload.js # expose monospaceSettings IPC scripts/download-tools.js # add Fira Code downloader package.json # build.asarUnpack + Fira font files ``` **Convention note:** All new modules are CommonJS (`require` / `module.exports`). Tests are in `tests/`, follow the existing pattern (require the module directly, no electron mocks for these). --- ## Phase A — Settings + path resolver ### Task 1: Settings schema with safe defaults **Files:** - Create: `src/main/settings/monospaceSettings.js` - Test: `tests/monospace-settings.test.js` - [ ] **Step 1: Write the failing test** ```js // tests/monospace-settings.test.js const { getDefaults, getActiveMonoFont, isLigaturesEnabled } = require('../src/main/settings/monospaceSettings'); describe('monospaceSettings', () => { test('getDefaults returns sane defaults', () => { const d = getDefaults(); expect(d.monospaceFont).toBe('jetbrains-mono'); expect(d.monospaceLigatures).toBe(false); }); test('getActiveMonoFont returns the active family', () => { expect(getActiveMonoFont({ monospaceFont: 'fira-code' })).toBe('Fira Code'); expect(getActiveMonoFont({})).toBe('JetBrains Mono'); expect(getActiveMonoFont({ monospaceFont: 'bogus' })).toBe('JetBrains Mono'); }); test('isLigaturesEnabled reads boolean strictly', () => { expect(isLigaturesEnabled({ monospaceLigatures: true })).toBe(true); expect(isLigaturesEnabled({ monospaceLigatures: false })).toBe(false); expect(isLigaturesEnabled({})).toBe(false); expect(isLigaturesEnabled({ monospaceLigatures: 'yes' })).toBe(false); }); }); ``` - [ ] **Step 2: Run the test, verify it fails** Run: `npm test -- tests/monospace-settings.test.js` Expected: FAIL — `Cannot find module '../src/main/settings/monospaceSettings'` - [ ] **Step 3: Implement** ```js // src/main/settings/monospaceSettings.js 'use strict'; const FAMILY_BY_KEY = { 'jetbrains-mono': 'JetBrains Mono', 'fira-code': 'Fira Code', }; function getDefaults() { return Object.freeze({ monospaceFont: 'jetbrains-mono', monospaceLigatures: false }); } function getActiveMonoFont(settings) { const key = settings && settings.monospaceFont; return FAMILY_BY_KEY[key] || 'JetBrains Mono'; } function isLigaturesEnabled(settings) { return Boolean(settings && settings.monospaceLigatures === true); } module.exports = { getDefaults, getActiveMonoFont, isLigaturesEnabled, FAMILY_BY_KEY }; ``` - [ ] **Step 4: Run the test, verify it passes** Run: `npm test -- tests/monospace-settings.test.js` Expected: PASS — 3 tests - [ ] **Step 5: Commit** ```bash git add src/main/settings/monospaceSettings.js tests/monospace-settings.test.js git commit -m "feat(monospace): add settings schema + safe defaults getDefaults(), getActiveMonoFont(), isLigaturesEnabled() with TDD." ``` --- ### Task 2: `MonospaceFontConfig` resolves dev + packaged TTF paths **Files:** - Create: `src/main/MonospaceFontConfig.js` - Test: `tests/monospace-font-config.test.js` - [ ] **Step 1: Write the failing test** ```js // tests/monospace-font-config.test.js jest.mock('electron', () => ({ app: { getPath: () => '/fake/userData' }, })); jest.mock('fs', () => ({ existsSync: jest.fn(), statSync: jest.fn() })); const path = require('path'); const fs = require('fs'); const MonospaceFontConfig = require('../src/main/MonospaceFontConfig'); describe('MonospaceFontConfig', () => { afterEach(() => { jest.clearAllMocks(); }); test('returns dev repo path when no packaged layout exists', () => { fs.existsSync.mockReturnValue(false); const p = MonospaceFontConfig.getMonoFontTtfPath('jetbrains-mono', 400); expect(p).toMatch(/assets\/fonts\/JetBrainsMono-Regular\.ttf$/); }); test('returns packaged asar.unpacked path when present and file exists', () => { fs.existsSync.mockImplementation((p) => p.includes('app.asar.unpacked') && p.endsWith('FiraCode-Regular.ttf')); const p = MonospaceFontConfig.getMonoFontTtfPath('fira-code', 400); expect(p).toContain('app.asar.unpacked'); expect(p).toContain('FiraCode-Regular.ttf'); }); test('returns null and warns when file is missing', () => { const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); fs.existsSync.mockReturnValue(false); const p = MonospaceFontConfig.getMonoFontTtfPath('fira-code', 700); expect(p).toBeNull(); expect(warn).toHaveBeenCalledWith(expect.stringMatching(/FiraCode-Bold\.ttf/)); warn.mockRestore(); }); test('ligaturesEnabled maps from settings', () => { expect(MonospaceFontConfig.ligaturesEnabled({ monospaceLigatures: true })).toBe(true); expect(MonospaceFontConfig.ligaturesEnabled({ monospaceLigatures: false })).toBe(false); expect(MonospaceFontConfig.ligaturesEnabled({})).toBe(false); }); }); ``` - [ ] **Step 2: Run the test, verify it fails** Run: `npm test -- tests/monospace-font-config.test.js` Expected: FAIL — `Cannot find module` - [ ] **Step 3: Implement** ```js // src/main/MonospaceFontConfig.js 'use strict'; const fs = require('fs'); const path = require('path'); const { app } = require('electron'); const { getActiveMonoFont, isLigaturesEnabled, FAMILY_BY_KEY } = require('./settings/monospaceSettings'); const WEIGHT_BY_KEY = { 300: 'Light', 400: 'Regular', 500: 'Medium', 600: 'SemiBold', 700: 'Bold' }; function getAppRoot() { // __dirname in packaged builds = .../app.asar/src/main; we need the app dir. // process.resourcesPath points to the unpacked root on all platforms. if (process.resourcesPath && fs.existsSync(path.join(process.resourcesPath, 'app.asar.unpacked'))) { return process.resourcesPath; } // Dev: walk up from src/main to repo root. return path.resolve(__dirname, '..', '..'); } function getCandidatePaths(family, weight) { const familyDir = family === 'Fira Code' ? 'FiraCode' : 'JetBrainsMono'; const weightName = WEIGHT_BY_KEY[weight] || 'Regular'; const filename = `${familyDir}-${weightName}.ttf`; const candidates = []; // 1. Repo dev path (used in `npm start`) candidates.push(path.resolve(getAppRoot(), 'assets', 'fonts', filename)); // 2. Packaged asar.unpacked const packagedRoot = process.resourcesPath || getAppRoot(); candidates.push(path.join(packagedRoot, 'app.asar.unpacked', 'assets', 'fonts', filename)); return candidates; } function getMonoFontTtfPath(familyKey, weight = 400) { const family = FAMILY_BY_KEY[familyKey] || 'JetBrains Mono'; for (const p of getCandidatePaths(family, weight)) { if (fs.existsSync(p)) return p; } console.warn(`[MonospaceFontConfig] bundled font missing for ${family} weight ${weight}; falling back to system`); return null; } function ligaturesEnabled(settings) { return isLigaturesEnabled(settings); } function getActiveFamily(settings) { return getActiveMonoFont(settings); } module.exports = { getMonoFontTtfPath, ligaturesEnabled, getActiveFamily }; ``` - [ ] **Step 4: Run the test, verify it passes** Run: `npm test -- tests/monospace-font-config.test.js` Expected: PASS — 4 tests - [ ] **Step 5: Commit** ```bash git add src/main/MonospaceFontConfig.js tests/monospace-font-config.test.js git commit -m "feat(monospace): add MonospaceFontConfig path resolver Resolves dev vs packaged (asar.unpacked) TTF paths. Logs warn, returns null when bundled font is missing." ``` --- ## Phase B — Bundled fonts ### Task 3: Add Fira Code TTF + LICENSE assets **Files:** - Create: `assets/fonts/FiraCode-Regular.ttf` - Create: `assets/fonts/FiraCode-Bold.ttf` - Create: `assets/fonts/FiraCode-LICENSE.txt` - [ ] **Step 1: Confirm JetBrains Mono assets are tracked** Run: `git ls-files assets/fonts/JetBrainsMono-*` — expect at minimum Regular, Bold, LICENSE listed. If `JetBrainsMono-Regular.ttf` and `JetBrainsMono-Bold.ttf` are still untracked (from prior session), `git add` them now. - [ ] **Step 2: Download Fira Code TTFs + license from upstream release** Run: `node scripts/download-tools.js --font fira-code` Expected: writes `assets/fonts/FiraCode-Regular.ttf`, `FiraCode-Bold.ttf`, `FiraCode-LICENSE.txt`. If the helper script doesn't accept `--font fira-code` yet, do this temporarily: ```bash curl -L -o assets/fonts/FiraCode-Regular.ttf \ https://github.com/tonsky/FiraCode/raw/master/distr/ttf/FiraCode-Regular.ttf curl -L -o assets/fonts/FiraCode-Bold.ttf \ https://github.com/tonsky/FiraCode/raw/master/distr/ttf/FiraCode-Bold.ttf curl -L -o assets/fonts/FiraCode-LICENSE.txt \ https://raw.githubusercontent.com/tonsky/FiraCode/master/LICENSE ``` - [ ] **Step 3: Verify file sizes are reasonable** Run: `ls -la assets/fonts/Fira*` — each TTF should be roughly 100–500 KB. - [ ] **Step 4: Commit** ```bash git add assets/fonts/FiraCode-Regular.ttf assets/fonts/FiraCode-Bold.ttf assets/fonts/FiraCode-LICENSE.txt assets/fonts/JetBrainsMono-Regular.ttf assets/fonts/JetBrainsMono-Bold.ttf assets/fonts/JetBrainsMono-LICENSE.txt git commit -m "feat(monospace): bundle JetBrainsMono + FiraCode TTF assets Both families are SIL OFL. TTF (not just woff2) is required so xelatex can embed into PDF and jszip can inject into DOCX." ``` --- ### Task 4: Extend `scripts/download-tools.js` with Fira Code support **Files:** - Modify: `scripts/download-tools.js` - [ ] **Step 1: Read existing tool and identify the pandoc download pattern** Look for how `download-pandoc` is structured (URL, version pin, mkdir, writeFile). - [ ] **Step 2: Add Fira Code section** Add a new exported function near the existing tools: ```js async function downloadFiraCode() { const base = 'https://github.com/tonsky/FiraCode/raw/master/distr/ttf'; const targets = [ { url: `${base}/FiraCode-Regular.ttf`, out: 'FiraCode-Regular.ttf' }, { url: `${base}/FiraCode-Bold.ttf`, out: 'FiraCode-Bold.ttf' }, ]; fs.mkdirSync('assets/fonts', { recursive: true }); for (const t of targets) { await downloadTo(t.url, path.join('assets/fonts', t.out)); } await downloadTo( 'https://raw.githubusercontent.com/tonsky/FiraCode/master/LICENSE', path.join('assets/fonts', 'FiraCode-LICENSE.txt') ); } ``` - [ ] **Step 3: Wire into the existing CLI dispatcher** If the script uses `if (cmd === '...')` blocks, add `else if (cmd === 'fira-code' || cmd === '--font fira-code') downloadFiraCode();`. If it auto-runs every helper, no extra wiring needed. - [ ] **Step 4: Run it as a smoke test** Run: `node scripts/download-tools.js fira-code` (or whatever your dispatch is) — verify no errors; rerun Task 3's `ls` to confirm files exist. - [ ] **Step 5: Commit** ```bash git add scripts/download-tools.js git commit -m "chore(monospace): extend download-tools with Fira Code downloader Matches the existing version-pinned approach for Pandoc." ``` --- ## Phase C — CSS layer ### Task 5: Add Fira Code `@font-face` in `src/fonts.css` **Files:** - Modify: `src/fonts.css` (append after the existing JetBrains Mono block) - [ ] **Step 1: Add the new `@font-face` rules** Append at the end of `src/fonts.css`: ```css /* Fira Code Font Family — bundled with the app */ @font-face { font-family: 'Fira Code'; font-style: normal; font-weight: 400; font-display: swap; src: url('../assets/fonts/FiraCode-Regular.woff2') format('woff2'), url('../assets/fonts/FiraCode-Regular.ttf') format('truetype'); } @font-face { font-family: 'Fira Code'; font-style: normal; font-weight: 700; font-display: swap; src: url('../assets/fonts/FiraCode-Bold.woff2') format('woff2'), url('../assets/fonts/FiraCode-Bold.ttf') format('truetype'); } ``` (`FiraCode-Regular.woff2` and `FiraCode-Bold.woff2` are optional. Chromium will pick whichever loads first; without them Chromium falls back to TTF. Skip the woff2 line if those files don't exist.) - [ ] **Step 2: Visual smoke test in `npm start`** Run: `npm start`. In the editor, set font-family on a paragraph to `'Fira Code', monospace` via DevTools; verify the active font shows the Fira signature slightly taller x-height. Revert the DevTools change. - [ ] **Step 3: Commit** ```bash git add src/fonts.css git commit -m "feat(monospace): register Fira Code @font-face in renderer Two weights: 400 (Regular) and 700 (Bold). Falls back to TTF if woff2 isn't bundled." ``` --- ### Task 6: CSS tokens for active family + ligatures **Files:** - Modify: `src/styles/tokens.css` - Modify: `src/styles-concreteinfo.css` - [ ] **Step 1: Read existing tokens and find the `--font-mono` declaration** ```bash grep -n "font-mono" src/styles/tokens.css src/styles-concreteinfo.css ``` Note the line where `--font-mono` is defined. - [ ] **Step 2: Add the two new tokens** In `src/styles/tokens.css`, add right after `--font-mono`: ```css --font-mono-active: 'JetBrains Mono', monospace; --font-mono-feature: 'liga' 0, 'calt' 0, 'dlig' 0; ``` - [ ] **Step 3: Add body-class overrides in `src/styles-concreteinfo.css`** Append: ```css body.mono-fira { --font-mono-active: 'Fira Code', monospace; } body.mono-fira.mono-ligatures-on { --font-mono-feature: normal; } body.mono-jetbrains { --font-mono-active: 'JetBrains Mono', monospace; } body.mono-ligatures-on { --font-mono-feature: normal; } ``` (The `.mono-jetbrains` family rule is the default; the explicit declaration is for clarity and to make Lighthouse detect both branches.) - [ ] **Step 4: Update the existing `--font-mono` declaration to use the new token** Find the line that says `--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;` and replace with: ```css --font-mono: var(--font-mono-active); ``` - [ ] **Step 5: Commit** ```bash git add src/styles/tokens.css src/styles-concreteinfo.css git commit -m "feat(monospace): add --font-mono-active / --font-mono-feature tokens Body classes (.mono-fira, .mono-ligatures-on) flip the tokens for live switching without re-rendering." ``` --- ### Task 7: `styles-modern.css` honours the new tokens **Files:** - Modify: `src/styles-modern.css` - [ ] **Step 1: Replace hard-coded font strings with the token** At each occurrence of a literal `'JetBrains Mono'` or `'JetBrains Mono', 'Fira Code', monospace`, replace with `var(--font-mono-active)`. Specifically: ```diff - font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Courier New', monospace; + font-family: var(--font-mono-active); ``` Apply to: - `.editor-textarea` - `.codemirror-container .cm-editor` - `.preview-content code` - `.preview-content pre` - `.source-code` / any pre/code block found in `styles-modern.css` - [ ] **Step 2: Add ligature control to pre/code/textarea** Append a new rule at the end of the file: ```css .editor-textarea, .preview-content code, .preview-content pre, .codemirror-container .cm-editor { font-feature-settings: var(--font-mono-feature); } ``` - [ ] **Step 3: Manual visual check** `npm start`. In DevTools, add `body.mono-fira` to `` — confirm family flips; add `body.mono-ligatures-on` — confirm ligatures enable. Revert. - [ ] **Step 4: Commit** ```bash git add src/styles-modern.css git commit -m "feat(monospace): wire preview + editor to --font-mono-active token" ``` --- ### Task 8: Renderer toggles body classes on settings change **Files:** - Modify: `src/renderer.js` - [ ] **Step 1: Locate the existing settings-changed handler** ```bash grep -n "settings-changed\|on('settings\|applyTheme\|currentSettings\|loadSettings" src/renderer.js | head ``` Pick the function that already applies user preferences on init / change. (Examples: `applyThemeSettings`, `applyUserSettings`, or wherever header/footer settings are wired.) - [ ] **Step 2: Implement the body-class flip** Inside the settings-applied function, add: ```js function applyMonospaceClasses(settings) { const body = document.body; body.classList.toggle('mono-jetbrains', settings.monospaceFont !== 'fira-code'); body.classList.toggle('mono-fira', settings.monospaceFont === 'fira-code'); body.classList.toggle('mono-ligatures-on', settings.monospaceLigatures === true); body.classList.toggle('mono-ligatures-off', settings.monospaceLigatures !== true); } ``` Call `applyMonospaceClasses(currentSettings)` on initial load (wherever other current-settings are applied). Call it again inside the existing settings-changed listener. - [ ] **Step 3: Request user contribution (learning mode)** **Your turn:** between initial-load application and the settings-changed listener, the exact wiring depends on the codebase's existing structure. Open `src/renderer.js`, find both call sites, and place the two calls to `applyMonospaceClasses(...)`. Trade-off: tightest coupling is right next to the other settings reads, loosest is a dedicated effect block — pick whichever matches the surrounding code's idiom. - [ ] **Step 4: Manual smoke test** `npm start`. Open Settings (if it exists in the UI yet) — otherwise toggle the class via DevTools to confirm CSS responds. Revert when done. - [ ] **Step 5: Commit** ```bash git add src/renderer.js git commit -m "feat(monospace): renderer toggles body classes on settings change" ``` --- ## Phase D — Helpers for non-CSS surfaces ### Task 9: ASCII Generator window uses local fonts **Files:** - Modify: `src/ascii-generator.html` - [ ] **Step 1: Replace the Google Fonts `` with local `fonts.css`** In `src/ascii-generator.html`, find the `` near line 8 and replace with: ```html ``` `fonts.css` already wires `@font-face` for both JetBrains Mono and Fira Code (after Task 5). - [ ] **Step 2: Ensure the window body has the right default class** Around the `` tag, add the default class: ```html ``` (If the file is opened by the renderer with explicit settings, the opener script can override via `document.body.classList.add/remove` — that wiring lives in the export-opening code path; consult `src/main.js` around `show-ascii-generator` for the open path.) - [ ] **Step 3: Ensure CSS fallback is local** Find the `.preview-content` rule (around line 142 of the file). Confirm it sets `font-family: 'JetBrains Mono', monospace;` or update it to use `var(--font-mono-active)`. If you reference the token, ensure the file links `tokens.css` or inlines the variable definition. Quick fix if you want the simplest behaviour: ```css .preview-content { font-family: 'JetBrains Mono', monospace; font-feature-settings: 'liga' 0, 'calt' 0, 'dlig' 0; } ``` - [ ] **Step 4: Verify ascii-generator still works** Run: `npm start`. Open the ASCII generator from the menu (or `Ctrl+Alt+A` if there's a binding). Confirm preview text uses JetBrains Mono and aligns correctly. - [ ] **Step 5: Commit** ```bash git add src/ascii-generator.html git commit -m "fix(ascii): replace Google Fonts CDN with local fonts.css ASCII generator now renders in bundled JetBrains Mono without internet, matching the preview pane." ``` --- ### Task 10: `ExportCss` — self-contained CSS with embedded woff2 **Files:** - Create: `src/main/ExportCss.js` - Test: `tests/export-css.test.js` - [ ] **Step 1: Write the failing test** ```js // tests/export-css.test.js const fs = require('fs'); const ExportCss = require('../src/main/ExportCss'); describe('ExportCss.build', () => { const fakeFontPath = require('path').join(__dirname, 'fixtures', 'fake.woff2'); const fixture = Buffer.from('woff2-binary-fake-data'); beforeAll(() => { fs.mkdirSync(require('path').dirname(fakeFontPath), { recursive: true }); fs.writeFileSync(fakeFontPath, fixture); }); afterAll(() => fs.rmSync(require('path').dirname(fakeFontPath), { recursive: true, force: true })); test('emits a self-contained CSS with embedded @font-face', () => { const css = ExportCss.build({ activeFontPath: fakeFontPath, family: 'JetBrains Mono', weight: 400, ligatures: false }); expect(css).toMatch(/@font-face\s*\{[^}]*src:\s*url\('data:font\/woff2;base64,/); expect(css).toContain('font-family: \'JetBrains Mono\''); expect(css).toMatch(/font-feature-settings:[^;]*liga[^;]*0/); }); test('falls back to family-only CSS when font path is missing', () => { const css = ExportCss.build({ activeFontPath: null, family: 'Fira Code', weight: 700, ligatures: true }); expect(css).not.toContain('data:font/woff2;'); expect(css).toContain('font-family: \'Fira Code\''); }); }); ``` - [ ] **Step 2: Run, fail** Run: `npm test -- tests/export-css.test.js` Expected: FAIL — `Cannot find module` - [ ] **Step 3: Implement** ```js // src/main/ExportCss.js 'use strict'; const fs = require('fs'); function toDataUri(filePath) { const buf = fs.readFileSync(filePath); return `data:font/woff2;base64,${buf.toString('base64')}`; } function build({ activeFontPath, family, weight = 400, ligatures = false }) { const features = ligatures ? 'normal' : "'liga' 0, 'calt' 0, 'dlig' 0"; const faceBlock = activeFontPath ? `@font-face { font-family: '${family}'; font-weight: ${weight}; font-style: normal; font-display: swap; src: url('${toDataUri(activeFontPath)}') format('woff2'); } ` : ''; return `${faceBlock}code, pre, kbd, samp { font-family: '${family}', monospace; font-feature-settings: ${features}; } pre, code { white-space: pre; tab-size: 4; } `; } module.exports = { build }; ``` (Note: for the test, the file needs to look like woff2 to `_not_` matter — the encode is the same. The real `activeFontPath` in production will be the actual `.woff2` file shipped with the app. There's no need to separately ship a `.ttf` here — `ExportCss` embeds the woff2.) - [ ] **Step 4: Run, pass** Run: `npm test -- tests/export-css.test.js` Expected: PASS — 2 tests - [ ] **Step 5: Commit** ```bash git add src/main/ExportCss.js tests/export-css.test.js git commit -m "feat(monospace): ExportCss embeds woff2 as base64 in CSS Self-contained CSS for HTML export and print-preview iframe." ``` --- ### Task 11: `print-preview.js` injects the ExportCss **Files:** - Modify: `src/print-preview.js` - [ ] **Step 1: Locate the srcdoc-building function** The file already has a `previewHtml` template literal around line 82. Inside its `