fix(reader): use blob URLs for fonts to bypass sandboxed iframe restrictions

The paginator renders inside a sandboxed iframe that blocks @font-face
URL fetches. Fonts were never loading — all font-family rules fell back
to the generic 'serif' system font, making every reading font identical.

Fix: fetch font files on the parent page, create blob: URLs via
URL.createObjectURL(), and use those blob URLs in the @font-face rules
injected into the iframe via setStyles(). Blob URLs are always
same-origin with the creating document, so the sandboxed iframe can
access them with allow-same-origin.

Also added Playwrite NZ Guides as a test font for verifying font
switching works.
This commit is contained in:
2026-04-19 21:36:19 -04:00
parent 0007b4d1d0
commit 9ca8e2aa44
4 changed files with 80 additions and 136 deletions
+1
View File
@@ -258,6 +258,7 @@ templ ReaderSettingsPanel() {
<option value="noto-serif">Noto Serif</option>
<option value="charis-sil">Charis SIL</option>
<option value="ibm-plex">IBM Plex Serif</option>
<option value="playwrite">Playwrite NZ Guides (Test)</option>
</select>
</label>
<label class="block mb-2">
+4 -4
View File
@@ -189,7 +189,7 @@ func ReaderSettingsPanel() templ.Component {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"dockable-panel\" data-panel=\"settings\" data-side=\"left\" x-ref=\"settingsPanel\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" @click=\"toggleWindowShade($refs.settingsPanel)\"><h3 class=\"panel-title font-semibold\">⚙️ Settings</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"window-shade-toggle\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\"><!-- Display settings --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Display</h3><label class=\"block mb-2\">Chrome Behavior <select name=\"chrome_behavior\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"auto-hide\">Auto Hide</option> <option value=\"always-visible\">Always Visible</option> <option value=\"hide-on-scroll\">Hide on Scroll</option></select></label> <label class=\"block mb-2\">Progress Mode <select name=\"progress_mode\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"pages\">Pages</option> <option value=\"chapter\">Chapter</option> <option value=\"percentage\">Percentage</option> <option value=\"time-left\">Time Left</option></select></label></div><!-- Reading Theme (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><div class=\"flex items-center justify-between mb-2\"><h3 class=\"font-semibold\">Reading Theme</h3><button @click=\"toggleReadingMode()\" class=\"relative w-14 h-7 rounded-full transition-colors duration-200 flex items-center px-1\" :class=\"readingMode === 'dark' ? 'bg-blue-500' : 'bg-gray-400'\" type=\"button\" :aria-label=\"readingMode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'\"><span class=\"absolute top-0.5 left-0.5 w-6 h-6 bg-white rounded-full shadow transition-transform duration-200 flex items-center justify-center text-xs\" :class=\"readingMode === 'dark' ? 'translate-x-7' : 'translate-x-0'\" x-text=\"readingMode === 'dark' ? '🌙' : '☀️'\">☀️</span></button></div><label class=\"block mb-2\"><select name=\"reading_theme\" x-model=\"readingTheme\" @change=\"applyTheme()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><optgroup label=\"📖 Classic Reading\"><option value=\"light\">Light</option> <option value=\"paper\">Paper</option> <option value=\"sepia\">Sepia</option> <option value=\"parchment\">Parchment</option> <option value=\"warm\">Warm</option> <option value=\"candlelight\">Candlelight</option></optgroup> <optgroup label=\"🌤️ Sky & Atmosphere\"><option value=\"azure\">Azure</option> <option value=\"sky\">Sky</option> <option value=\"arctic\">Arctic</option> <option value=\"frost\">Frost</option></optgroup> <optgroup label=\"🌅 Sunset & Warmth\"><option value=\"dusk\">Dusk</option> <option value=\"sunset\">Sunset</option> <option value=\"twilight\">Twilight</option></optgroup> <optgroup label=\"🌲 Nature & Earth\"><option value=\"forest\">Forest</option> <option value=\"moss\">Moss</option> <option value=\"slate\">Slate</option></optgroup> <optgroup label=\"⚡ High Performance\"><option value=\"oled\">OLED</option> <option value=\"solarized\">Solarized</option></optgroup></select></label></div><!-- Typography (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><h3 class=\"font-semibold mb-2\">Typography</h3><label class=\"block mb-2\">Reading Font <select name=\"reading_font\" x-model=\"readingFont\" @change=\"applyFont()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"literata\">Literata (Default)</option> <option value=\"crimson\">Crimson Text</option> <option value=\"source-serif\">Source Serif 4</option> <option value=\"eb-garamond\">EB Garamond</option> <option value=\"libertinus\">Libertinus Serif</option> <option value=\"noto-serif\">Noto Serif</option> <option value=\"charis-sil\">Charis SIL</option> <option value=\"ibm-plex\">IBM Plex Serif</option></select></label> <label class=\"block mb-2\">Font Size <input type=\"range\" name=\"font_size\" x-model.number=\"fontSize\" @change=\"applyFont()\" min=\"10\" max=\"40\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"fontSize + 'px'\" style=\"color: var(--text-secondary)\">18px</span></label> <label class=\"block mb-2\">Line Height <input type=\"range\" name=\"line_height\" x-model.number=\"lineHeight\" @change=\"applyFont()\" min=\"0.8\" max=\"3.0\" step=\"0.1\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"lineHeight\" style=\"color: var(--text-secondary)\">1.6</span></label></div><!-- Navigation --><div class=\"mb-6\" x-show=\"isFixedLayout\"><h3 class=\"font-semibold mb-2\">Navigation</h3><label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"double_page_spread\" class=\"mr-2\"> Double Page Spread</label></div><button @click=\"toggleSettings()\" class=\"w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">Done</button></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"dockable-panel\" data-panel=\"settings\" data-side=\"left\" x-ref=\"settingsPanel\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" @click=\"toggleWindowShade($refs.settingsPanel)\"><h3 class=\"panel-title font-semibold\">⚙️ Settings</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"window-shade-toggle\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\"><!-- Display settings --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Display</h3><label class=\"block mb-2\">Chrome Behavior <select name=\"chrome_behavior\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"auto-hide\">Auto Hide</option> <option value=\"always-visible\">Always Visible</option> <option value=\"hide-on-scroll\">Hide on Scroll</option></select></label> <label class=\"block mb-2\">Progress Mode <select name=\"progress_mode\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"pages\">Pages</option> <option value=\"chapter\">Chapter</option> <option value=\"percentage\">Percentage</option> <option value=\"time-left\">Time Left</option></select></label></div><!-- Reading Theme (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><div class=\"flex items-center justify-between mb-2\"><h3 class=\"font-semibold\">Reading Theme</h3><button @click=\"toggleReadingMode()\" class=\"relative w-14 h-7 rounded-full transition-colors duration-200 flex items-center px-1\" :class=\"readingMode === 'dark' ? 'bg-blue-500' : 'bg-gray-400'\" type=\"button\" :aria-label=\"readingMode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'\"><span class=\"absolute top-0.5 left-0.5 w-6 h-6 bg-white rounded-full shadow transition-transform duration-200 flex items-center justify-center text-xs\" :class=\"readingMode === 'dark' ? 'translate-x-7' : 'translate-x-0'\" x-text=\"readingMode === 'dark' ? '🌙' : '☀️'\">☀️</span></button></div><label class=\"block mb-2\"><select name=\"reading_theme\" x-model=\"readingTheme\" @change=\"applyTheme()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><optgroup label=\"📖 Classic Reading\"><option value=\"light\">Light</option> <option value=\"paper\">Paper</option> <option value=\"sepia\">Sepia</option> <option value=\"parchment\">Parchment</option> <option value=\"warm\">Warm</option> <option value=\"candlelight\">Candlelight</option></optgroup> <optgroup label=\"🌤️ Sky & Atmosphere\"><option value=\"azure\">Azure</option> <option value=\"sky\">Sky</option> <option value=\"arctic\">Arctic</option> <option value=\"frost\">Frost</option></optgroup> <optgroup label=\"🌅 Sunset & Warmth\"><option value=\"dusk\">Dusk</option> <option value=\"sunset\">Sunset</option> <option value=\"twilight\">Twilight</option></optgroup> <optgroup label=\"🌲 Nature & Earth\"><option value=\"forest\">Forest</option> <option value=\"moss\">Moss</option> <option value=\"slate\">Slate</option></optgroup> <optgroup label=\"⚡ High Performance\"><option value=\"oled\">OLED</option> <option value=\"solarized\">Solarized</option></optgroup></select></label></div><!-- Typography (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><h3 class=\"font-semibold mb-2\">Typography</h3><label class=\"block mb-2\">Reading Font <select name=\"reading_font\" x-model=\"readingFont\" @change=\"applyFont()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"literata\">Literata (Default)</option> <option value=\"crimson\">Crimson Text</option> <option value=\"source-serif\">Source Serif 4</option> <option value=\"eb-garamond\">EB Garamond</option> <option value=\"libertinus\">Libertinus Serif</option> <option value=\"noto-serif\">Noto Serif</option> <option value=\"charis-sil\">Charis SIL</option> <option value=\"ibm-plex\">IBM Plex Serif</option> <option value=\"playwrite\">Playwrite NZ Guides (Test)</option></select></label> <label class=\"block mb-2\">Font Size <input type=\"range\" name=\"font_size\" x-model.number=\"fontSize\" @change=\"applyFont()\" min=\"10\" max=\"40\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"fontSize + 'px'\" style=\"color: var(--text-secondary)\">18px</span></label> <label class=\"block mb-2\">Line Height <input type=\"range\" name=\"line_height\" x-model.number=\"lineHeight\" @change=\"applyFont()\" min=\"0.8\" max=\"3.0\" step=\"0.1\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"lineHeight\" style=\"color: var(--text-secondary)\">1.6</span></label></div><!-- Navigation --><div class=\"mb-6\" x-show=\"isFixedLayout\"><h3 class=\"font-semibold mb-2\">Navigation</h3><label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"double_page_spread\" class=\"mr-2\"> Double Page Spread</label></div><button @click=\"toggleSettings()\" class=\"w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">Done</button></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -293,7 +293,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.CfiPosition)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 358, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 359, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -306,7 +306,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 361, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 362, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -319,7 +319,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Position)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 363, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 364, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
+67 -132
View File
@@ -12,142 +12,76 @@ const FONT_MAP: Record<string, string> = {
"noto-serif": '"Noto Serif"',
"charis-sil": '"Charis SIL"',
"ibm-plex": '"IBM Plex Serif"',
"playwrite": '"Playwrite NZ Guides"',
};
const FONT_FACE_RULES = `
@font-face {
font-family: "Literata";
src: url("/static/fonts/literata/Literata-Variable.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
interface FontFile {
family: string;
url: string;
weight: string;
style: string;
}
@font-face {
font-family: "Literata";
src: url("/static/fonts/literata/Literata-Italic-Variable.woff2") format("woff2");
font-weight: 100 900;
font-style: italic;
const FONT_FILES: FontFile[] = [
{ family: '"Literata"', url: "/static/fonts/literata/Literata-Variable.woff2", weight: "100 900", style: "normal" },
{ family: '"Literata"', url: "/static/fonts/literata/Literata-Italic-Variable.woff2", weight: "100 900", style: "italic" },
{ family: '"Crimson Pro"', url: "/static/fonts/crimson/CrimsonPro-Variable.woff2", weight: "100 900", style: "normal" },
{ family: '"Crimson Pro"', url: "/static/fonts/crimson/CrimsonPro-Italic-Variable.woff2", weight: "100 900", style: "italic" },
{ family: '"Source Serif 4"', url: "/static/fonts/source-serif/SourceSerif4-Variable.woff2", weight: "100 900", style: "normal" },
{ family: '"Source Serif 4"', url: "/static/fonts/source-serif/SourceSerif4-Italic-Variable.woff2", weight: "100 900", style: "italic" },
{ family: '"EB Garamond"', url: "/static/fonts/eb-garamond/EBGaramond-Variable.woff2", weight: "100 900", style: "normal" },
{ family: '"EB Garamond"', url: "/static/fonts/eb-garamond/EBGaramond-Italic-Variable.woff2", weight: "100 900", style: "italic" },
{ family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-Regular.woff2", weight: "400", style: "normal" },
{ family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-Bold.woff2", weight: "700", style: "normal" },
{ family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-Italic.woff2", weight: "400", style: "italic" },
{ family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-BoldItalic.woff2", weight: "700", style: "italic" },
{ family: '"Noto Serif"', url: "/static/fonts/noto-serif/NotoSerif-Variable.woff2", weight: "100 900", style: "normal" },
{ family: '"Noto Serif"', url: "/static/fonts/noto-serif/NotoSerif-Italic-Variable.woff2", weight: "100 900", style: "italic" },
{ family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-Regular.woff2", weight: "400", style: "normal" },
{ family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-Bold.woff2", weight: "700", style: "normal" },
{ family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-Italic.woff2", weight: "400", style: "italic" },
{ family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-BoldItalic.woff2", weight: "700", style: "italic" },
{ family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-Regular.woff2", weight: "400", style: "normal" },
{ family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-Bold.woff2", weight: "700", style: "normal" },
{ family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-Italic.woff2", weight: "400", style: "italic" },
{ family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-BoldItalic.woff2", weight: "700", style: "italic" },
{ family: '"Playwrite NZ Guides"', url: "/static/fonts/Playwrite_NZ_Guides/PlaywriteNZGuides-Regular.woff2", weight: "400", style: "normal" },
];
let fontBlobUrls: Record<string, string> = {};
let fontsLoaded = false;
async function loadFontBlobUrls(): Promise<void> {
if (fontsLoaded) return;
const entries = await Promise.all(
FONT_FILES.map(async (f) => {
try {
const resp = await fetch(f.url);
const blob = await resp.blob();
const blobUrl = URL.createObjectURL(blob);
return { key: f.url, blobUrl };
} catch (_e) {
return { key: f.url, blobUrl: f.url };
}
}),
);
for (const { key, blobUrl } of entries) {
fontBlobUrls[key] = blobUrl;
}
fontsLoaded = true;
}
@font-face {
font-family: "Crimson Pro";
src: url("/static/fonts/crimson/CrimsonPro-Variable.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
function buildFontFaceCSS(): string {
return FONT_FILES.map((f) => {
const src = fontBlobUrls[f.url] ?? f.url;
return `@font-face {
font-family: ${f.family};
src: url("${src}") format("woff2");
font-weight: ${f.weight};
font-style: ${f.style};
}`;
}).join("\n");
}
@font-face {
font-family: "Crimson Pro";
src: url("/static/fonts/crimson/CrimsonPro-Italic-Variable.woff2") format("woff2");
font-weight: 100 900;
font-style: italic;
}
@font-face {
font-family: "Source Serif 4";
src: url("/static/fonts/source-serif/SourceSerif4-Variable.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
}
@font-face {
font-family: "Source Serif 4";
src: url("/static/fonts/source-serif/SourceSerif4-Italic-Variable.woff2") format("woff2");
font-weight: 100 900;
font-style: italic;
}
@font-face {
font-family: "EB Garamond";
src: url("/static/fonts/eb-garamond/EBGaramond-Variable.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
}
@font-face {
font-family: "EB Garamond";
src: url("/static/fonts/eb-garamond/EBGaramond-Italic-Variable.woff2") format("woff2");
font-weight: 100 900;
font-style: italic;
}
@font-face {
font-family: "Libertinus Serif";
src: url("/static/fonts/libertinus/LibertinusSerif-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: "Libertinus Serif";
src: url("/static/fonts/libertinus/LibertinusSerif-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: "Libertinus Serif";
src: url("/static/fonts/libertinus/LibertinusSerif-Italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: "Libertinus Serif";
src: url("/static/fonts/libertinus/LibertinusSerif-BoldItalic.woff2") format("woff2");
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: "Noto Serif";
src: url("/static/fonts/noto-serif/NotoSerif-Variable.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
}
@font-face {
font-family: "Noto Serif";
src: url("/static/fonts/noto-serif/NotoSerif-Italic-Variable.woff2") format("woff2");
font-weight: 100 900;
font-style: italic;
}
@font-face {
font-family: "Charis SIL";
src: url("/static/fonts/charis-sil/CharisSIL-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: "Charis SIL";
src: url("/static/fonts/charis-sil/CharisSIL-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: "Charis SIL";
src: url("/static/fonts/charis-sil/CharisSIL-Italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: "Charis SIL";
src: url("/static/fonts/charis-sil/CharisSIL-BoldItalic.woff2") format("woff2");
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: "IBM Plex Serif";
src: url("/static/fonts/ibm-plex/IBMPlexSerif-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: "IBM Plex Serif";
src: url("/static/fonts/ibm-plex/IBMPlexSerif-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: "IBM Plex Serif";
src: url("/static/fonts/ibm-plex/IBMPlexSerif-Italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: "IBM Plex Serif";
src: url("/static/fonts/ibm-plex/IBMPlexSerif-BoldItalic.woff2") format("woff2");
font-weight: 700;
font-style: italic;
}
`;
interface ThemeColors {
fg: string;
@@ -259,7 +193,7 @@ const getCSS = ({
}` : "";
return `
${FONT_FACE_RULES}
${buildFontFaceCSS()}
@namespace epub "http://www.idpf.org/2007/ops";
html {
color-scheme: light dark;
@@ -362,6 +296,7 @@ document.addEventListener("alpine:init", () => {
const themeColors = this.readingMode === "dark" ? themeSet.dark : themeSet.light;
viewport.style.backgroundColor = themeColors.bg;
this.view = document.getElementById("reader-view") as any;
await loadFontBlobUrls();
const resp = await fetch(config.fileUrl, {
headers: { Authorization: `Bearer ${getToken()}` },
});
+8
View File
@@ -227,3 +227,11 @@
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: "Playwrite NZ Guides";
src: url("/static/fonts/Playwrite_NZ_Guides/PlaywriteNZGuides-Regular.woff2")
format("woff2");
font-weight: 400;
font-style: normal;
}