diff --git a/library/packet/packet-render.js b/library/packet/packet-render.js
index 3931dbd5f..6e22c1499 100644
--- a/library/packet/packet-render.js
+++ b/library/packet/packet-render.js
@@ -26,7 +26,23 @@
// so a new Arabic-script locale is one entry here and not a schema change.
const RTL = new Set(['ar', 'he', 'fa', 'ur', 'ps', 'sd', 'yi']);
-export function createPacketRenderer({ strings, lang = 'en' }) {
+// Scripts with no coverage in the packet's Georgia/Palatino/serif stack, mapped to
+// the vendored face that does cover them. MEASURED by rendering each script at 48px
+// and reading the PIXELS: Latin, Cyrillic, Arabic and CJK all shape correctly with
+// system fonts; DEVANAGARI renders as tofu boxes.
+// ⚠ A width heuristic got this exactly backwards — it reported Arabic and Chinese
+// broken and Hindi fine, because Devanagari tofu boxes happen to match real glyph
+// advance. Never infer glyph coverage from measurements; look at the pixels.
+const SCRIPT_FONT = {
+ hi: { family: 'Noto Serif Devanagari', dir: 'noto-serif-devanagari',
+ file: 'noto-serif-devanagari-400-devanagari.woff2' },
+ mr: { family: 'Noto Serif Devanagari', dir: 'noto-serif-devanagari',
+ file: 'noto-serif-devanagari-400-devanagari.woff2' }, // Marathi, same script
+ ne: { family: 'Noto Serif Devanagari', dir: 'noto-serif-devanagari',
+ file: 'noto-serif-devanagari-400-devanagari.woff2' }, // Nepali, same script
+};
+
+export function createPacketRenderer({ strings, lang = 'en', fontData = null }) {
const EN = strings.en;
const L = strings[lang] || {};
const t = (k) => (L[k] !== undefined ? L[k] : EN[k]);
@@ -56,8 +72,28 @@ export function createPacketRenderer({ strings, lang = 'en' }) {
const sheet = (inner, cls = '') =>
`
${inner}
`;
+ // The script font for THIS language, if it needs one. `fontData` is a base64
+ // woff2 the caller supplies: the packet must stay a SELF-CONTAINED file that
+ // prints offline (the Node generator writes standalone HTML with no server, and
+ // photos are already inlined for the same reason), so a URL would break exactly
+ // where the packet is most used. Absent fontData, the rule is simply omitted and
+ // the stack falls back to serif — a legible wrong font beats a broken document.
+ const SF = SCRIPT_FONT[lang] || null;
+ const fontFace = (SF && fontData)
+ ? `@font-face{font-family:'${SF.family}';font-style:normal;font-weight:400;`
+ + `font-display:swap;src:url(data:font/woff2;base64,${fontData}) format('woff2')}`
+ : '';
+ // Prepend the script face to EVERY element's stack in one rule rather than
+ // editing 22 hardcoded Georgia declarations. The browser falls through per
+ // CHARACTER, so Latin text in a Hindi packet still renders in Georgia.
+ const scriptStack = SF
+ ? `.sheet,.sheet *{font-family:'${SF.family}',Georgia,'Palatino Linotype',serif !important}`
+ : '';
+
function css() {
return `
+ ${fontFace}
+ ${scriptStack}
@page{size:letter portrait;margin:0}
html,body{margin:0;background:#fff;-webkit-print-color-adjust:exact;print-color-adjust:exact}
.sheet{position:relative;width:${W}px;height:${H}px;background:#f7f4ec;overflow:hidden;
diff --git a/library/scripts/posters/packet-strings.json b/library/scripts/posters/packet-strings.json
index 125a4b3e2..2778e9343 100644
--- a/library/scripts/posters/packet-strings.json
+++ b/library/scripts/posters/packet-strings.json
@@ -4,7 +4,7 @@
"how_to_translate": "Copy the \"en\" block, change the key to the ISO code, translate the values. Keep {placeholders} exactly as they are — they are substituted at render time. Do not translate scripture REFERENCES (book names are resolved by the reader's own Bible); DO translate the prayer text.",
"note_on_scripture": "point4_passage is the ONLY full scripture text in the packet (Mark 5:1-20, printed in full at the Driver's instruction). A translation MUST supply its own rendering in a public-domain or properly-licensed version FOR THAT LANGUAGE. ⚠ Do NOT translate it yourself and do NOT leave a machine rendering in place: this is printed scripture in a prayer packet. A language block that omits point4_passage falls back to the English WEB text per-key, which is honest — an untrusted translation of Mark 5 is not.",
"note_on_references": "Scripture REFERENCES (Matthew 27:27, Mark 5:1-20) are deliberately left in English in every language block, so a reader can look them up in their own Bible regardless of the packet's language. ⚠ MEASURED TRADEOFF, not an oversight: on a rendered French page the English book names are visibly foreign next to fully-translated prayer text. If a language wants localised book names, add a `books` map to that block and teach the renderer to use it — do NOT translate the refs inline, because the same strings identify the passage for any future lookup.",
- "note_on_scripts": "MEASURED 2026-09-24 by rendering each script at 48px and READING THE PIXELS: Latin, Cyrillic (ru), Arabic (ar) and CJK (zh) all shape correctly in the packet's Georgia/serif stack. DEVANAGARI (hi) renders as TOFU BOXES — this box has zero Devanagari fonts installed (fc-list). So hi is BLOCKED on vendoring a Devanagari font (e.g. Noto Serif Devanagari) into public/assets/fonts and adding it to the packet's font stack; adding a hi block before then would ship a page of empty rectangles. ⚠ A WIDTH HEURISTIC GOT THIS BACKWARDS: comparing rendered advance against a missing-font baseline reported ar and zh as broken and hi as fine — the exact opposite of the truth, because Devanagari tofu boxes happen to match real glyph advance. Read the pixels.",
+ "note_on_scripts": "ALL 11 app locales now ship. Script coverage MEASURED by rendering at 48px and READING THE PIXELS: Latin, Cyrillic (ru), Arabic (ar) and CJK (zh) shape correctly with the system Georgia/Palatino/serif stack. DEVANAGARI (hi) does NOT — it renders as tofu boxes — so Noto Serif Devanagari is vendored (OFL) and base64-INLINED into hi packets by the renderer's SCRIPT_FONT map. Inlined, not linked, because a packet is a standalone file that must print offline; ~165KB on a 3.5MB packet, and only for that language. mr and ne map to the same face.\n⚠ TWO MEASUREMENTS LIED ABOUT THIS, so do not automate it cheaply: (1) comparing shaped width against a missing font reported ar and zh BROKEN and hi FINE — exactly backwards, because Devanagari tofu boxes match real glyph advance; (2) an ink-column-profile heuristic scored CORRECTLY-RENDERED Devanagari the same as tofu (3 vs 2 distinct heights), because its baseline bar gives a uniform profile. check:packet-render therefore asserts only that a DECLARED @font-face LOADED — the mechanical half — and leaves glyph correctness to a human reading a screenshot. A detector that blocks working packets is worse than none.",
"note_on_rtl": "ar is right-to-left. The renderer emits dir on from an RTL set (packet-render.js), and scripture REFERENCES are wrapped in dir=ltr + unicode-bidi:isolate. Without that isolation the bidi algorithm reorders a reference that STARTS WITH A DIGIT: '1 Peter 5:6-11' rendered as 'Peter 5:6-11 1'. Note the reference LIST then reads right-to-left on an RTL page (first item rightmost) — that is correct for an RTL reader, not a defect."
},
"en": {
@@ -1430,5 +1430,321 @@
]
}
]
+ },
+ "hi": {
+ "map_page_kicker": "अनछुए जनसमूह",
+ "map_page_sub": "{count} अनछुए जनसमूहों की प्रार्थना सूची",
+ "list_header": "{count} में से {from}–{to} · उनके नाम लेकर प्रार्थना करें",
+ "people_suffix": "लोग",
+ "no_photo": "कोई चित्र उपलब्ध नहीं",
+ "deck_title": "प्रार्थना कार्ड",
+ "card_country": "देश",
+ "card_population": "जनसंख्या",
+ "card_language": "भाषा",
+ "card_religion": "धर्म",
+ "card_cta": "उनके लिए प्रार्थना करें",
+ "points_title": "12 प्रार्थना बिंदु",
+ "points_intro": "परमेश्वर के साथ एक घंटा, हर बिंदु के लिए पाँच मिनट। इस पैकेट के जनसमूहों के लिए ये बारह बिंदु प्रार्थना करें।",
+ "points_attrib": "प्रार्थना लय Metacamp से प्रेरित · metacamp.org",
+ "wheel_title": "प्रार्थना चक्र",
+ "wheel_intro": "परमेश्वर के साथ एक घंटा बिताना? यह आपके सोचने से आसान है। इसे 5 मिनट के हिस्सों में बाँटें और हर हिस्से में इनमें से एक पर ध्यान दें।",
+ "wheel_minutes": "मिनट",
+ "wheel_attrib": "प्रार्थना चक्र · Metacamp मॉड्यूल 1 · metacamp.org",
+ "privacy_note": "चिह्नों की स्थिति अनुमानित है · गोपनीयता के लिए जनसमूहों के स्थान स्रोत पर सामान्यीकृत किए गए हैं",
+ "rhythms": {
+ "praise": "स्तुति",
+ "wait": "प्रतीक्षा",
+ "confess": "अंगीकार",
+ "read": "वचन पढ़ना",
+ "ask": "माँगना",
+ "intercede": "मध्यस्थता",
+ "pray_word": "वचन से प्रार्थना",
+ "thank": "धन्यवाद",
+ "sing": "गायन",
+ "meditate": "मनन",
+ "listen": "सुनना",
+ "praise_end": "स्तुति"
+ },
+ "points": [
+ {
+ "n": 1,
+ "rhythm": "praise",
+ "sub": "दो क्रूसों का सुसमाचार",
+ "text": "कि वे मसीह को राजा मानकर ग्रहण करें, उसके क्रूस से क्षमा पाएँ — और अपना क्रूस उठाएँ, सताव के क्षेत्र में दुख उठाने को तैयार। मेम्ने को उसके दुख का प्रतिफल मिले, मुझसे आरंभ करके।",
+ "refs": [
+ "Matthew 27:27",
+ "Matthew 16:24–25"
+ ]
+ },
+ {
+ "n": 2,
+ "rhythm": "wait",
+ "sub": "पवित्र आत्मा और छुटकारा",
+ "text": "कि वे आत्मा से भरें और साहसी गवाह बनें; अकेले न रहें; जल और आत्मा से बपतिस्मा पाएँ; बुराई की हर पकड़ टूट जाए। कि पड़ोसी जातियों के विश्वासी मज़दूरों के रूप में उठ खड़े हों।",
+ "refs": [
+ "Acts 1:8",
+ "John 14:26"
+ ]
+ },
+ {
+ "n": 3,
+ "rhythm": "confess",
+ "sub": "",
+ "text": "हम इस अविश्वास को मानते हैं कि यह परमेश्वर के लिए बहुत बड़ा है। कि वह संसार के अनछुए लोगों के लिए हमारे प्रेम और विश्वास को फिर से जगाए।",
+ "refs": [
+ "John 3:16",
+ "1 John 1:9"
+ ]
+ },
+ {
+ "n": 4,
+ "rhythm": "read",
+ "sub": "पिता का प्रेम",
+ "text": "कि वे उद्धार पाएँ और जानें कि उनका स्वर्गीय पिता उनसे प्रेम करता है — और उनके द्वारा वह उनके परिवार, मित्रों, पड़ोसियों, सहकर्मियों, सहपाठियों, उन सबके पास उद्धार पहुँचाए जिन्हें वे व्यक्तिगत रूप से जानते हैं।",
+ "refs": [
+ "Mark 5:1–20",
+ "Acts 16:31"
+ ],
+ "print_passage": true
+ },
+ {
+ "n": 5,
+ "rhythm": "ask",
+ "sub": "बपतिस्मा",
+ "text": "स्थानीय लोगों में नए बपतिस्मा हों, और हर बपतिस्मा पाया हुआ व्यक्ति दूसरों को बपतिस्मा देने के लिए प्रशिक्षित हो, और यह बढ़ता जाए।",
+ "refs": [
+ "Acts 16",
+ "Acts 10"
+ ]
+ },
+ {
+ "n": 6,
+ "rhythm": "intercede",
+ "sub": "मज़दूर और चरवाहे",
+ "text": "मज़दूर भेजे जाएँ जो उस जाति से गहरा प्रेम करें जिससे वे कभी नहीं मिले; और भीतर से अच्छे चरवाहे उठाए जाएँ।",
+ "refs": [
+ "Luke 10:2",
+ "John 10:11–15"
+ ]
+ },
+ {
+ "n": 7,
+ "rhythm": "pray_word",
+ "sub": "शांति के जन और घर",
+ "text": "कि मज़दूर और स्थानीय अगुवे ग्रहणशील व्यक्ति और शांति के घर को पाएँ, और उन्हें भली भाँति चेला बनाएँ।",
+ "refs": [
+ "Luke 10:5–7",
+ "Matthew 10:5–14"
+ ]
+ },
+ {
+ "n": 8,
+ "rhythm": "thank",
+ "sub": "विश्राम",
+ "text": "परमेश्वर का धन्यवाद कि उसने विश्राम दिया है; कि वे मसीह में उस विश्राम में प्रवेश करें, और उनका आत्मिक परिवार एक सुरक्षित स्थान हो।",
+ "refs": [
+ "Mark 4:26–29",
+ "Hebrews 4:9–11"
+ ]
+ },
+ {
+ "n": 9,
+ "rhythm": "sing",
+ "sub": "",
+ "text": "कि वह जनसमूह आराधकों से भर जाए — हर जगह आराधना, स्वतंत्रता में और बेड़ियों में भी।",
+ "refs": [
+ "Acts 16:25–26",
+ "Colossians 3:16"
+ ]
+ },
+ {
+ "n": 10,
+ "rhythm": "meditate",
+ "sub": "प्रभु भोज",
+ "text": "कि वे मिलकर प्रभु भोज लें और उसकी मृत्यु, गाड़े जाने और पुनरुत्थान के भार को समझें।",
+ "refs": [
+ "Matthew 26:26–30",
+ "1 Corinthians 11:23–26"
+ ]
+ },
+ {
+ "n": 11,
+ "rhythm": "listen",
+ "sub": "जवाबदेही",
+ "text": "केवल यह नहीं कि वे सुनें, परन्तु यह कि उनके जीवन में ऐसे लोग हों जो उन्हें वचन पढ़ने, प्रार्थना करने, संगति में लगे रहने और दुख का सही उत्तर देने में सहायता करें — उस शत्रु का सामना करते हुए जो सारे संसार में विश्वासियों के पीछे पड़ा है।",
+ "refs": [
+ "1 Peter 5:6–11",
+ "Hebrews 10:24–25"
+ ]
+ },
+ {
+ "n": 12,
+ "rhythm": "praise_end",
+ "sub": "महान · और महान · सबसे महान",
+ "text": "पहले से स्तुति — उद्धार के लिए; और महान, बढ़ते हुए आत्मिक परिवारों के लिए; सबसे महान, समर्पित जीवनों और उठाए गए स्थानीय कलीसिया-रोपकों के लिए जो पूरे जनसमूह में स्वस्थ कलीसियाएँ बढ़ाते हैं, चाहे मूल्य कुछ भी हो।",
+ "refs": [
+ "Acts 2:42–47",
+ "Matthew 28:19–20"
+ ]
+ }
+ ]
+ },
+ "zh": {
+ "map_page_kicker": "未得之民",
+ "map_page_sub": "為 {count} 個未得之民的禱告名單",
+ "list_header": "{count} 之中的 {from}–{to} · 按名為他們禱告",
+ "people_suffix": "人",
+ "no_photo": "沒有照片",
+ "deck_title": "禱告卡",
+ "card_country": "國家",
+ "card_population": "人口",
+ "card_language": "語言",
+ "card_religion": "宗教",
+ "card_cta": "為他們禱告",
+ "points_title": "12 個禱告要點",
+ "points_intro": "與神同在一小時,每個要點五分鐘。為這份禱告冊中的族群禱告這十二個要點。",
+ "points_attrib": "禱告節奏取材自 Metacamp · metacamp.org",
+ "wheel_title": "禱告輪",
+ "wheel_intro": "與神同在一小時?比你想的容易。把它分成五分鐘一段,每一段專注其中一個要點。",
+ "wheel_minutes": "分鐘",
+ "wheel_attrib": "禱告輪 · Metacamp 第一單元 · metacamp.org",
+ "privacy_note": "標記位置為概略位置 · 族群所在地已於資料來源處作概化處理以保護隱私",
+ "rhythms": {
+ "praise": "讚美",
+ "wait": "等候",
+ "confess": "認罪",
+ "read": "讀神的話",
+ "ask": "祈求",
+ "intercede": "代禱",
+ "pray_word": "以神的話禱告",
+ "thank": "感謝",
+ "sing": "歌唱",
+ "meditate": "默想",
+ "listen": "聆聽",
+ "praise_end": "讚美"
+ },
+ "points": [
+ {
+ "n": 1,
+ "rhythm": "praise",
+ "sub": "兩個十字架的福音",
+ "text": "願他們接受基督為王,藉著祂的十字架得赦免——也背起自己的十字架,甘願在逼迫之地受苦。願羔羊得著祂受苦的賞賜,從我開始。",
+ "refs": [
+ "Matthew 27:27",
+ "Matthew 16:24–25"
+ ]
+ },
+ {
+ "n": 2,
+ "rhythm": "wait",
+ "sub": "聖靈與釋放",
+ "text": "願他們被聖靈充滿,成為剛強的見證人;不至孤單;受水和聖靈的洗;一切邪惡的轄制都被打破。願鄰近族群中的信徒被興起作工人。",
+ "refs": [
+ "Acts 1:8",
+ "John 14:26"
+ ]
+ },
+ {
+ "n": 3,
+ "rhythm": "confess",
+ "sub": "",
+ "text": "我們承認自己的不信,以為這事對神太難。願祂重新點燃我們對普世未得之民的愛與信心。",
+ "refs": [
+ "John 3:16",
+ "1 John 1:9"
+ ]
+ },
+ {
+ "n": 4,
+ "rhythm": "read",
+ "sub": "天父的愛",
+ "text": "願他們得救,並知道天父愛他們——也願神藉著他們,把救恩帶給他們的家人、朋友、鄰舍、同事、同學,以及他們所認識的每一個人。",
+ "refs": [
+ "Mark 5:1–20",
+ "Acts 16:31"
+ ],
+ "print_passage": true
+ },
+ {
+ "n": 5,
+ "rhythm": "ask",
+ "sub": "洗禮",
+ "text": "願當地有新的受洗者,並且每一位受洗的人都被裝備去為別人施洗,不斷繁增。",
+ "refs": [
+ "Acts 16",
+ "Acts 10"
+ ]
+ },
+ {
+ "n": 6,
+ "rhythm": "intercede",
+ "sub": "工人與牧人",
+ "text": "願工人被差派去深愛一群素未謀面的人;也願好牧人從他們中間被興起。",
+ "refs": [
+ "Luke 10:2",
+ "John 10:11–15"
+ ]
+ },
+ {
+ "n": 7,
+ "rhythm": "pray_word",
+ "sub": "平安之子與平安之家",
+ "text": "願工人與當地帶領者尋得那樂意接待的人和平安之家,並好好門訓他們。",
+ "refs": [
+ "Luke 10:5–7",
+ "Matthew 10:5–14"
+ ]
+ },
+ {
+ "n": 8,
+ "rhythm": "thank",
+ "sub": "安息",
+ "text": "感謝神所賜的安息;願他們在基督裡進入那安息,也願他們的屬靈家庭是安全的地方。",
+ "refs": [
+ "Mark 4:26–29",
+ "Hebrews 4:9–11"
+ ]
+ },
+ {
+ "n": 9,
+ "rhythm": "sing",
+ "sub": "",
+ "text": "願這族群充滿敬拜者——處處敬拜,在自由中,也在捆鎖中。",
+ "refs": [
+ "Acts 16:25–26",
+ "Colossians 3:16"
+ ]
+ },
+ {
+ "n": 10,
+ "rhythm": "meditate",
+ "sub": "聖餐",
+ "text": "願他們一同領受聖餐,深深體會祂的死、埋葬與復活的份量。",
+ "refs": [
+ "Matthew 26:26–30",
+ "1 Corinthians 11:23–26"
+ ]
+ },
+ {
+ "n": 11,
+ "rhythm": "listen",
+ "sub": "彼此守望",
+ "text": "不只願他們聆聽,更願他們身邊有人幫助他們讀神的話、禱告、委身群體,並在苦難中正確回應——抵擋那在全世界追逼信徒的仇敵。",
+ "refs": [
+ "1 Peter 5:6–11",
+ "Hebrews 10:24–25"
+ ]
+ },
+ {
+ "n": 12,
+ "rhythm": "praise_end",
+ "sub": "大 · 更大 · 最大",
+ "text": "先行讚美——為著得救的人;更大的,為著不斷繁增的屬靈家庭;最大的,為著獻上的生命,以及被興起的本地植堂者,無論代價如何,都在整個族群中繁增健康的教會。",
+ "refs": [
+ "Acts 2:42–47",
+ "Matthew 28:19–20"
+ ]
+ }
+ ]
}
}
\ No newline at end of file
diff --git a/library/scripts/posters/packet.mjs b/library/scripts/posters/packet.mjs
index 29a53e0e0..51e602a1b 100644
--- a/library/scripts/posters/packet.mjs
+++ b/library/scripts/posters/packet.mjs
@@ -74,7 +74,30 @@ const fill = (s, vars) => String(s).replace(/\{(\w+)\}/g, (_, k) => (vars[k] ??
// the SAME packet from the SAME code. Changing a page here changes it in both.
const { createPacketRenderer } = await import(
join(__dirname, '..', '..', 'packet', 'packet-render.js'));
-const R = createPacketRenderer({ strings: STRINGS, lang: LANG });
+// SCRIPT FONT, inlined. Some languages need a face the system stack does not
+// carry — Hindi renders as tofu boxes in Georgia/Palatino/serif. The packet is a
+// standalone file meant to print offline, so the font is base64'd INTO it rather
+// than linked, exactly as the photos are. Absent, the renderer omits the rule and
+// falls back to serif rather than failing.
+const SCRIPT_FONT_FILE = {
+ hi: 'noto-serif-devanagari/noto-serif-devanagari-400-devanagari.woff2',
+ mr: 'noto-serif-devanagari/noto-serif-devanagari-400-devanagari.woff2',
+ ne: 'noto-serif-devanagari/noto-serif-devanagari-400-devanagari.woff2',
+};
+let fontData = null;
+const fontRel = SCRIPT_FONT_FILE[LANG];
+if (fontRel) {
+ const fp = join(__dirname, '..', '..', '..', 'public', 'assets', 'fonts', fontRel);
+ if (existsSync(fp)) {
+ fontData = readFileSync(fp).toString('base64');
+ } else {
+ // Loud, not silent: a missing font here means the packet prints as empty
+ // rectangles, which looks like a content bug rather than a missing asset.
+ console.warn(`[packet] ⚠ ${LANG} needs ${fontRel} and it is MISSING — `
+ + 'the packet will render as tofu boxes. Vendor the font first.');
+ }
+}
+const R = createPacketRenderer({ strings: STRINGS, lang: LANG, fontData });
const { mapPage, listPages, pointsPage, wheelPage, css, mapCss, PER_PAGE, W, H } = R;
// ── DATA ────────────────────────────────────────────────────────────────────
diff --git a/public/assets/fonts/noto-serif-devanagari/LICENSE b/public/assets/fonts/noto-serif-devanagari/LICENSE
new file mode 100644
index 000000000..cd2cc5c94
--- /dev/null
+++ b/public/assets/fonts/noto-serif-devanagari/LICENSE
@@ -0,0 +1,93 @@
+Copyright 2022 The Noto Project Authors (https://github.com/notofonts/devanagari)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/public/assets/fonts/noto-serif-devanagari/README.md b/public/assets/fonts/noto-serif-devanagari/README.md
new file mode 100644
index 000000000..68253f5db
--- /dev/null
+++ b/public/assets/fonts/noto-serif-devanagari/README.md
@@ -0,0 +1,24 @@
+# Noto Serif Devanagari — the Devanagari subset only
+
+**Why vendored:** the prayer packet's font stack is Georgia / Palatino Linotype /
+serif, none of which carry Devanagari. Measured by rendering Devanagari at 48px
+and READING THE PIXELS, Hindi came out as **tofu boxes** — this machine has zero
+Devanagari fonts (`fc-list`). A `hi` translation without this file would ship a
+page of empty rectangles.
+
+**Licence:** SIL Open Font License 1.1 (see `LICENSE`). Free to bundle and serve.
+
+**Subset:** `U+0900-097F` plus the Devanagari extensions Google's own subsetting
+selects — the *devanagari* range ONLY, matching how `poppins/` ships `latin` and
+`latin-ext` separately so a page downloads only what it needs. 124 KB, against
+6-8 KB for a Latin subset; that is the cost of the script's glyph count, and it
+loads only on a Hindi packet.
+
+**Source:** the woff2 Google Fonts serves for
+`Noto+Serif+Devanagari:wght@400;700` with a modern browser UA. Note the 400 and
+700 faces resolve to the SAME file — it is a variable font, so one file covers
+both weights and vendoring two would duplicate 124 KB for nothing.
+
+To refresh: request that CSS with a Chrome user-agent (without one Google serves
+unsubsetted TTF), take the `@font-face` block whose `unicode-range` contains
+`U+0900`, and download its woff2.
diff --git a/public/assets/fonts/noto-serif-devanagari/noto-serif-devanagari-400-devanagari.woff2 b/public/assets/fonts/noto-serif-devanagari/noto-serif-devanagari-400-devanagari.woff2
new file mode 100644
index 000000000..713584ed3
Binary files /dev/null and b/public/assets/fonts/noto-serif-devanagari/noto-serif-devanagari-400-devanagari.woff2 differ