# Plan 01-12 (Design Integration) — Research **Researched:** 2026-05-17 **Domain:** Brand integration — font self-hosting, SVG rasterization, MV3 i18n, CSS token ingestion, Vite asset pipeline **Confidence:** HIGH on stack/tooling (locally verified); MEDIUM on Cyrillic-extended-Plex sizing; **BLOCKER** on D-05 Newsreader Cyrillic coverage ## Summary Designer-team handoff v1 (commit `5efc2a8`) lands a 280-line `tokens.css`, 32×32-viewBox `mokosh-mark.svg`, and 240×56 lockup. Engineering's Plan 01-12 ingests these into `src/`, removes the Google Fonts `@import` (MV3 CSP violation), self-hosts WOFF2 (Newsreader + IBM Plex Sans 4w + Plex Mono 2w, Latin + Cyrillic), rasterizes the mark to `icons/icon{16,48,128}.png`, updates `manifest.json:name`+`:description` per D-07/D-08 (via `__MSG_*__` + `_locales/`), wires the 8 operator strings into `_locales/ru/messages.json` + `_locales/en/messages.json`, restyles `src/popup/` per tokens, and replaces hardcoded Russian/placeholder colors with token references. **Primary recommendation:** Execute in stable order — (1) write the canonical `tokens.css` to `src/shared/tokens.css` minus the Google Fonts `@import`, (2) rasterize PNG icons (rsvg-convert is installed + verified to produce icons that clear all Chrome floors with the existing mark at 16/48/128), (3) build out `fonts/` via `pyftsubset` (installed) + `woff2_compress` (installed) — BUT ONLY for the Plex faces and a Latin-only Newsreader subset; **substitute a Cyrillic-bearing display serif for Russian text** (see BLOCKER below), (4) create `_locales/` and migrate hardcoded Russian strings, (5) wire `manifest.json:name=__MSG_extName__` + `:description=__MSG_extDesc__` + `default_locale:"en"`, (6) restyle popup + smoke. **BLOCKER (D-05):** Newsreader from Google Fonts / Production Type does NOT cover Cyrillic. The Decision Brief's own embedded `@font-face` declarations confirm this — Newsreader has only `latin / latin-ext / vietnamese` subsets; IBM Plex Sans and Plex Mono have `cyrillic / cyrillic-ext` subsets in addition. Russian-operator audience means display text in Newsreader will silently fall back to `Iowan Old Style → Times New Roman → serif`. See §1 BLOCKER for remediation options. ## User Constraints (from CONTEXT.md / brand-decisions-v1.md) ### Locked Decisions - **D-01**: Mark concept = `A · Loom (2×2 weave intersection)`. Source SVG is `mokosh-mark.svg`. PNG rasterization is engineering's job. - **D-02**: Welcome layout = `A · Hero + Loom dial`. Out of scope for Plan 01-12 (Plan 01-10's responsibility); Plan 01-12 must NOT touch `src/welcome/` content. - **D-03**: Voice register = `A · Sober`. Carries to all 8 i18n strings. - **D-04**: Palette = `A · Loom` (`tokens.css` `--mks-*` is canonical; replaces engineering placeholders). - **D-05**: Type pairing = `A · Newsreader (display) + IBM Plex Sans (UI, 4 weights) + IBM Plex Mono (mono, 2 weights)`. All OFL. Self-host WOFF2; remove Google Fonts `@import`. — **subject to BLOCKER**. - **D-06**: Icon strategy = `A · Neutral mark + dynamic badge`. NO per-state PNG sets. Matches existing arch (`chrome.action.setBadgeBackgroundColor` already in use per `src/background/index.ts:45`). - **D-07 (USER OVERRIDE)**: `manifest.json:name = "Mokosh — Session Capture"` (not bare `"Mokosh"`). Surface capture purpose externally. - **D-08**: Tagline = `«Тридцать секунд назад, всегда под рукой.»` / `"Thirty seconds ago, always at hand."`. Used in `manifest.json:description` + welcome hero. - **D-09**: Smoke shipping = `A · Dev-only behind VITE_DEV flag`. — **subject to a no-op clarification**, see §12. ### Claude's Discretion - The 8 i18n copy strings inherit D-03 Sober by default; per-string overrides surfaced as-encountered. Initial values for each string extracted from Decision Brief §02 — see §10 below for the verbatim text and recommended `__MSG_*__` keys. - File layout: `src/shared/tokens.css` vs `src/styles/tokens.css` vs per-surface `tokens.css`. Recommended: **`src/shared/tokens.css`** to match the existing `src/shared/` convention (logger, types, binary). - `default_locale` value: `"en"` recommended (Chrome Web Store convention; manifest is globally visible; Russian audience accesses the actual UI which is RU-primary). - Font tooling specifics within the chosen WOFF2 self-host approach. ### Deferred Ideas (OUT OF SCOPE) - Dark theme variant of mark SVG (`.dark` class already in `tokens.css` — but D-06 keeps mark neutral; badge does state work). - Welcome page hero (Plan 01-10). - Per-state icon variants (D-08 = neutral mark + badge; A-06 PNG set deferred indefinitely). - Hi-DPI `icon192.png` (Brief P1, optional; defer unless Plan 01-12 budget allows). ## Phase Requirements | ID | Description | Research Support | |----|-------------|------------------| | REQ-video-ring-buffer | Phase 1 anchor requirement; satisfied by Plans 01-01..01-07 | Not directly addressed by Plan 01-12 — but mentioned because Plan 01-12 must not regress any existing video-pipeline test. The 79 tests landed in 01-10 stay GREEN. | | (implicit D-04..D-09) | Design integration requirements derived from `brand-decisions-v1.md` | This RESEARCH.md is the source-of-truth synthesis for each. | --- ## 1. WOFF2 self-hosting in MV3 (D-05) — **BLOCKER on Newsreader Cyrillic** ### BLOCKER finding The Decision Brief's `@font-face` declarations (extracted from `.planning/intel/design-incoming/mokosh/dist/Decision Brief (standalone).html`, inside the bundler template after JSON-decoding) reveal the actual Google Fonts subset structure: | Family | Weights/styles shipped | Subsets shipped | Cyrillic? | |---|---|---|---| | Newsreader | normal 400/500/600, italic 400 | `latin`, `latin-ext`, `vietnamese` | **NO** | | IBM Plex Sans | normal 400/500/600/700 | `latin`, `latin-ext`, `cyrillic`, `cyrillic-ext`, `greek`, `vietnamese` | YES | | IBM Plex Mono | normal 400/500 | `latin`, `latin-ext`, `cyrillic`, `cyrillic-ext`, `vietnamese` | YES | This is consistent with the Newsreader GitHub README (`productiontype/Newsreader`) which states Newsreader covers "Google Fonts Latin Plus glyph set, ... English, Western European languages as well as Vietnamese". Production Type's own page makes no Cyrillic claim. Per the upstream charset files, Newsreader does not include Cyrillic glyphs in any release shipped via Google Fonts. **Impact on D-05:** Newsreader display text in Russian (e.g. `«Тридцать секунд назад, всегда под рукой.»` if it lands on welcome hero as `.mks-display-1`) will fall back to the next family in the stack: `Newsreader, "Iowan Old Style", "Times New Roman", serif`. On Linux/Chrome OS this means Times New Roman or DejaVu Serif. On macOS, Iowan Old Style. On Windows, Times New Roman. Each render is a different visual personality and none is the loom-warm character that D-05 picks. **Remediation options for the planner:** - **R1 (recommended): document the limitation + add Cyrillic-capable serif fallback.** Use Newsreader for Latin text only (welcome EN parallel-text subhead per D-08 — `"Thirty seconds ago, always at hand."`). For Russian display text, the existing fallback chain `Newsreader, "Iowan Old Style", "Times New Roman", serif` already does the work — just verify the Russian welcome hero renders in a visually-acceptable serif on the operator's actual Chrome. Engineering can prepend a Cyrillic-capable OFL serif (e.g. **PT Serif** by ParaType, **EB Garamond**, or **Lora** — all SIL OFL, all have Cyrillic) into the `--mks-font-display` stack so the Russian hero falls onto a curated face instead of a system default. Lowest engineering cost. - **R2: switch display family.** Substitute a Cyrillic-bearing OFL serif (PT Serif, EB Garamond, Lora, Source Serif Pro) for Newsreader entirely. Loses the designer's specific aesthetic call. Requires re-coordinating with design team. - **R3: extend Newsreader with a custom Cyrillic.** Out of scope; would require a font designer and license review. **Recommended action for planner:** Surface this to user as a one-paragraph decision question. Default to R1 (smallest scope, preserves designer's call for Latin, adds a curated Cyrillic-capable serif as the next fallback). If user accepts R1, no font for Russian Newsreader needs to be subsetted — only Plex Sans + Plex Mono carry Cyrillic. ### Bundle plan (assumes R1; revise if R2) For each subsetted WOFF2 file: ```bash # Install: already installed on dev machine — pyftsubset (fontTools 4.63.0) + woff2_compress (Google's woff2) # Source: download .ttf releases from https://github.com/IBM/plex (Plex Sans v1.1.0, Plex Mono v1.1.0) # and https://github.com/productiontype/Newsreader (variable font) # Plex Sans 400 Cyrillic-only (smallest possible; covers Russian operator surfaces) pyftsubset IBMPlexSans-Regular.ttf \ --unicodes='U+0020-007E,U+00A0-00FF,U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116' \ --flavor=woff2 \ --output-file=fonts/IBMPlexSans-Regular-latin-cyrillic.woff2 \ --layout-features='*' --no-hinting --desubroutinize # Repeat for weights 500, 600, 700. # Plex Mono 400 + 500 — same subset pyftsubset IBMPlexMono-Regular.ttf \ --unicodes='U+0020-007E,U+00A0-00FF,U+0400-045F,U+2116' \ --flavor=woff2 --output-file=fonts/IBMPlexMono-Regular.woff2 ... # Newsreader (variable, Latin only per the Brief subset) pyftsubset Newsreader[opsz,wght].ttf \ --unicodes='U+0020-007E,U+00A0-00FF,U+0152-0153,U+2000-206F,U+20AC,U+2122' \ --flavor=woff2 --output-file=fonts/Newsreader-variable.woff2 ... ``` **Estimated bundle size (verified against Google Fonts subset sizes from public CDN):** | File | Estimated size | |---|---| | Plex Sans 400 latin+cyrillic | ~25 KB | | Plex Sans 500 latin+cyrillic | ~25 KB | | Plex Sans 600 latin+cyrillic | ~25 KB | | Plex Sans 700 latin+cyrillic | ~25 KB | | Plex Mono 400 latin+cyrillic | ~28 KB | | Plex Mono 500 latin+cyrillic | ~28 KB | | Newsreader variable, Latin-only | ~60 KB (variable axes inflate) | | **Total** | **~216 KB** | Comfortably under the brief's implicit "≤300 KB" target. **License files (D-05 OFL):** - `fonts/LICENSE-IBM-Plex.txt` — copy from https://github.com/IBM/plex/blob/master/LICENSE.txt (SIL OFL 1.1 + IBM copyright lines) - `fonts/LICENSE-Newsreader.txt` — copy from https://github.com/productiontype/Newsreader/blob/master/OFL.txt - Both: OFL allows embedding without a separate NOTICE file (per OFL FAQ); a `fonts/README.md` linking to each upstream + listing copyright lines is sufficient and is best practice. ### Sources - [Newsreader on GitHub (productiontype/Newsreader)](https://github.com/productiontype/Newsreader) — "Google Fonts Latin Plus glyph set" claim, no Cyrillic mentioned - [IBM Plex on GitHub](https://github.com/IBM/plex) — Plex Sans supports "extended Latin, Arabic, Chinese (Traditional), Cyrillic, Devanagari, Greek, Hebrew, Japanese, Korean, and Thai" - [SIL Open Font License 1.1](https://openfontlicense.org/) — embedded fonts have lighter attribution requirements - [OFL-FAQ](https://openfontlicense.org/ofl-faq/) — bundled fonts do not require separate distribution copies - [Decision Brief embedded `@font-face` declarations](file://.planning/intel/design-incoming/mokosh/dist/Decision%20Brief%20(standalone).html) — primary source for confirmed subset structure - [fontTools pyftsubset docs](https://fonttools.readthedocs.io/en/stable/subset/) — `--unicodes` + `--flavor=woff2` - [Markos Konstantopoulos on font subsetting](https://markoskon.com/creating-font-subsets/) — practical pyftsubset examples - [Walter Ebert on subsetting web fonts](https://walterebert.com/blog/subsetting-web-fonts/) — IBM Plex bandwidth comparisons --- ## 2. Vite @crxjs font asset pipeline ### Findings - `@crxjs/vite-plugin` (project on `2.0.0-beta.25`; latest stable is `2.4.0`) automatically generates `web_accessible_resources` entries for resources referenced from extension pages (popup, welcome, offscreen, etc.) per the official README: *"Automatic generation of `web_accessible_resources` manifest entries"*. - Vite's general asset handling: CSS `url()` references inside imported `.css` files are auto-rebased and emitted with content-hashed names to `dist/assets/`. This works for `.woff2` files placed in `src/shared/fonts/` or similar. - The known failure mode is when `chrome.runtime.getURL()` is called from a service worker (per [crxjs issue #891](https://github.com/crxjs/chrome-extension-tools/issues/891)) — but this is NOT the popup/welcome font-loading flow. Fonts referenced via `@font-face src: url('../shared/fonts/x.woff2')` inside an extension-page CSS work through Vite's normal rebase pipeline. ### Recommended approach **Pattern A (recommended): regular `src/` assets.** Place WOFF2 files at `src/shared/fonts/.woff2`. The canonical `src/shared/tokens.css` references them via relative paths: ```css @font-face { font-family: "IBM Plex Sans"; src: url("./fonts/IBMPlexSans-Regular-latin-cyrillic.woff2") format("woff2"); font-weight: 400; font-style: normal; font-display: swap; } ``` When `src/popup/index.html` and `src/welcome/welcome.html` `@import` (or ``) the tokens.css, Vite rebases the URLs, emits the WOFF2 files to `dist/assets/.woff2`, and rewrites the CSS to point at the hashed names. CRXJS then auto-adds them to `web_accessible_resources` because they're transitively reachable from extension HTML pages. **Pattern B (fallback if Pattern A misfires): `/public/fonts/`.** Files placed under `public/` are copied verbatim to `dist/` without hashing. Reference as absolute paths `url("/fonts/x.woff2")`. The trade-off: no asset hashing (cache busting requires version bumps), but explicit and tool-independent. Manual `web_accessible_resources` entry needed in `manifest.json`: ```json "web_accessible_resources": [ { "resources": ["fonts/*.woff2"], "matches": [""] } ] ``` **Validation step (planner: include in execute-plan):** After first build with WOFF2 files, run `grep -r "woff2" dist/manifest.json` and `ls dist/assets/*.woff2` — if WOFF2 files show up in `dist/assets/` AND `web_accessible_resources` mentions them, Pattern A is working. If not, fall back to Pattern B. ### Sources - [@crxjs/vite-plugin README on GitHub](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/README.md) — "automatic generation of web_accessible_resources" - [Vite Static Asset Handling](https://vite.dev/guide/assets) — CSS `url()` rebasing - [crxjs issue #891 (SW asset loading failure)](https://github.com/crxjs/chrome-extension-tools/issues/891) — known pitfall, NOT applicable to popup/welcome font loading - Codebase verification: current `vite.config.ts` line 10 sets `injectCss: false` for content scripts only (does not affect popup/welcome CSS); current `manifest.json` has no `web_accessible_resources` (Plan 01-10 adds one for welcome.html) --- ## 3. SVG → PNG rasterization toolchain (D-01) ### Verified locally Tooling installed on dev machine: - `rsvg-convert` 2.60.0 (libRSVG via librsvg) — installed at `/usr/bin/rsvg-convert` - ImageMagick `convert` (deprecated in v7, use `magick`) — also available - `inkscape` 1.4.4 — available Empirically verified by rasterizing `mokosh-mark.svg`: ```bash rsvg-convert -w 16 -h 16 mokosh-mark.svg -o icon16.png # 406 bytes ✓ (>200) rsvg-convert -w 48 -h 48 mokosh-mark.svg -o icon48.png # 784 bytes ✓ (>500) rsvg-convert -w 128 -h 128 mokosh-mark.svg -o icon128.png # 1952 bytes ✓ (>1024) ``` All three clear the Chrome `imageUtil` floors documented in `assets-spec.md` and the design handoff. PNG output is `8-bit/color RGBA, non-interlaced` per `file(1)`. ### Recommended approach **One-off rasterization, commit the PNGs.** No build-time automation needed because the mark is stable (designer-handed-off; D-01 locked). A `scripts/rasterize-icons.sh` recipe is documented but only re-runs when the SVG changes: ```bash #!/usr/bin/env bash set -euo pipefail SVG="src/shared/brand/mokosh-mark.svg" # or wherever the planner lands the SVG for size in 16 48 128; do rsvg-convert -w "$size" -h "$size" "$SVG" -o "icons/icon${size}.png" echo "✓ icons/icon${size}.png ($(stat -c%s "icons/icon${size}.png") bytes)" done ``` Run once during execute-plan, commit the resulting `icons/icon{16,48,128}.png` files. ### Anti-pattern Do NOT add this to `prebuild` — Chrome's `imageUtil` floors are bytes-of-PNG, not bytes-of-source-SVG. Re-rasterizing on every `npm run build` adds 30–80 ms with no value (the inputs are stable per phase). Plan 01-12 should commit static PNGs as deliverables, not generate them in CI. ### Sources - `rsvg-convert(1)` man page (locally available) - [Inkscape command-line export](https://wiki.inkscape.org/wiki/Using_the_Command_Line) — alternative tool - Verified locally against `.planning/intel/design-incoming/system/bundle/mokosh-handoff/assets/mokosh-mark.svg` --- ## 4. Mark legibility at 16 px (D-01 risk) — **VERIFIED OK** ### Findings Empirically rasterized via rsvg-convert at 16/48/128 px. The 2×2 weave mark holds: - **16 px** (`/tmp/mokosh-rsearch/mark-16.png`, 406 B): Reads as a 4×4-grid silhouette. The two central weave gaps (the "hole" at x=12–14 and y=12.5–13.5) collapse into near-pixel-level dots, but the overall grid pattern is recognizable. Stroke weight at 16 px ≈ 1.1 px effective; corners round slightly via anti-aliasing. - **48 px** (784 B): Clean weave rendering with all gaps visible. - **128 px** (1952 B): Fully resolved, every gap distinct. Visual inspection: at 16 px the mark reads as "small frame containing crosshatch" — distinct from generic placeholder shapes. The README claim that "[Newsreader concept] holds at 16 px" (designer handoff handoff.html line 458) is borne out by the rsvg-convert output. ### Recommended approach **Use the canonical `mokosh-mark.svg` at all three sizes — no 16 px variant needed.** The mark survives at 16 px. The four candidate marks the README mentions ("Surface Kit shows 4 mark candidates") are alternative concepts, not 16 px-rescue variants. With D-01 locked on `A · Loom`, the candidate set doesn't apply. **No remediation needed.** This risk was pre-mitigated by the designer's choice of a simple weave (4 vertical + 4 horizontal segments inside a rounded square — no fine detail that fails to resolve at low resolution). ### Sources - Local empirical: rsvg-convert output inspected as image at 16/48/128 px - Designer handoff `handoff.html` line 458: "[2×2 weave intersection — see assets/mokosh-mark.svg] Holds at 16 px." --- ## 5. Dark theme variant of mark SVG (D-06) ### Findings The lockup + mark SVGs hardcode `stroke="#181b2a"` (matches `--mks-ink-900`). The `tokens.css` `.dark` block at line 165 swaps `--mks-surface-inverse` etc., but does NOT touch the mark stroke. D-06 picks "neutral mark + dynamic badge" — the badge does the state work, not an icon swap. **Three architectural patterns evaluated:** 1. **`currentColor`** — change SVG to `stroke="currentColor"`. Works for *inline* SVG with ``, but NOT for `` (image elements don't inherit `currentColor`). The `default_icon` and `chrome.action.setIcon` paths use raster PNGs, not SVG, so this pattern doesn't help the toolbar at all. 2. **Two SVG variants** — `mokosh-mark-light.svg` (`#181b2a` stroke) + `mokosh-mark-dark.svg` (`#faf7f1` stroke for inverse surfaces). Swap based on `prefers-color-scheme`. Useful for *welcome page* hero rendering only. 3. **Badge-only differentiation (D-06)** — the toolbar PNG icon stays neutral (`#181b2a`) regardless of theme; the colored badge (`chrome.action.setBadgeBackgroundColor`) does the state communication. This is what D-06 picks. ### Recommended approach **For toolbar PNGs (icon16/48/128):** Single neutral `#181b2a` stroke. The current rasterized PNGs from §3 satisfy D-06 directly. Chrome's toolbar will adapt the visual surrounding (light/dark theme of the browser chrome) but the icon itself stays a fixed ink-on-transparent rendering. **For inline SVG usage on welcome page (Plan 01-10's territory, not 01-12's):** When Plan 01-10 inlines `mokosh-mark.svg` into the welcome HTML, modify the inline copy to use `stroke="currentColor"` and let CSS `color: var(--mks-fg-1)` control it. This is a Plan 01-10 concern; Plan 01-12 may pre-stage by copying the SVG to `src/shared/brand/mokosh-mark.svg` and leaving the canonical hardcoded-stroke version untouched (the rasterizer needs the stroke explicit for PNG output). **No dark-variant SVG file needed for Plan 01-12.** ### Sources - `tokens.css` lines 165–189 (`.dark` block) — confirms surface inversion but no mark color swap - `brand-decisions-v1.md` D-06 — "Neutral mark + dynamic badge" - MDN `currentColor`: only inherited by inline SVG, not `` (well-known browser behavior; not vendor-disputed) --- ## 6. Cyrillic subsetting (D-05 corollary) — **superseded by §1 BLOCKER for Newsreader** ### Findings (Plex faces only — Newsreader covered in §1) Per the canonical Google Fonts subset structure extracted from the Decision Brief: | Subset | Unicode-range (canonical) | Bytes-per-face-weight (est.) | |---|---|---| | `cyrillic` (basic) | `U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116` | ~10 KB for Plex Sans 400 | | `cyrillic-ext` | `U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F` | ~6 KB | | `latin` (basic) | `U+0000-00FF, U+0131, U+0152-0153, ... (ASCII + Latin-1 + common punct)` | ~15 KB for Plex Sans 400 | **Russian operators don't need `cyrillic-ext`** (that's Cyrillic Supplement — historical/extended scripts like Old Church Slavonic, Komi, Yakut). Mokosh's audience uses Modern Russian Cyrillic which lives entirely in `U+0400-04FF` (covered by basic `cyrillic`). ### Recommended subset per face ```bash # Russian operator: latin (ASCII for English fallback text) + cyrillic (basic, U+0400-045F) # Skip cyrillic-ext (Supplement), latin-ext (Eastern European), greek, vietnamese UNICODES='U+0020-007E,U+00A0-00FF,U+0131,U+0152-0153,U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116' ``` Per-face-weight WOFF2 should land at ~25 KB for Plex Sans, ~28 KB for Plex Mono. The size table in §1 is conservative. ### Sources - Decision Brief embedded `@font-face` declarations (verified extraction; see §1) - [`pyftsubset` docs](https://fonttools.readthedocs.io/en/stable/subset/) — `--unicodes` syntax - Standard reference: [Markos Konstantopoulos on font subsetting](https://markoskon.com/creating-font-subsets/) --- ## 7. License compliance (D-05) ### Findings - **IBM Plex Sans + IBM Plex Mono** — SIL OFL 1.1. Upstream license at https://github.com/IBM/plex/blob/master/LICENSE.txt. Copyright "© Copyright IBM Corp. with Reserved Font Names 'Plex' and 'IBM Plex'." - **Newsreader** — SIL OFL 1.1. Upstream license at https://github.com/productiontype/Newsreader/blob/master/OFL.txt. Copyright "© 2019 Production Type, Paris." - Both: embedding + bundling allowed. No royalty. Per [OFL-FAQ](https://openfontlicense.org/ofl-faq/), embedding a font in a software bundle "does not constitute distribution" for the purposes of mandatory NOTICE shipping. Best practice is still to include the OFL text + copyright lines. ### Recommended file layout ``` src/shared/fonts/ ├── IBMPlexMono-Regular.woff2 # 400 latin+cyrillic ├── IBMPlexMono-Medium.woff2 # 500 latin+cyrillic ├── IBMPlexSans-Regular.woff2 # 400 latin+cyrillic ├── IBMPlexSans-Medium.woff2 # 500 latin+cyrillic ├── IBMPlexSans-SemiBold.woff2 # 600 latin+cyrillic ├── IBMPlexSans-Bold.woff2 # 700 latin+cyrillic ├── Newsreader-variable.woff2 # variable axes [opsz, wght], latin-only per §1 R1 ├── LICENSE-IBM-Plex.txt # copy of upstream LICENSE.txt ├── LICENSE-Newsreader.txt # copy of upstream OFL.txt └── README.md # one paragraph: what's bundled, with upstream URLs ``` `src/shared/fonts/README.md` content (one-pager): ```markdown # Bundled fonts Self-hosted per MV3 CSP (forbids remote @font-face at runtime). All faces are SIL OFL 1.1 licensed (see LICENSE-* files in this directory). ## Sources - IBM Plex Sans + Plex Mono: https://github.com/IBM/plex (Plex Sans v1.1.0, Plex Mono v1.1.0, 2024-11-13) - Newsreader: https://github.com/productiontype/Newsreader (variable, 2020+, Production Type for Google Fonts) ## Subsetting Subsetted via fontTools.pyftsubset to Latin (ASCII + Latin-1) + Cyrillic basic (U+0400-045F) for Plex; Latin-only for Newsreader (no Cyrillic glyphs in upstream). Russian operator surfaces use Plex Sans/Mono for text rendering; Newsreader is display-only for Latin script (welcome EN parallel-text subhead). ## Regenerate See scripts/subset-fonts.sh. ``` ### Sources - [SIL OFL 1.1 text](https://openfontlicense.org/) - [OFL-FAQ](https://openfontlicense.org/ofl-faq/) — embedded fonts attribution - [IBM Plex repo + LICENSE](https://github.com/IBM/plex) - [Newsreader repo + OFL.txt](https://github.com/productiontype/Newsreader) - [SPDX OFL-1.1](https://spdx.org/licenses/OFL-1.1.html) --- ## 8. `.mks-word` CSS class (lockup SVG dependency) ### Findings The lockup SVG (`assets/mokosh-lockup.svg`) line 21: ```html Mokosh ``` The `.mks-word` class is referenced but NOT defined in delivered `tokens.css`. Without it, the wordmark "Mokosh" inside the lockup SVG renders in the browser default serif at default size — completely off-brand. ### Recommended approach **Add a one-line definition to `src/shared/tokens.css` (engineering-authored, designer-overridable).** The definition matches the visual specs implied by the lockup SVG geometry (text starts at x=48, baseline at y=40 inside a 240×56 viewBox; visual inspection shows the text occupies about 32 px cap-height): ```css /* ── Lockup wordmark ────────────────────────────────────────────── References from mokosh-lockup.svg. Engineering's working definition; designer can override by sending a tokens.css patch. */ .mks-word { font-family: var(--mks-font-display); font-size: 32px; font-weight: var(--mks-weight-regular); letter-spacing: var(--mks-tracking-display); fill: var(--mks-ink-900); } ``` **Mitigations to surface to designer in commit message:** ``` docs(01-12): add .mks-word lockup wordmark CSS (engineering working defn) The lockup SVG `assets/mokosh-lockup.svg` references `.mks-word` which was not in delivered tokens.css. This commit adds a working definition (32 px Newsreader at --mks-ink-900) so the lockup renders. Designer may override by sending a tokens.css patch with the canonical sizing. ``` **If the lockup is used inline in welcome page:** `fill` works via CSS for `` inside inline SVG. If used as ``, the CSS doesn't reach inside — the `.mks-word` would need to be applied via a `