Files
Mark 3df2750c64 docs(01-12): research Plan 01-12 (Design Integration) — 13 areas + BLOCKER
13 research surfaces investigated per Plan 01-12 prompt. Key findings:

- BLOCKER §1: Newsreader has no Cyrillic glyphs (verified via Decision
  Brief's own embedded @font-face subsets + Production Type README).
  Russian display text in Newsreader will silently fall through to
  serif fallback. Recommend §1 R1: add Cyrillic-capable OFL serif (PT
  Serif / EB Garamond / Lora) to --mks-font-display stack.

- §3 + §4: Mark legibility at 16 px is VERIFIED OK. rsvg-convert
  produces 406 / 784 / 1952 B PNGs at 16/48/128 — all clear Chrome
  imageUtil floors. The 2x2 weave holds at 16 px.

- §10: 8 i18n strings extracted verbatim from Brief §02 (decoded via
  Python JSON-decode of the bundler-template script). Recommended
  __MSG_*__ key names + source-file mapping table included.

- §12: D-09 (smoke gating) is NEAR-NO-OP. smoke.sh is a standalone
  bash script; nothing in src/ references it; dist/ carries no smoke
  artifacts. Recommend documenting current state + adding optional
  no-smoke-in-dist regression test.

- §13: tokens.css line 12 Google Fonts @import migration → 8 local
  @font-face rules (Plex variable consolidation collapses the
  estimated 12-18 down to 8).

All tooling verified installed on dev machine: rsvg-convert 2.60.0,
pyftsubset 4.63.0, woff2_compress, inkscape 1.4.4. No npm install
needed for execute-plan; only one-off font source TTF downloads.

7 assumptions logged for planner to surface (A1 default_locale, A2
popup DOM rework, A4 Cyrillic serif fallback are highest-impact).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:23:27 +02:00

83 KiB
Raw Permalink Blame History

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:

# 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):

Sources


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(<importedAsset>) is called from a service worker (per crxjs issue #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.

Pattern A (recommended): regular src/ assets. Place WOFF2 files at src/shared/fonts/<face>.woff2. The canonical src/shared/tokens.css references them via relative paths:

@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 <link>) the tokens.css, Vite rebases the URLs, emits the WOFF2 files to dist/assets/<hash>.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:

"web_accessible_resources": [
  { "resources": ["fonts/*.woff2"], "matches": ["<all_urls>"] }
]

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


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:

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).

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:

#!/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 3080 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 — 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=1214 and y=12.513.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.

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 <svg style="color: var(--mks-fg-1)">, but NOT for <img src="mokosh-mark.svg"> (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 variantsmokosh-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.

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 165189 (.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 <img src=svg> (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).

# 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


7. License compliance (D-05)

Findings

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):

# 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


8. .mks-word CSS class (lockup SVG dependency)

Findings

The lockup SVG (assets/mokosh-lockup.svg) line 21:

<text x="48" y="40" class="mks-word">Mokosh</text>

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.

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):

/* ── 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 <text> inside inline SVG. If used as <img src="lockup.svg">, the CSS doesn't reach inside — the .mks-word would need to be applied via a <style> element inside the SVG itself (per SVG2 CSS rules). For Plan 01-12, the simplest path: add the CSS to tokens.css, and any consumer of the lockup either (a) inlines the SVG into HTML (CSS reaches), or (b) inlines the same class definition into a <style> block within the SVG file (small duplication, acceptable).

Sources

  • assets/mokosh-lockup.svg line 21 (read verbatim)
  • tokens.css lines 14280 (verified .mks-word is absent)
  • SVG2 § 6: Styling with CSS — CSS-from-outside-SVG rules

9. tokens.css integration into existing src/ (D-04)

Findings

Current src/ layout:

src/
├── background/index.ts        # SW
├── content/index.ts           # content script
├── offscreen/{index.html, recorder.ts}
├── popup/{index.html, index.ts, style.css}   # current popup w/ placeholder palette
└── shared/{binary.ts, logger.ts, types.ts}   # shared module convention exists

Current popup style (src/popup/style.css) uses hardcoded hex (#2563eb blue button, #f8f9fa body bg, #dc2626 error) — entirely engineering placeholder colors per the design handoff's diagnostic. Plan 01-12 rewires these to tokens.

Future welcome page (Plan 01-10's territory) at src/welcome/welcome.css uses hardcoded placeholders too (#00C853 green button, #1f1f1f text) — per current Plan 01-10 contents. Plan 01-12 SHOULD NOT modify Plan 01-10's CSS unless the planner explicitly decides to rewire it as a hand-off to Plan 01-10 execution (which is a coordination question).

src/shared/tokens.css       # canonical tokens (this is the single source of truth)
src/shared/fonts/           # WOFF2 files + LICENSE-* (per §7)

Import strategy: Each top-level CSS file (src/popup/style.css, src/welcome/welcome.css, smoke data: URL) prepends a relative @import:

/* src/popup/style.css */
@import "../shared/tokens.css";

body {
  background: var(--mks-surface);
  color: var(--mks-fg-1);
  font-family: var(--mks-font-ui);
  ...
}

Vite resolves @import paths at build time and emits a single CSS chunk per HTML entry, deduplicating the token block. No PostCSS pipeline needed — CSS custom properties (--mks-*) are runtime browser features, resolved by the browser at paint time. Vite ships them as literal CSS without transformation.

Anti-pattern

Do NOT use Vite's ?inline CSS imports for tokens — that's for JS-side string imports of CSS source. Tokens go through normal @import to participate in the deduplication.

Do NOT split tokens into per-surface files. The handoff is one tokens.css; engineering's job is to keep that one file canonical so designer overrides are single-file patches per handoff.html line 27 ("the tokens are wired so a re-skin is a single-file edit").

Smoke.sh consumption

smoke.sh builds a data:text/html URL containing the entire smoke page as a base64-encoded blob (verified at smoke.sh:132181). The smoke HTML has inline <style> blocks. For Plan 01-12, D-04 wants the smoke page styled per tokens. Three options:

  • A: Inline the relevant --mks-* definitions into the smoke <style> block (the smoke page only uses a handful of tokens — #000/#0f0 mono timer overlay etc.). Replace hardcoded colors with the --mks-* values literally (because data: URLs can't @import from extension-relative paths). Verbosity cost: ~30 lines added to smoke.sh's SMOKE_HTML heredoc.
  • B: Convert smoke from data: URL to a Chrome-extension-relative URL (chrome-extension://<id>/smoke.html). Adds bundled smoke page artifacts to dist/ — directly conflicts with D-09 ("dev-only behind VITE_DEV"). Increases bundle size for production.
  • C: Leave smoke unstyled per tokens. The smoke page is dev-only; designer's restyling intent is implicit; engineering accepts the imperfection.

Recommended: A — copy a minimal subset of --mks-* literally into the smoke <style> block. Small verbosity cost, no architectural change, satisfies D-04's "rewire smoke.sh to use these tokens" directive in spirit.

Sources

  • Codebase verification: src/popup/style.css (read verbatim, lines 184)
  • Codebase verification: vite.config.ts (no custom PostCSS plugin configured)
  • Vite CSS handling@import deduplication
  • MDN CSS custom properties — runtime resolution

10. The 8 i18n copy strings (Brief §02)

Verbatim extraction from Decision Brief §02

Extracted from .planning/intel/design-incoming/mokosh/dist/Decision Brief (standalone).html (decoded bundler-template script, §02 "Восемь операторских строк"). All strings inherit D-03 Sober register.

# Surface RU (sober — recommended) EN (welcome hero only) Recommended __MSG_*__ key Source src/* file
1 Toolbar tooltip · OFF (action.default_title) Mokosh — щёлкните, чтобы начать запись tooltipOff src/background/index.ts (chrome.action.setTitle for OFF state)
2 Toolbar tooltip · REC Mokosh — идёт запись (00:42) (with timer interpolation; the (00:42) is a runtime substitution) tooltipRec (with $TIME$ placeholder) src/background/index.ts (chrome.action.setTitle for REC state, requires a timer-update interval)
3 Toolbar tooltip · ERR Mokosh — ошибка записи, щёлкните для восстановления tooltipErr src/background/index.ts (chrome.action.setTitle for ERR state, currently set in setErrorMode)
4 Popup idle body / CTA Сохранить отчёт об ошибке? / Сохранить отчёт (the "?" is the body label; "Сохранить отчёт" is the button) popupSaveCta, popupSavePrompt (two strings: prompt + button) src/popup/index.html (button text "Сохранить отчёт об ошибке"), src/popup/index.ts line 38 (idle case)
5 Popup done Архив сохранён в Загрузки. popupSaveDone src/popup/index.ts (replaces current line 91 'Архив успешно сохранён!')
6 Notification startup Запись запущена. Я слежу за последними 30 секундами. notifStartup src/background/index.ts (chrome.notifications.create startup message in setRecordingMode flow)
7 Notification recovery Запись возобновлена. Буфер снова заполняется. notifRecovery src/background/index.ts lines 766768 (recoveryId notification body)
8 Welcome hero · RU (+ EN parallel) Тридцать секунд назад, всегда под рукой. Thirty seconds ago, always within reach. welcomeHeroRu, welcomeHeroEn (two keys: RU primary, EN parallel) src/welcome/welcome.html (Plan 01-10's territory; Plan 01-12 produces the messages.json entries even if Plan 01-10 hasn't wired them yet)

Note on string #4 vs the current popup (src/popup/index.html line 12 + src/popup/index.ts line 38): existing copy is Сохранить отчёт об ошибке (no "?", used as button text). The new sober register splits this into a label-with-question + a button — Plan 01-12 will need a small DOM rework in src/popup/index.html to render both. Alternatively, keep the existing single-button form and use string #4 ("Сохранить отчёт об ошибке?") as the combined tooltip + button label. Recommend: ask user during execute-plan whether to rework popup DOM. Simplest path: keep current single-button form, use Сохранить отчёт (the shorter button form) and put the Сохранить отчёт об ошибке? label above as <p class="popup-prompt"> per the existing <p class="info-text"> pattern.

Note on string #2 (REC tooltip with (00:42)): Chrome's chrome.action.setTitle accepts a string, no placeholder syntax. The $TIME$ interpolation must happen in TS code before the API call. Suggested: define __MSG_tooltipRecPrefix__ = "Mokosh — идёт запись" and concat ' (' + formatElapsed(seconds) + ')' in JS. This keeps messages.json clean and lets the SW format the timestamp.

_locales/
├── en/messages.json    # default_locale fallback; non-Russian system locales see English
└── ru/messages.json    # primary operator locale

Example _locales/ru/messages.json:

{
  "extName":         { "message": "Mokosh — Запись сессии",
                       "description": "manifest.json:name; surfaces in Chrome's extensions list, Web Store, and right-click menus" },
  "extDesc":         { "message": "Тридцать секунд назад, всегда под рукой.",
                       "description": "manifest.json:description" },
  "tooltipOff":      { "message": "Mokosh — щёлкните, чтобы начать запись" },
  "tooltipRecPrefix":{ "message": "Mokosh — идёт запись",
                       "description": "Suffix '(MM:SS)' appended at runtime by the SW" },
  "tooltipErr":      { "message": "Mokosh — ошибка записи, щёлкните для восстановления" },
  "popupSavePrompt": { "message": "Сохранить отчёт об ошибке?" },
  "popupSaveCta":    { "message": "Сохранить отчёт" },
  "popupSaveDone":   { "message": "Архив сохранён в Загрузки." },
  "notifStartup":    { "message": "Запись запущена. Я слежу за последними 30 секундами." },
  "notifRecovery":   { "message": "Запись возобновлена. Буфер снова заполняется." },
  "welcomeHeroRu":   { "message": "Тридцать секунд назад, всегда под рукой." },
  "welcomeHeroEn":   { "message": "Thirty seconds ago, always within reach.",
                       "description": "Parallel English subhead under the RU hero on the welcome page" }
}

And _locales/en/messages.json (fallback for non-RU Chrome locales) — same keys, English values, with extDesc = "Thirty seconds ago, always within reach." and extName = "Mokosh — Session Capture" (per D-07 user override).

Sources

  • Decision Brief §02 verbatim (extracted via Python JSON-decode of bundler template; see tool-results/bdp4vyvas.txt)
  • src/popup/index.html + index.ts codebase grep
  • src/background/index.ts:54, 766768, 868870 for notification call sites
  • Chrome i18n message formatmessages.json schema

11. manifest.json i18n via __MSG_*__ (D-07 + D-08)

Findings

Per Chrome i18n docs and the default_locale manifest reference:

  • manifest.json:name and manifest.json:description accept __MSG_<key>__ references — both fields support i18n.
  • default_locale field is REQUIRED if _locales/ directory exists. Without it, _locales/ is ignored.
  • Lookup order: user's preferred locale (e.g. ru_RU) → locale without region (ru) → default_locale → first-found in directory.
  • "As long as the default locale's messages.json file has a value for every string, your extension will run no matter how sparse a translation is" (per Chrome docs).
  • Chrome supports 54 store-listing locales; Russian (ru) is among them.
{
  "manifest_version": 3,
  "name": "__MSG_extName__",
  "version": "1.0.0",
  "description": "__MSG_extDesc__",
  "default_locale": "en",
  "permissions": [...],
  ...
  "action": {
    "default_popup": "src/popup/index.html",
    "default_title": "__MSG_tooltipOff__",   // NEW per #1 above
    "default_icon": {...}
  },
  ...
}

Why default_locale: "en":

  • The Chrome Web Store fallback metadata (when displayed in a non-RU browser) uses default_locale. Setting it to "en" means the store listing for non-Russian users sees the English name + description, while RU-locale users (the operator audience) see the Russian.
  • If default_locale: "ru", then non-RU locales would see Russian (likely rendered correctly in the store but jarring for a Polish/German viewer).
  • The Russian operator audience accesses the actual UI which is RU-localized regardless of default_localedefault_locale only controls fallback.

If the user prefers default_locale: "ru" (operator-only deployment, no Chrome Web Store distribution): set it to "ru". The mechanism is the same; only the fallback differs.

Anti-pattern

Do NOT use __MSG_@@extension_id__ in manifest.json — the docs explicitly note "The special message @@extension_id ... doesn't work in manifest files."

Do NOT skip default_locale if _locales/ exists — manifest validation fails at install time.

Sources


12. VITE_DEV smoke-page gating (D-09) — NEAR-NO-OP

Findings

Verified by grepping the entire src/ tree:

$ grep -rn "smoke\|SMOKE\|data:text/html" src/
# (empty)
$ find dist -name "smoke*" -o -name "*SMOKE*"
# (empty)

smoke.sh is a standalone bash script. The smoke page HTML is embedded as a heredoc inside smoke.sh:132181 and assembled into a data:text/html,... URL at line 181. The data: URL is passed to Chrome as a command-line argument:

SMOKE_DATA_URL="data:text/html,$(printf '%s' "${SMOKE_HTML}" | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read(), safe=""))')"

SMOKE_HTML exists only in shell process memory. Nothing in src/ references it. Vite's build pipeline has zero awareness of smoke.sh. dist/ carries no smoke artifacts.

Why D-09 ("dev-only behind VITE_DEV") is a near-no-op

The decision was framed as if smoke might leak into the production bundle. It can't — smoke lives outside Vite's input set entirely. The production dist/ does not ship smoke regardless of any flag.

Document the existing state — minimal code change. Plan 01-12 adds two artifacts to satisfy D-09's spirit:

  1. scripts/README.md (new file, 1 paragraph): "Diagnostic / smoke scripts live in smoke.sh and scripts/*. These are dev-only tools and are not bundled by npm run build. The production dist/ produced by npm run build does NOT contain smoke artifacts (verified by grep of dist/ after every build)."
  2. vite.config.ts add a define block (defensive, for any future inline smoke flag):
    define: {
      // D-09: any future inline-smoke-feature check should use this flag.
      // Currently smoke lives entirely in smoke.sh; this flag is reserved
      // for future code that wants conditional dev-only behavior.
      __VITE_DEV__: JSON.stringify(process.env.VITE_DEV === '1' || !!process.env.npm_lifecycle_event?.startsWith('dev')),
    },
    

Optional (recommended but not required): Add a vitest test that asserts dist/ does NOT contain smoke strings after build:

// tests/build/no-smoke-in-dist.test.ts
it('dist/ contains no smoke artifacts', () => {
  // ...recursively read dist/, assert no file path or content contains 'smoke'
});

If the user wants stronger guarantees (D-09 audit-ready), this test is the right shape.

Anti-pattern

Do NOT add if (import.meta.env.VITE_DEV) { ... smoke code ... } blocks to src/ files — there are no such blocks today, and creating them would introduce the very leak D-09 is asking to prevent. The current architecture (smoke isolated to a shell script) is already the safest.

Sources

  • Codebase verification (grep results above)
  • smoke.sh line 132181 (read verbatim)
  • Vite env vars + modesimport.meta.env.VITE_* pattern
  • Vite define option — for __VITE_DEV__ constant injection

13. tokens.css line 12 Google Fonts @import (CSP migration)

Findings

Verified — tokens.css line 12 is:

@import url('https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap');

This violates MV3 CSP. Per Chrome MV3 CSP docs, the default extension-pages CSP is script-src 'self'; object-src 'self'; plus style-src 'self' 'unsafe-inline'; for stylesheets. The style-src 'self' directive prevents the Google Fonts stylesheet from loading at runtime. (The font-src directive defaults to 'self' as well, so even if the CSS loaded, the @font-face src: url(https://fonts.gstatic.com/...) inside would also fail.)

In practice, this means popups built today using the unmodified tokens.css would silently fall back to system serif/sans because the @font-face declarations never get loaded.

Replace line 12 with local @font-face rules pointing at self-hosted WOFF2 files in src/shared/fonts/. Per the chosen weight matrix (D-05; subject to §1 BLOCKER R1 — Newsreader Latin-only):

/* ── Self-hosted fonts (MV3 CSP) ──
   Faces and weights per D-05. Subsetting: Plex Sans + Plex Mono are
   Latin + Cyrillic (U+0400-045F); Newsreader is Latin-only (no
   Cyrillic glyphs in upstream — see RESEARCH.md §1 BLOCKER R1). */

/* IBM Plex Sans 400 / 500 / 600 / 700, normal */
@font-face {
  font-family: "IBM Plex Sans";
  src: url("./fonts/IBMPlexSans-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Sans";
  src: url("./fonts/IBMPlexSans-Medium.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Sans";
  src: url("./fonts/IBMPlexSans-SemiBold.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Sans";
  src: url("./fonts/IBMPlexSans-Bold.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}

/* IBM Plex Mono 400 / 500, normal */
@font-face {
  font-family: "IBM Plex Mono";
  src: url("./fonts/IBMPlexMono-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("./fonts/IBMPlexMono-Medium.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}

/* Newsreader variable — Latin only (no Cyrillic in upstream) */
@font-face {
  font-family: "Newsreader";
  src: url("./fonts/Newsreader-variable.woff2") format("woff2-variations"),
       url("./fonts/Newsreader-variable.woff2") format("woff2");
  font-weight: 400 700;     /* variable axis range */
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Newsreader";
  src: url("./fonts/Newsreader-italic-variable.woff2") format("woff2-variations"),
       url("./fonts/Newsreader-italic-variable.woff2") format("woff2");
  font-weight: 400 700;
  font-style: italic;
  font-display: swap;
}

Rule count: 8 (4 Plex Sans + 2 Plex Mono + 2 Newsreader variable). Less than the 1218 estimate in the prompt because using a variable font for Newsreader collapses 4 static-weight rules into 2 (one per style).

If §1 R1 is adopted with an additional Cyrillic-capable serif fallback (e.g. PT Serif), add 24 more @font-face rules for that face.

Anti-pattern

Do NOT use the format('woff2-variations') hint with a fixed-weight WOFF2 file — the hint is for variable fonts only. Browsers without variable-font support fall back to the second src URL (format('woff2')).

Do NOT keep the Google Fonts @import "for dev fallback" — MV3 CSP refuses it in all modes (dev + prod alike); leaving it in would generate console errors during development.

Sources


Architectural Responsibility Map

Capability Primary Tier Secondary Tier Rationale
Brand mark display (toolbar PNG) Browser (Chrome action API) chrome.action.setIcon consumes PNG; only the SW initially configures; rendering is browser-owned
Brand mark display (welcome inline SVG) Browser (DOM) Frontend Server (built HTML) Plan 01-10 inlines the SVG into welcome.html; Vite builds the HTML; browser paints
CSS tokens Browser (CSS custom properties) All --mks-* resolve at paint time in the browser; no SSR; Vite ships them as literal CSS
Font loading (WOFF2) Browser CDN/Static (Vite-emitted dist/assets/) Browser parses @font-face, fetches from extension origin; Vite asset pipeline emits + hashes
i18n string resolution (__MSG_*__) Browser (Chrome i18n runtime) CDN/Static (_locales/*/messages.json) Chrome reads _locales/ at install time, substitutes at runtime; no server involvement
Manifest:name + :description Chrome runtime (manifest parser) Chrome reads manifest, substitutes __MSG_*__, displays in Web Store / Extensions list
Popup HTML/CSS rendering Browser Frontend (built HTML) Popup is a self-contained Chrome extension HTML page; browser-only rendering
Smoke page styling Browser (data: URL DOM) data:text/html URL renders client-side; no Vite bundling involved

Standard Stack

Core

Library / tool Version (verified) Purpose Why standard
@crxjs/vite-plugin 2.0.0-beta.25 (project); 2.4.0 (latest) MV3 bundle + manifest rewriting + WAR auto-generation Project already uses it; required for MV3 ergonomics on Vite
vite 5.4.2 (project); 8.0.13 (latest) Build tool; CSS @import dedup; asset hashing Project already uses it; Vite native @import handling resolves tokens.css from anywhere in src/
fontTools (pyftsubset) 4.63.0 (verified locally) WOFF2 subsetting Industry standard; preinstalled on dev machine
woff2_compress (Google's woff2) installed via /usr/bin/woff2_compress WOFF2 encoding Native compression; what pyftsubset uses under the hood when --flavor=woff2 is specified
rsvg-convert (librsvg) 2.60.0 (verified locally) SVG → PNG rasterization Reliable; produces valid 8-bit RGBA PNGs that clear all Chrome imageUtil floors

Supporting

Library / tool Version Purpose When to use
vitest 4 (project) Unit tests Plan 01-12 adds 13 tests (manifest i18n shape, fonts present, no smoke in dist)
Newsreader (Production Type) variable, current upstream Display serif (Latin only) Welcome page Latin headlines, lockup wordmark
IBM Plex Sans v1.1.0 (2024-11-13) UI body sans, Cyrillic + Latin Popup, welcome RU body, all --mks-font-ui
IBM Plex Mono v1.1.0 (2024-11-13) Diagnostic mono, Cyrillic + Latin Timer overlay (smoke), code-like UI labels
PT Serif or EB Garamond or Lora (optional, §1 R1) OFL Cyrillic-capable serif fallback for Russian display Russian welcome hero, RU display text falling out of Newsreader stack

Alternatives considered

Instead of Could use Tradeoff
rsvg-convert inkscape --export-png Inkscape is heavier (X server may be needed for headless run); rsvg-convert is purer + smaller
pyftsubset glyphhanger (extracts unicode-range from HTML) glyphhanger is for discovering needed glyphs from real content; not needed when subset ranges are known (we have the Google Fonts canonical ranges)
Newsreader (D-05) PT Serif (Cyrillic-capable display serif) Loses designer's specific D-05 choice; gains Russian rendering correctness — see §1 BLOCKER
_locales/ for i18n Hardcoded RU strings only Locks out future EN store distribution; _locales/ is one-time setup cost; manifest:name needs it anyway for D-07 EN listing
Asset path in src/shared/fonts/ (Pattern A) /public/fonts/ (Pattern B) Pattern A: hashed names, deduplication; Pattern B: stable names, no @crxjs WAR auto-gen

Installation

# All required tools are ALREADY installed on dev machine; verified:
which pyftsubset    # /usr/bin/pyftsubset (fontTools 4.63.0)
which woff2_compress # /usr/bin/woff2_compress
which rsvg-convert  # /usr/bin/rsvg-convert (2.60.0)

# No npm installs needed for Plan 01-12 — the tooling is all native binaries.
# IF the planner wants asset-pipeline tests, no new dev-deps either; vitest is already present.

# Font source downloads (one-off, will land via curl in execute-plan):
# - https://github.com/IBM/plex/releases (Plex Sans + Plex Mono v1.1.0)
# - https://github.com/productiontype/Newsreader/releases (variable WOFF2 + TTF)

Version verification

Verified versions are current as of 2026-05-17 dev machine probe (npm view for npm packages; which/--version for system binaries). No package install needed for Plan 01-12 execution — Vite + crxjs versions stay where the project currently has them (upgrading Vite from 5 → 8 is out of scope for a brand-integration plan).


Architecture Patterns

Pattern 1: Tokens-first CSS

What: Every CSS rule consuming a color, font-family, spacing, or radius value references a --mks-* custom property defined in src/shared/tokens.css. No literal hex / px / font-family strings in src/popup/style.css, src/welcome/welcome.css, or smoke HTML.

When to use: All surfaces touched by Plan 01-12 (popup, smoke; welcome is Plan 01-10's).

Example:

/* src/popup/style.css after Plan 01-12 */
@import "../shared/tokens.css";

body {
  background: var(--mks-surface);
  color: var(--mks-fg-1);
  font-family: var(--mks-font-ui);
  padding: var(--mks-space-4);
}
.save-button {
  background: var(--mks-rec);            /* madder for the SAVE button per D-04 */
  color: var(--mks-fg-inverse);
  border: var(--mks-border-width) solid var(--mks-border-strong);
  border-radius: var(--mks-radius-md);
  box-shadow: var(--mks-shadow-1);
  font-family: var(--mks-font-ui);
  font-weight: var(--mks-weight-semibold);
}

Pattern 2: i18n __MSG_*__ at every Chrome API call site

What: Every call to chrome.action.setTitle, chrome.notifications.create, chrome.action.setBadgeText, and any DOM-rendered Russian string uses chrome.i18n.getMessage('key') instead of a literal.

When to use: Every operator-facing string. Plan 01-12 audits src/background/index.ts + src/popup/index.ts for hardcoded RU strings and migrates each.

Example:

// src/background/index.ts (was: 'Запись возобновлена')
chrome.notifications.create(recoveryId, {
  type: 'basic',
  iconUrl: chrome.runtime.getURL(NOTIFICATION_ICON_PATH),
  title: chrome.i18n.getMessage('notifRecoveryTitle'),
  message: chrome.i18n.getMessage('notifRecovery'),
});

// src/popup/index.ts (was: buttonText.textContent = 'Сохранить отчёт об ошибке')
buttonText.textContent = chrome.i18n.getMessage('popupSaveCta');

Pattern 3: Single source of truth for tokens

What: src/shared/tokens.css is canonical. The handoff tokens.css is the input; engineering's src/shared/tokens.css is its production-ready descendant (Google Fonts @import replaced, .mks-word class added). The Decision Brief's tokens.css and the handoff's preview do NOT get bundled — only the engineering canonical.

Anti-patterns to avoid

  • Per-surface mini-tokens. Do NOT create src/popup/popup-tokens.css or similar. One tokens.css.
  • Inline color literals "for clarity". Defeats the purpose. Use the token.
  • Skipping _locales/en/ because audience is RU. Manifest validation requires default_locale to have a messages.json. Plan 01-12 ships both.
  • Loading fonts with chrome.runtime.getURL from inside JS. @font-face url("./fonts/x.woff2") in CSS is the only correct path (CSP-safe, Vite-rebased).

Don't Hand-Roll

Problem Don't build Use instead Why
Font subsetting Custom Python script reading .ttf bytes pyftsubset --flavor=woff2 --unicodes=... Glyph metric preservation + variable-axis-aware + handles ligatures correctly
SVG → PNG Custom canvas rasterizer in Node rsvg-convert or Inkscape RGBA alpha, sub-pixel anti-aliasing, ImageMagick parity
i18n key resolution Custom string-template engine chrome.i18n.getMessage Native Chrome integration; manifest substitution; locale fallback chain
WOFF2 compression Custom Brotli encoder woff2_compress (Google's reference impl) The reference; what pyftsubset already calls internally
CSS custom properties → static colors PostCSS plugin to inline Use var(--mks-*) at runtime Browser-native; runtime themable via .dark class addition

Key insight: All five problems have battle-tested solutions in standard tooling. Engineering's role in Plan 01-12 is integration + wiring, not building. The single piece of "logic" Plan 01-12 writes is the __MSG_*__ migration pass over the 812 hardcoded Russian strings + the tokens.css adoption — neither is library-grade work.


Runtime State Inventory

Plan 01-12 includes brand-related runtime state changes; checking all five categories:

Category Items Found Action Required
Stored data chrome.storage.local key onboarding-completed (boolean, Plan 01-10). No PII; no brand text stored. None — brand changes don't touch storage.
Live service config chrome.action.setTitle() / setBadgeText() calls in src/background/index.ts currently pass literal Russian strings. After Plan 01-12, they pass chrome.i18n.getMessage(...). Runtime state (the actual displayed tooltip / badge text per active locale) changes on next SW startup after the extension reload. Code edit; no manual user action required.
OS-registered state manifest.json:name changes from "AI Call Recorder""Mokosh — Session Capture" (per D-07). Chrome stores the manifest name in the extensions list. On chrome://extensions reload, the displayed name will update; old name remains briefly in any cached store listing. None — Chrome handles on reload.
Secrets / env vars None — no secrets in brand integration. None.
Build artifacts dist/icons/icon{16,48,128}.png currently hold the placeholder PNGs (dark square + green dot per assets-spec.md Path A description; verified file sizes 574 B / 1153 B / 2615 B in working tree at /home/parf/projects/work/repremium/icons/). After Plan 01-12 rasterizes the new mark, these are overwritten in source icons/ and re-mirrored into dist/icons/ on next npm run build. Also: dist/manifest.json will reflect new name/description/default_locale/__MSG_*__ references; dist/_locales/ will be a new directory. npm run build after Plan 01-12 changes. Smoke test re-runs the operator UAT to confirm the new icons render correctly in toolbar + notification.

Verified by: Codebase grep + file system probe. Nothing in chrome.storage.session, no IndexedDB brand caches, no remote services configured per brand identity.


Common Pitfalls

Pitfall 1: WOFF2 silent fallback to system fonts

What goes wrong: A misnamed font-family in @font-face (e.g. font-family: "IBM Plex Sans Variable" instead of "IBM Plex Sans") doesn't error — the browser silently falls back to the next family in the stack ("Segoe UI", -apple-system, ...). Visual result: clean-looking sans-serif text, but NOT IBM Plex.

Why it happens: CSS @font-face matching is name-based; a typo creates a phantom face that no var(--mks-font-ui) reference resolves to.

How to avoid: Match the font-family name in @font-face EXACTLY to the name in var(--mks-font-ui) ("IBM Plex Sans" quoted, with the trailing space-and-Sans). The tokens.css already names them correctly; just verify each new @font-face rule matches.

Warning signs: Inspect popup with DevTools → Computed styles → font-family — should read "IBM Plex Sans" as the applied family, not "Segoe UI". If "Segoe UI" appears, the @font-face didn't load.

Pitfall 2: Chrome notification icon silently rejecting PNGs

What goes wrong: chrome.notifications.create({iconUrl}) returns no error but the notification renders with a blank gray placeholder instead of the icon.

Why it happens: Chrome's imageUtil silently rejects PNGs below ~1 KB at 128×128 — a known artifact found during Plan 01-09 debugging (per assets-spec.md line 72). The rejection has no API surface; the only signal is "no icon."

How to avoid: Ensure icons/icon128.png is ≥ 1024 bytes. The rsvg-convert output at 128×128 produces ~1952 B (verified locally) — well above the floor.

Warning signs: Run smoke.sh, click SAVE, wait for the recovery notification. If the notification has no left-icon, the PNG was rejected. Plan 01-09's empirical UAT covers this; Plan 01-12's icon-replacement should re-run that UAT.

Pitfall 3: __MSG_*__ substitution failing in HTML

What goes wrong: Putting __MSG_popupSaveCta__ literally in src/popup/index.html as text content does NOT get substituted. The string renders to the user verbatim.

Why it happens: __MSG_*__ substitution only works in (a) manifest.json fields, (b) CSS via i18n predefined messages, and (c) JavaScript-side via chrome.i18n.getMessage(). HTML text content is NOT processed.

How to avoid: In HTML, leave the static text empty (or use a sensible default) and populate it from JS using chrome.i18n.getMessage() after DOMContentLoaded:

<!-- src/popup/index.html -->
<button id="saveButton" class="save-button" disabled>
  <span class="button-text"></span>  <!-- populated by JS -->
</button>
// src/popup/index.ts in init()
const buttonText = saveButton.querySelector('.button-text') as HTMLSpanElement;
buttonText.textContent = chrome.i18n.getMessage('popupSaveCta');

Warning signs: Popup renders with __MSG_*__ strings visible — substitution wasn't applied.

Pitfall 4: Default locale missing a key

What goes wrong: A chrome.i18n.getMessage('foo') call returns an empty string when the user's locale messages.json has foo but the default_locale messages.json doesn't.

Why it happens: Chrome falls back to default_locale on any missing key in the user's locale — but if default_locale itself lacks the key, the chain ends. chrome.i18n.getMessage returns "" (no warning).

How to avoid: Maintain the rule "every key present in any _locales/*/messages.json MUST be present in _locales/<default_locale>/messages.json." Write a vitest test if defensive coverage is wanted:

// tests/i18n/locale-parity.test.ts
it('every key in ru/messages.json exists in en/messages.json', () => {
  const ru = JSON.parse(readFileSync('_locales/ru/messages.json', 'utf8'));
  const en = JSON.parse(readFileSync('_locales/en/messages.json', 'utf8'));
  for (const k of Object.keys(ru)) expect(en).toHaveProperty(k);
});

Warning signs: Empty strings in popup / notification body.

Pitfall 5: Vite hashing breaks @font-face url() in dev mode

What goes wrong: In dev mode (npm run dev), the WOFF2 file path ./fonts/IBMPlexSans-Regular.woff2 works, but on npm run build the output @font-face src becomes url("/assets/IBMPlexSans-Regular-<hash>.woff2") — same file, hashed name.

Why it happens: Vite's asset pipeline hashes assets for cache busting. The @import "../shared/tokens.css" in popup CSS gets re-emitted with hashed URLs.

How to avoid: This is correct behavior; the dev/prod discrepancy is intentional. The pitfall is only when the planner inspects dist/manifest.json for a specific filename — use a regex pattern instead. CRXJS handles web_accessible_resources registration based on the hashed names automatically.

Warning signs: Hardcoded asset paths in tests; tests fail in build mode but pass in dev mode.


Code Examples

Verified patterns from official sources + designer handoff:

Subsetting a WOFF2 with pyftsubset

# Source: fontTools.readthedocs.io/en/stable/subset/
# Verified locally: pyftsubset 4.63.0 installed at /usr/bin/pyftsubset

pyftsubset IBMPlexSans-Regular.ttf \
  --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' \
  --flavor=woff2 \
  --output-file=src/shared/fonts/IBMPlexSans-Regular.woff2 \
  --layout-features='*' \
  --no-hinting \
  --desubroutinize

Rasterizing a PNG icon at a specific size

# Source: rsvg-convert(1) man page (locally available)
# Verified locally: produces RGBA 8-bit non-interlaced PNG that clears Chrome floors

rsvg-convert -w 128 -h 128 src/shared/brand/mokosh-mark.svg -o icons/icon128.png
stat -c%s icons/icon128.png  # confirm ≥1024 bytes (verified: ~1952)

manifest.json with __MSG_*__ + default_locale

{
  "manifest_version": 3,
  "name": "__MSG_extName__",
  "version": "1.0.0",
  "description": "__MSG_extDesc__",
  "default_locale": "en",
  "action": {
    "default_popup": "src/popup/index.html",
    "default_title": "__MSG_tooltipOff__",
    "default_icon": {
      "16":  "icons/icon16.png",
      "48":  "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "icons": {
    "16":  "icons/icon16.png",
    "48":  "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

Reading an i18n string from JS

// Source: developer.chrome.com/docs/extensions/reference/api/i18n
// chrome.i18n.getMessage(messageName: string, substitutions?: string | string[]): string

const cta = chrome.i18n.getMessage('popupSaveCta');
buttonText.textContent = cta;

// With substitution placeholder ($1):
// messages.json: "tooltipRecWithTime": { "message": "Mokosh — идёт запись ($1)" }
const tooltip = chrome.i18n.getMessage('tooltipRecWithTime', formatElapsed(seconds));
chrome.action.setTitle({ title: tooltip });

State of the Art

Old approach Current approach When changed Impact
Bundle TTF / OTF directly WOFF2 only ~2020 (universal browser support) 3050% smaller, native browser parsing
Static-weight WOFF2 files Variable font WOFF2 2018 (Chrome 62+, FF 62+) Single file replaces 59 weight files for Newsreader
Inline manifest:name __MSG_*__ + _locales/ Pre-MV3 Required for Chrome Web Store store-listing localization
Google Fonts @import in MV2 popups Self-hosted WOFF2 in dist/ MV3 mandate (2024 deprecation of MV2) Forced by CSP style-src 'self' + font-src 'self'
Per-state PNG icon swaps via chrome.action.setIcon Neutral mark + setBadgeBackgroundColor 2015+ best practice Lower asset count; faster state transitions; no PNG decoding per state change
convert (ImageMagick) for SVG → PNG rsvg-convert (librsvg) (preference shift, both still work) rsvg-convert known-good for Inkscape-authored SVGs and produces predictable byte counts

Deprecated / outdated for this project:

  • A-06 per-state PNG variants (per D-06 = neutral mark + badge). Do not produce A-06 deliverables.
  • @import url(...remote...) (per MV3 CSP). Removed from tokens.css in Plan 01-12.
  • Engineering placeholder palette (#2563eb, #dc2626, etc. in src/popup/style.css and SW BADGE_REC_COLOR = '#00C853'). Replaced by --mks-rec etc. Note: src/background/index.ts:45 has BADGE_REC_COLOR = '#00C853' as a TS literal — the badge color is set via chrome.action.setBadgeBackgroundColor which takes an [r, g, b, a] tuple OR a hex string. JS can't var(--mks-*) directly; Plan 01-12 needs to mirror the token value as a TS constant (e.g. BADGE_REC_COLOR = '#b2543d' /* --mks-madder-600 */). Document the dual ownership clearly.

Environment Availability

Dependency Required by Available Version Fallback
rsvg-convert §3 PNG rasterization 2.60.0 inkscape --export-png (also installed) or convert (ImageMagick)
pyftsubset (fontTools) §1, §6 WOFF2 subsetting 4.63.0 glyphhanger (npm; would need install)
woff2_compress §1 WOFF2 encoding (system pkg) bundled inside pyftsubset already; standalone tool is redundant
inkscape §3 SVG rendering 1.4.4
convert / magick §3 SVG fallback (ImageMagick 7+, deprecated convert symlink)
python3 §1 subsetting + smoke.sh URL encoding 3.x (per pyftsubset / smoke.sh hashbang)
node Vite build 24.14.0
npm (or pnpm/yarn) Package management bundled with node
xmllint / brotli NOT required (pyftsubset bundles Brotli compression) not checked
Internet access (one-off, to fetch upstream IBM Plex + Newsreader releases) §1 source TTFs required at execute-plan time Can curl from CDN as alternate (https://fonts.gstatic.com/... paths leaked from the Brief's bundler — uuid'd file names extractable from same source)

Missing dependencies with no fallback: None.

Missing dependencies with fallback: None applicable.

Network requirement: One-time download of TTF source files from GitHub Releases (IBM Plex ~30 MB; Newsreader ~10 MB). Within scope of any normal curl/wget step in execute-plan.


Validation Architecture

Test Framework

Property Value
Framework vitest v4
Config file none in repo root; vitest uses defaults (verified via package.json:24)
Quick run command npx vitest run
Full suite command npx vitest run (no separate full / quick distinction in current setup)

Phase Requirements → Test Map

Req ID Behavior Test type Automated command File exists?
D-01 / D-06 icons/icon{16,48,128}.png exist, are PNGs, clear size floors unit (build artifact assertion) npx vitest run tests/build/icons-present.test.ts Wave 0
D-04 src/shared/tokens.css exists; popup CSS @imports it; no #2563eb/#dc2626 hex in popup CSS unit (codebase grep) npx vitest run tests/build/tokens-adopted.test.ts Wave 0
D-05 + §1 R1 src/shared/fonts/IBMPlexSans-{Regular,Medium,SemiBold,Bold}.woff2 exist; Newsreader-variable.woff2 exists; tokens.css @font-face rules match expected count unit (filesystem + grep) npx vitest run tests/build/fonts-present.test.ts Wave 0
D-07 manifest.json:name === "__MSG_extName__" AND _locales/en/messages.json has extName.message === "Mokosh — Session Capture" unit (JSON parity) npx vitest run tests/i18n/manifest-i18n.test.ts Wave 0
D-08 manifest.json:description === "__MSG_extDesc__"; _locales/{en,ru}/messages.json has extDesc with the tagline text unit (JSON parity) npx vitest run tests/i18n/manifest-i18n.test.ts (same as D-07)
D-09 dist/ after npm run build contains no file or content with smoke/SMOKE strings integration (filesystem walk after build) npm run build && npx vitest run tests/build/no-smoke-in-dist.test.ts Wave 0
(i18n parity) Every key in _locales/ru/messages.json exists in _locales/en/messages.json (the default_locale) unit npx vitest run tests/i18n/locale-parity.test.ts Wave 0
(CSP) Built dist/src/shared/tokens.css does not contain fonts.googleapis.com or any https:// URL unit npx vitest run tests/build/no-remote-fonts.test.ts Wave 0
(Operator manual) After load-unpacked: extension name in chrome://extensions reads "Mokosh — Session Capture"; tooltip on hover reads RU sober string; popup renders in Plex Sans; SAVE button is madder-rust color manual-only (operator UAT) n/a

Sampling rate

  • Per task commit: npx vitest run tests/build/*.test.ts tests/i18n/*.test.ts (Plan 01-12-specific tests only; ~12 seconds).
  • Per wave merge: npx vitest run && npm run build && npx tsc --noEmit (full suite + build clean + types clean).
  • Phase gate: Full suite green + operator empirical (load extension, verify name + popup + tooltip + notification rendering, capture screenshots) before /gsd-verify-work.

Wave 0 gaps

  • tests/build/icons-present.test.ts — covers D-01 / D-06 PNG floors
  • tests/build/tokens-adopted.test.ts — covers D-04 token usage in popup CSS
  • tests/build/fonts-present.test.ts — covers D-05 + §1 R1 WOFF2 presence + count
  • tests/i18n/manifest-i18n.test.ts — covers D-07 + D-08 manifest:name/description shape + messages.json values
  • tests/i18n/locale-parity.test.ts — covers i18n key parity across locales
  • tests/build/no-smoke-in-dist.test.ts — covers D-09 (no-op-confirmation test)
  • tests/build/no-remote-fonts.test.ts — covers CSP migration in tokens.css
  • tests/build/ directory may not exist yet — create it; convention matches existing tests/{background,offscreen}/ structure.

Security Domain

Applicable ASVS categories

ASVS category Applies Standard control
V2 Authentication no Plan 01-12 is brand integration; no auth surface
V3 Session Management no No sessions introduced
V4 Access Control no No new permission gates
V5 Input Validation yes (loosely) i18n messages.json files are JSON; ensure JSON validity in CI (vitest JSON.parse)
V6 Cryptography no No new crypto
V12 File / Resources yes Self-hosted WOFF2 files MUST live under src/shared/fonts/; no https:// URLs in tokens.css; Chrome MV3 CSP enforces this at runtime
V14 Configuration yes manifest.json:default_locale set; _locales/ directory complete; web_accessible_resources correctly auto-generated by @crxjs

Known threat patterns for this stack

Pattern STRIDE Standard mitigation
Remote font load via @import (MV3 CSP violation) Information Disclosure (font fingerprinting), Tampering (CDN MITM) Self-host all fonts; explicitly assert no https:// in built tokens.css via vitest
Malformed PNG triggering Chrome imageUtil rejection (Plan 01-09 known issue) Denial of Service (notification flow blocked) PNG byte-size floor assertions in tests (tests/build/icons-present.test.ts); empirical UAT
_locales/ key tampering causing manifest validation failure Denial of Service (extension fails to install) JSON validity check in Wave 0 tests; manifest:name resolution test
__MSG_*__ injection from user-controllable input Cross-Site Scripting (low surface; manifest fields don't render arbitrary user input) Not applicable — __MSG_*__ is engineering-authored, no user input flow
WOFF2 with malicious OpenType tables Code execution (historical browser CVEs) Use upstream-distributed faces (IBM, Production Type); do not accept third-party fonts

Assumptions Log

Every claim tagged [ASSUMED] in this research. The planner and discuss-phase should confirm these with the user before locking decisions that depend on them.

# Claim Section Risk if wrong
A1 default_locale: "en" is correct (Web Store convention) §11 If Mokosh is only distributed via Load Unpacked or operator-only channels, "ru" may be preferable. Low blast radius — flip the value, regenerate.
A2 The Russian "Сохранить отчёт об ошибке?" (label) + "Сохранить отчёт" (button) split per Brief §02 means DOM rework in src/popup/ §10 string #4 If user prefers keeping the single-button form, the rework is unnecessary. Verify before executing.
A3 src/shared/tokens.css is the right path (vs src/styles/tokens.css) "Claude's Discretion" + §9 Low blast radius — rename, update 23 @import paths.
A4 A Cyrillic-capable serif fallback should be added to --mks-font-display stack for Russian display text (§1 R1) §1 BLOCKER If user accepts the OS-serif fallback for Russian (Times New Roman on Windows, Iowan Old Style on macOS), no Cyrillic serif font needs subsetting + bundling. Lower bundle size; lower visual consistency.
A5 Newsreader-variable.woff2 covers the full italic + roman variable font range in a single file (Production Type's variable build may ship italic separately) §1, §13 If italic is a separate variable file, add a second @font-face rule with font-style: italic;. Verify at execute-plan time by downloading the actual release archive.
A6 The smoke page tokenization (§9 Option A) is "verbose but acceptable" — adds ~30 lines to smoke.sh's heredoc §9 If user thinks it's too noisy, fall back to Option C (leave smoke unstyled per tokens, accept the imperfection — smoke is dev-only).
A7 BADGE_REC_COLOR constant in src/background/index.ts should be updated to #b2543d (--mks-madder-600) with a comment cross-referencing the token §10 / Anti-patterns If user prefers keeping the existing material green, document the divergence between badge color and palette. The "dye reference" semantic of madder is the designer's intent per D-04.

If this table is empty, all claims in this research were verified or cited. This table has 7 entries — the planner should surface A1, A2, A4 in particular (highest-impact assumptions) during plan creation, ideally via the discussion log or as plan-level questions.


Open Questions

  1. D-05 Cyrillic for Newsreader (BLOCKER per §1)

    • What we know: Newsreader from upstream has no Cyrillic. Documented in Production Type repo and verified empirically in Decision Brief's @font-face subsets.
    • What's unclear: User's preference among R1 (add Cyrillic-capable serif fallback), R2 (substitute display family), R3 (extend Newsreader — out of scope).
    • Recommendation: Default to R1, add PT Serif as the Cyrillic fallback (OFL, ParaType, ~30 KB subsetted for RU); surface as a plan-level question.
  2. D-07 wording for English manifest:name

    • What we know: User picked "Mokosh — Session Capture".
    • What's unclear: Should the EN messages.json extName.message exactly match the user's pick, or follow D-07 alt "Mokosh / АИ Регистратор" for transliteration support?
    • Recommendation: Exact user pick (no transliteration). Lock as-stated unless user clarifies.
  3. Plan 01-10 coordination

    • What we know: Plan 01-10 created src/welcome/welcome.{html,ts,css} with placeholder palette (#00C853 green button, #1f1f1f text). Plan 01-12 wants to ingest tokens.
    • What's unclear: Does Plan 01-12 rewire Plan 01-10's CSS now (single-pass token migration), or does Plan 01-12 leave Plan 01-10 alone and a future Plan 01-13 / 02-XX migrates welcome separately?
    • Recommendation: Rewire in Plan 01-12. Welcome page is in Phase 1 scope; one-pass migration is cheaper than two-pass.
  4. D-08 tagline wording mismatch

    • What we know: Brief §02 wrote "Thirty seconds ago, always within reach."; brand-decisions-v1.md wrote "Thirty seconds ago, always at hand.". Both attribute to D-08 user-accepted.
    • What's unclear: Which exact wording for the EN parallel?
    • Recommendation: Default to brand-decisions-v1.md ("always at hand.") as it's the more recent doc and reflects the user's final pick. Surface as a single confirmation question.
  5. Plex Sans variable font availability

    • What we know: IBM published @ibm/plex-sans-variable@0.2.0 (June 2024). The static per-weight files are also distributed.
    • What's unclear: Whether the variable build is mature enough to replace the 4 static weights (which would shave ~80 KB).
    • Recommendation: Use static per-weight files (Pattern A from §1) for the first pass. Migrate to variable in a future polish phase if bundle size matters.
  6. Brief §02 string #4 popup-DOM rework

    • What we know: Brief splits "Сохранить отчёт об ошибке?" (label) from "Сохранить отчёт" (button).
    • What's unclear: Single-button vs label+button.
    • Recommendation: A2 in Assumptions Log. Default to single-button form; ask user during execute-plan if rework is wanted.

Sources

Primary (HIGH confidence)

Secondary (MEDIUM confidence)

Tertiary (LOW confidence — flagged)

  • WebSearch's initial claim that "Newsreader supports Cyrillic" — contradicted by primary sources (Decision Brief subsets, Production Type repo README). Resolved as BLOCKER §1.
  • WebSearch claim about Chrome notification icon 3:2 ratio and imageUtil floors — official docs don't document these; empirical Plan 01-09 finding is the only confirmed reference (assets-spec.md:72). The 1024-byte floor is verified by Plan 01-09 ops; not in any docs page.

Metadata

Confidence breakdown:

  • Standard stack: HIGH — all tools verified locally; versions checked via --version and npm view.
  • Architecture (tokens.css integration, @crxjs WAR auto-gen): HIGH — codebase + official docs.
  • WOFF2 self-hosting for Plex faces: HIGH — canonical subset structure verified from Decision Brief.
  • WOFF2 for Newsreader: MEDIUM — Latin-only subset is well-documented; Cyrillic handling required §1 BLOCKER resolution.
  • i18n via __MSG_*__: HIGH — official docs + repo + 2023 tutorial concur.
  • PNG rasterization: HIGH — locally empirically verified rsvg-convert at 16/48/128.
  • Smoke gating (D-09): HIGH — codebase grep + smoke.sh inspection confirm no-op status.
  • Notification icon floors (1024 B): MEDIUM — official docs don't document; relies on Plan 01-09 empirical finding.
  • Operator string i18n migration: HIGH — strings verbatim from Brief §02; call sites grep-verified.

Research date: 2026-05-17

Valid until: ~30 days (2026-06-16) for stable claims (font upstreams, OFL, Chrome i18n API). 7 days for cutting-edge claims (@crxjs/vite-plugin@2.4.0, IBM Plex variable v0.2.0 maturity).