diff --git a/.planning/phases/01-stabilize-video-pipeline/01-12-RESEARCH.md b/.planning/phases/01-stabilize-video-pipeline/01-12-RESEARCH.md new file mode 100644 index 0000000..eaef5da --- /dev/null +++ b/.planning/phases/01-stabilize-video-pipeline/01-12-RESEARCH.md @@ -0,0 +1,1309 @@ +# 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 `