diff --git a/docs/demos/common/src/main/css/GuideRootFont.ttf b/docs/demos/common/src/main/css/GuideRootFont.ttf new file mode 100644 index 00000000000..bd39ecf6b74 Binary files /dev/null and b/docs/demos/common/src/main/css/GuideRootFont.ttf differ diff --git a/docs/demos/common/src/main/css/guide-snippets-theme.css b/docs/demos/common/src/main/css/guide-snippets-theme.css index 63c674395d1..fd01290cfb3 100644 --- a/docs/demos/common/src/main/css/guide-snippets-theme.css +++ b/docs/demos/common/src/main/css/guide-snippets-theme.css @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + /** * Compiled developer-guide CSS snippets. * Complete examples that do not require external assets or generated image borders live here. @@ -419,6 +442,34 @@ MyLabel { } /* end::css-css-029[] */ +/* A relative src URL resolves against the directory holding this CSS file, so + GuideRootFont.ttf sits beside it and GuideDemoFont-Bold.ttf sits in res/. + Both forms compile, which is the point the guide makes with this snippet. */ +/* tag::css-css-044[] */ +@font-face { + font-family: "GuideRootFont"; + src: url(GuideRootFont.ttf); +} + +@font-face { + font-family: "GuideDemoFont Bold"; + src: url(res/GuideDemoFont-Bold.ttf); +} +/* end::css-css-044[] */ + +/* Both families come from the snippet above, so this fixture also proves the + root-level font actually resolves and ships: a @font-face is only copied to + the build output when some style references it. */ +/* tag::css-css-045[] */ +Default { + font-family: "GuideRootFont"; +} + +Title { + font-family: "GuideDemoFont Bold"; +} +/* end::css-css-045[] */ + /* tag::css-css-030[] */ @font-face { font-family: "GuideDownloadedFont"; diff --git a/docs/developer-guide/css.asciidoc b/docs/developer-guide/css.asciidoc index 589da8ae5da..be293c5836d 100644 --- a/docs/developer-guide/css.asciidoc +++ b/docs/developer-guide/css.asciidoc @@ -560,7 +560,7 @@ CN1 resource files support both PNG and JPEG images, but PNG is the default. Mul === Fonts -This library supports the https://developer.mozilla.org/en/docs/Web/CSS/font[font], https://developer.mozilla.org/en/docs/Web/CSS/font-size[font-size], https://developer.mozilla.org/en/docs/Web/CSS/font-family[font-family], https://developer.mozilla.org/en/docs/Web/CSS/font-style[font-style], https://developer.mozilla.org/en/docs/Web/CSS/font-weight[font-weight], and https://developer.mozilla.org/en/docs/Web/CSS/text-decoration[text-decoration] properties, as well at the https://developer.mozilla.org/en/docs/Web/CSS/@font-face[@font-face] CSS "at" rule for including TTF/OTF fonts. +This library supports the https://developer.mozilla.org/en/docs/Web/CSS/font[font], https://developer.mozilla.org/en/docs/Web/CSS/font-size[font-size], https://developer.mozilla.org/en/docs/Web/CSS/font-family[font-family], https://developer.mozilla.org/en/docs/Web/CSS/font-style[font-style], https://developer.mozilla.org/en/docs/Web/CSS/font-weight[font-weight], and https://developer.mozilla.org/en/docs/Web/CSS/text-decoration[text-decoration] properties, as well at the https://developer.mozilla.org/en/docs/Web/CSS/@font-face[@font-face] CSS "at" rule for including TTF fonts. ==== `font-family` @@ -593,6 +593,8 @@ If you want to use a font other than the built-in fonts, you'll need to define t include::../demos/common/src/main/css/guide-snippets-theme.css[tag=css-css-028,indent=0] ---- +IMPORTANT: Only TrueType (`.ttf`) files are supported. OpenType (`.otf`) files compile without an error, but the runtime rejects any font file whose name doesn't end in `.ttf`, and the iOS build registers only `.ttf` files with the operating system. Convert an OpenType font to TrueType before referencing it. + Then you'll be able to reference the font using the specified `font-family` in any CSS element. For example: [source,css] @@ -600,6 +602,30 @@ Then you'll be able to reference the font using the specified `font-family` in a include::../demos/common/src/main/css/guide-snippets-theme.css[tag=css-css-029,indent=0] ---- +===== Where to put the font file + +A relative `src` URL is resolved against the directory that holds the CSS file. You can keep font files directly beside `theme.css`, or in any subdirectory of it, whichever you prefer: + +[source,css] +---- +include::../demos/common/src/main/css/guide-snippets-theme.css[tag=css-css-044,indent=0] +---- + +===== Family names, weights and styles + +A `font-family` name that contains spaces must be quoted, both in the `@font-face` rule and wherever you reference it. An unquoted name is parsed as a list of separate identifiers, so `font-family: GuideDemoFont Bold` registers the family as `GuideDemoFont` and collides with your regular weight. + +`font-weight` and `font-style` select between the built-in `native:` fonts, but they have no effect once `font-family` resolves to a `@font-face` rule. Declare one `@font-face` per weight and style you need, each with its own family name, as in the example above, then reference the right family from each UIID. + +To change the base font of an entire theme, set `font-family` on the special `Default` selector, then override the UIIDs that need a bold or italic face: + +[source,css] +---- +include::../demos/common/src/main/css/guide-snippets-theme.css[tag=css-css-045,indent=0] +---- + +===== Remote and GitHub-hosted fonts + The `@font-face` directive's `src` property will accept both local and remote URLs. The guide fixture below uses a local font so the demo build remains offline and repeatable; application CSS can replace the URL with an HTTPS font URL: [source,css] @@ -607,9 +633,9 @@ The `@font-face` directive's `src` property will accept both local and remote UR include::../demos/common/src/main/css/guide-snippets-theme.css[tag=css-css-030,indent=0] ---- -In this case, it will download the `myfont.ttf` file to the same directory as the CSS file. From then on it will use that locally downloaded version of the font so that it doesn't have to make a network request for each build. +In this case, it will download the `myfont.ttf` file into the build directory alongside the merged CSS file, and reuse that copy on later builds so that it doesn't have to make a network request every time. A `mvn clean` discards the cache and the next build downloads the font again. -Fonts are automatically copied to the project's "src" directory when the CSS file is compiled so that they will be distributed with the app and available at runtime. +Fonts are automatically copied next to the compiled `theme.res` when the CSS file is compiled, so that they're distributed with the app and available at runtime. The copy uses the font's file name only, which means two `@font-face` rules that point at identically named files in different directories will collide. The copy is also skipped when a file of that name is already there, so run `mvn clean` after you replace a font file with a different one of the same name. **GitHub URLs** diff --git a/maven/css-cli/src/test/java/com/codename1/designer/css/CN1CSSCLILogicTest.java b/maven/css-cli/src/test/java/com/codename1/designer/css/CN1CSSCLILogicTest.java index 3121de5477f..a4ebe7a20e1 100644 --- a/maven/css-cli/src/test/java/com/codename1/designer/css/CN1CSSCLILogicTest.java +++ b/maven/css-cli/src/test/java/com/codename1/designer/css/CN1CSSCLILogicTest.java @@ -88,4 +88,30 @@ void detectsContainmentBeyondADirectParent(@TempDir Path tempDir) throws Excepti assertFalse((Boolean) invoke("contains", sig, root, sibling), "unrelated directory"); assertFalse((Boolean) invoke("contains", sig, child, root), "containment is not symmetric"); } + + /** + * Merge mode re-anchors every relative url() at the synced copy of the CSS + * directory, so an @font-face src that names a file sitting directly in the + * CSS root is as valid as one under a fonts/ subdirectory. Absolute and + * remote URLs have to come through untouched. + */ + @Test + void prefixesRootLevelFontUrlsAlongsideNestedOnes() throws Exception { + String css = "@font-face { font-family: \"A\"; src: url(A-Regular.ttf); }\n" + + "@font-face { font-family: \"B\"; src: url('fonts/B-Regular.ttf'); }\n" + + "@font-face { font-family: \"C\"; src: url(\"https://example.com/C.ttf\"); }\n" + + "@font-face { font-family: \"D\"; src: url(/opt/fonts/D.ttf); }\n"; + + String out = (String) invoke("prefixUrls", new Class>[]{String.class, String.class}, + css, "cn1-merged-files/abc123/"); + + assertTrue(out.contains("url(\"cn1-merged-files/abc123/A-Regular.ttf\")"), + "font in the CSS root is prefixed, was: " + out); + assertTrue(out.contains("url(\"cn1-merged-files/abc123/fonts/B-Regular.ttf\")"), + "font in a subdirectory is prefixed, was: " + out); + assertTrue(out.contains("url(\"https://example.com/C.ttf\")"), + "remote URL is left alone, was: " + out); + assertTrue(out.contains("url(\"/opt/fonts/D.ttf\")"), + "absolute path is left alone, was: " + out); + } } diff --git a/maven/css-compiler/src/test/java/com/codename1/designer/css/CSSFontFaceLocationTest.java b/maven/css-compiler/src/test/java/com/codename1/designer/css/CSSFontFaceLocationTest.java new file mode 100644 index 00000000000..8a3c2201755 --- /dev/null +++ b/maven/css-compiler/src/test/java/com/codename1/designer/css/CSSFontFaceLocationTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.designer.css; + +import com.codename1.ui.EditorTTFFont; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Hashtable; + +/** + * Regression tests for where an {@code @font-face} {@code src:} URL is allowed + * to point. A relative URL resolves against the directory holding the CSS file, + * so a font sitting directly beside {@code theme.css} is just as valid as one in + * a subdirectory. The documentation used to imply a {@code fonts/} subdirectory + * was required, and these tests pin the looser contract down. + * + *
They also cover the quoted multi-word family name. An unquoted + * {@code font-family: TestFont Bold} parses as two idents and only the first is + * read back, so quoting is what keeps two weights apart.
+ */ +public class CSSFontFaceLocationTest { + + /** + * The icon font already carried by the CSSFontFaceTest sample, reused here + * so the module doesn't need a second font of its own. + */ + private static final String FIXTURE = "TestFont.ttf"; + + @BeforeAll + static void installHeadlessImplementation() throws Exception { + HeadlessTestSupport.installHeadlessImplementation(); + } + + /** + * A font file dropped straight into the CSS directory, with no + * subdirectory, has to resolve and ship. + */ + @Test + void testFontBesideThemeCssResolves() throws Exception { + Path cssDir = Files.createTempDirectory("cn1-font-root"); + Path outDir = Files.createTempDirectory("cn1-font-root-out"); + try { + copyFixture(cssDir.resolve("TestFont-Regular.ttf")); + Path cssFile = cssDir.resolve("theme.css"); + Files.write(cssFile, ("@font-face {" + + " font-family: \"TestFont\";" + + " src: url(TestFont-Regular.ttf);" + + "}" + + "Label { font-family: \"TestFont\"; font-size: 3mm; }") + .getBytes(StandardCharsets.UTF_8)); + + Hashtable themeProps = compile(cssFile, outDir.resolve("theme.res")); + + EditorTTFFont font = fontFor(themeProps, "Label.font"); + assertNotNull(font.getFontFile(), "Label.font resolved to a font file"); + assertEquals("TestFont-Regular.ttf", font.getFontFile().getName(), "Resolved font file"); + assertTrue(outDir.resolve("TestFont-Regular.ttf").toFile().exists(), + "Font deployed next to theme.res"); + } finally { + deleteTree(cssDir); + deleteTree(outDir); + } + } + + /** + * The subdirectory form keeps working, and the deployed copy is flattened + * to the bare file name because the runtime forbids a path separator in a + * true type font name. + */ + @Test + void testFontInSubdirectoryResolvesAndDeploysFlat() throws Exception { + Path cssDir = Files.createTempDirectory("cn1-font-sub"); + Path outDir = Files.createTempDirectory("cn1-font-sub-out"); + try { + Path fontsDir = cssDir.resolve("fonts"); + Files.createDirectories(fontsDir); + copyFixture(fontsDir.resolve("TestFont-Regular.ttf")); + Path cssFile = cssDir.resolve("theme.css"); + Files.write(cssFile, ("@font-face {" + + " font-family: \"TestFont\";" + + " src: url(fonts/TestFont-Regular.ttf);" + + "}" + + "Label { font-family: \"TestFont\"; font-size: 3mm; }") + .getBytes(StandardCharsets.UTF_8)); + + Hashtable themeProps = compile(cssFile, outDir.resolve("theme.res")); + + EditorTTFFont font = fontFor(themeProps, "Label.font"); + assertNotNull(font.getFontFile(), "Label.font resolved to a font file"); + assertEquals("TestFont-Regular.ttf", font.getFontFile().getName(), "Resolved font file"); + assertTrue(outDir.resolve("TestFont-Regular.ttf").toFile().exists(), + "Font deployed flat next to theme.res"); + } finally { + deleteTree(cssDir); + deleteTree(outDir); + } + } + + /** + * Two weights, two quoted family names, two distinct files. This is the + * shape the guide tells people to use, since the {@code font-weight} + * descriptor on {@code @font-face} is not consulted when a family is + * matched. + */ + @Test + void testQuotedMultiWordFamilyKeepsWeightsApart() throws Exception { + Path cssDir = Files.createTempDirectory("cn1-font-weights"); + Path outDir = Files.createTempDirectory("cn1-font-weights-out"); + try { + copyFixture(cssDir.resolve("TestFont-Regular.ttf")); + copyFixture(cssDir.resolve("TestFont-Bold.ttf")); + Path cssFile = cssDir.resolve("theme.css"); + Files.write(cssFile, ("@font-face {" + + " font-family: \"TestFont\";" + + " src: url(TestFont-Regular.ttf);" + + "}" + + "@font-face {" + + " font-family: \"TestFont Bold\";" + + " src: url(TestFont-Bold.ttf);" + + "}" + + "Label { font-family: \"TestFont\"; font-size: 3mm; }" + + "Title { font-family: \"TestFont Bold\"; font-size: 4mm; }") + .getBytes(StandardCharsets.UTF_8)); + + Hashtable themeProps = compile(cssFile, outDir.resolve("theme.res")); + + assertEquals("TestFont-Regular.ttf", fontFor(themeProps, "Label.font").getFontFile().getName(), + "Regular weight"); + assertEquals("TestFont-Bold.ttf", fontFor(themeProps, "Title.font").getFontFile().getName(), + "Bold weight resolved through the quoted family name"); + } finally { + deleteTree(cssDir); + deleteTree(outDir); + } + } + + private static Hashtable compile(Path cssFile, Path resFile) throws Exception { + CSSTheme theme = CSSTheme.load(cssFile.toUri().toURL()); + theme.resourceFile = resFile.toFile(); + theme.res = new com.codename1.ui.util.EditableResourcesForCSS(resFile.toFile()); + theme.res.setTheme("Theme", new Hashtable()); + theme.updateResources(); + return theme.res.getTheme("Theme"); + } + + private static EditorTTFFont fontFor(Hashtable themeProps, String key) { + Object font = themeProps.get(key); + assertNotNull(font, "Theme property " + key); + assertTrue(font instanceof EditorTTFFont, key + " is a true type font, was " + font.getClass()); + return (EditorTTFFont) font; + } + + /** + * Writes the shared TTF fixture out under whatever name the test needs. The + * tests only care about which file a family resolves to, not about what the + * glyphs look like, so one fixture stands in for every weight. + */ + private static void copyFixture(Path dest) throws IOException { + try (InputStream in = CSSFontFaceLocationTest.class.getResourceAsStream(FIXTURE)) { + assertNotNull(in, "Test fixture " + FIXTURE); + Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING); + } + } + + private static void deleteTree(Path path) { + File file = path.toFile(); + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteTree(child.toPath()); + } + } + file.delete(); + } + + private static void assertEquals(Object expected, Object actual, String message) { + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + " expected=" + expected + " actual=" + actual); + } + } + + private static void assertNotNull(Object actual, String message) { + if (actual == null) { + throw new AssertionError(message + " was null"); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } +} diff --git a/maven/css-compiler/src/test/resources/com/codename1/designer/css/TestFont.ttf b/maven/css-compiler/src/test/resources/com/codename1/designer/css/TestFont.ttf new file mode 100644 index 00000000000..bd39ecf6b74 Binary files /dev/null and b/maven/css-compiler/src/test/resources/com/codename1/designer/css/TestFont.ttf differ diff --git a/scripts/initializr/common/src/main/resources/skill/references/android-to-cn1.md b/scripts/initializr/common/src/main/resources/skill/references/android-to-cn1.md index 96654d3b9ed..abc6d44d881 100644 --- a/scripts/initializr/common/src/main/resources/skill/references/android-to-cn1.md +++ b/scripts/initializr/common/src/main/resources/skill/references/android-to-cn1.md @@ -129,7 +129,7 @@ The EDT/UI-thread rule is identical in spirit to Android: never touch a componen | `res/values/strings.xml` | `common/src/main/l10n/messages.properties` (and per-locale `messages_de.properties`, etc.) — see `references/build-and-run.md`. | | `res/drawable/foo.png` | `common/src/main/resources/foo.png` (flat namespace — `references/java-api-subset.md`). | | `res/values/colors.xml` | Theme constants in `theme.css` under `#Constants { ... }`. | -| `res/font/x.ttf` | `common/src/main/css/fonts/x.ttf`, declared via `@font-face` in `theme.css`. | +| `res/font/x.ttf` | Anywhere under `common/src/main/css/` (beside `theme.css` or in a subdirectory), declared via `@font-face` in `theme.css`. TrueType only — convert `.otf` files first. | | `res/raw/seed.json` | `common/src/main/resources/seed.json` — read with `Display.getInstance().getResourceAsStream("/seed.json")`. | | `res/layout/*.xml` | No equivalent — build the layout in Java (`Container` + `Layout` + components). | diff --git a/scripts/initializr/common/src/main/resources/skill/references/css.md b/scripts/initializr/common/src/main/resources/skill/references/css.md index 90786b6c9e0..e827e76ec42 100644 --- a/scripts/initializr/common/src/main/resources/skill/references/css.md +++ b/scripts/initializr/common/src/main/resources/skill/references/css.md @@ -333,7 +333,9 @@ For the full Lottie feature matrix and troubleshooting, point users to `docs/dev ### Custom TTF fonts -Drop a `.ttf` (or `.otf`) under `common/src/main/css/fonts/`, then reference its **font name (not file name)** in `font-family`: +**TrueType only.** A `.otf` compiles without an error but is rejected at runtime — `Font.createTrueTypeFont` throws unless the file name ends in `.ttf`, and the iOS build registers only `.ttf` files with the OS. Convert OpenType fonts to TrueType first. + +Drop the `.ttf` anywhere under `common/src/main/css/` — relative `src:` URLs resolve against the directory holding `theme.css`, so both `url("Inter-Regular.ttf")` beside the CSS and `url("fonts/Inter-Regular.ttf")` in a subdirectory work. Then reference its **font name (not file name)** in `font-family`: ```css @font-face { @@ -349,7 +351,9 @@ Title { font-family: "Inter Bold"; font-size: 4mm; } Body { font-family: "Inter"; font-size: 3mm; } ``` -Custom TTF/OTF files are **packaged with the app binary** (placed under the build output so the runtime can `Font.createTrueTypeFont(name, file)` them at startup) — they are **not** embedded inside `theme.res`. That means each font you add increases the deployed app size; choose lean subsets where possible. +A family name containing a space must be quoted everywhere it appears — unquoted, `font-family: Inter Bold` parses as two identifiers and registers under `Inter`, silently colliding with the regular weight. Note also that `font-weight` / `font-style` only select between the built-in `native:` fonts; once `font-family` matches a `@font-face`, they are ignored. That is why each weight needs its own family name. For a whole-theme font swap, set `font-family` on the `Default` selector, then override the UIIDs that need bold or italic. + +Custom TTF files are **packaged with the app binary** (placed under the build output so the runtime can `Font.createTrueTypeFont(name, file)` them at startup) — they are **not** embedded inside `theme.res`. That means each font you add increases the deployed app size; choose lean subsets where possible. To load a TTF programmatically: @@ -555,7 +559,8 @@ Painters are for **drawing** (custom backgrounds, decorations), not for animatin | `text-align` does nothing | Add the `align` fallback (the initializr appends one automatically for `text-align`). | | New CSS only takes effect after restart | The build cache may be stale — `mvn -pl common clean compile`. | | 9-piece border looks blurry on iPhone Pro | Expected — 9-piece images are rasterized at the bundled resolution. Use a vector border (`RoundBorder`/`RoundRectBorder`) instead. | -| Custom TTF doesn't render on device but works in simulator | The `@font-face` `src:` filename and the JS-side `Font.createTrueTypeFont(name, file)` filename must match exactly, and the file must end up packaged with the app. Re-check spelling and confirm the file is under `common/src/main/css/fonts/`. | +| Custom TTF doesn't render on device but works in simulator | The `@font-face` `src:` filename and the JS-side `Font.createTrueTypeFont(name, file)` filename must match exactly, and the file must end up packaged with the app. Re-check spelling and confirm the file is under `common/src/main/css/` (beside `theme.css` or in a subdirectory of it). | +| Custom font is ignored entirely, no error | Either the file is an `.otf` (only `.ttf` works), or the family name has an unquoted space, or `theme.css` isn't at `common/src/main/css/theme.css` — the compiler looks for a `css` directory that is a sibling of a compile source root, and silently does nothing if it isn't there. | ## Reaching beyond the compiler diff --git a/scripts/initializr/common/src/main/resources/skill/references/react-to-cn1.md b/scripts/initializr/common/src/main/resources/skill/references/react-to-cn1.md index a22514dfa5e..9bf97c74326 100644 --- a/scripts/initializr/common/src/main/resources/skill/references/react-to-cn1.md +++ b/scripts/initializr/common/src/main/resources/skill/references/react-to-cn1.md @@ -80,7 +80,8 @@ Then translate sizes. A web design is in **px**; CN1 sizes in **mm** (density independent). At the desktop/browser scale CN1 effectively renders ~3.78 px/mm, so `16px ~= 4.2mm`, `24px ~= 6.3mm`. Borders/radii in `px` stay crisp; size text and spacing in `mm`. **Bundle the real font** if you want to match the typeface: drop -the `.ttf` files under `common/src/main/css/fonts/` and reference them with +the `.ttf` files anywhere under `common/src/main/css/` (beside `theme.css` or in a +subdirectory of it) and reference them with `@font-face { font-family: "Inter"; src: url("fonts/Inter-Regular.ttf"); }` (one `@font-face` per weight, distinct family names like `"Inter SemiBold"`).