diff --git a/.gitattributes b/.gitattributes
index 0347eda..4cb8e39 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -8,6 +8,9 @@
*.md text eol=lf
*.yml text eol=lf
*.sh text eol=lf
+*.svg text eol=lf
+*.toml text eol=lf
+*.py text eol=lf
*.editorconfig text eol=lf
*.sln text eol=crlf
*.png binary
diff --git a/CLAUDE.md b/CLAUDE.md
index d656159..9a88e4b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -133,6 +133,7 @@ Before you say a task is done, run `dotnet build`, `dotnet test -f net10.0` and
- If a change contradicts an accepted ADR, stop and use `/adr` instead of working around it. Until the first stable release, accepted ADRs are changed in place with a dated note (ADR-0001); from 1.0 on, a new superseding ADR.
- Before opening a PR, run the `reviewer` agent on the diff.
- The NuGet workflows build and pack `OpenUrzednik.Packages.slnf` (the `src/` projects only). **A new `src/` project must be added to it**, or it isn't published. Every package gets the version of the root `version.json` (lockstep, ADR-0008); don't add a `version.json` to a project. A new package input outside `src/` goes into both `pathFilters` in `version.json` and the `paths` filter of `nuget-pre-release.yml` (a directory is `dir` in the first and `dir/**` in the second).
+- Package icons follow `assets/branding/BRANDING.md`: a provider package sets `OpenUrzednikPackageIcon` in its csproj, and its PNG joins both publish lists above.
- **CI job names are required status checks in the GitHub ruleset.** If you rename a job in `.github/workflows/build.yml`, `format.yml`, `dependency-review.yml` or `release-label.yml`, say so: the ruleset must be updated (see `docs/GITHUB-SETUP.md`).
- Language ([ADR-0009](docs/adr/0009-documentation-language.md)): everything is in English, user-facing docs included (`README.md` in the root and per package, `CONTRIBUTING.md`, `SECURITY.md`, the samples README). English is the single source of truth: if a Polish text differs, the English one wins. `README.pl.md` next to the root README and each published package's README has only the informational parts: what the package is and the "unofficial" notice, installation, the quick-start example (the same code) and links to the English sections. Contract and reference content (API limits, data availability, errors, telemetry, configuration rules, dependencies, versioning, support) is English only. `SECURITY.pl.md` says how to report a vulnerability. Skeleton READMEs are English only; issue forms are bilingual inline. When a PR changes an informational part of a README, it updates `README.pl.md` too, and renaming or removing a section of an English README updates the links to it in `README.pl.md`.
- Prose style: when you write or edit user-facing prose (READMEs, `CONTRIBUTING`, `SECURITY`), run `/stop-slop` on the English files and `/miodkuj` on the Polish ones (`*.pl.md`). Edit only the prose: keep code blocks, commands, package and API names, badges, links and numbers exactly as they are. Meaning comes before style: keep API limits and qualifiers ("only", "never", "at most") even where a style rule says to cut them (stop-slop removes absolutes and adverbs).
diff --git a/Directory.Build.props b/Directory.Build.props
index c499b46..9850683 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -7,8 +7,9 @@
true
true
- milten89
- OpenUrzednik Open Source
+ Andrzej Ochodek
+ Andrzej Ochodek
+ Copyright (c) 2026 Andrzej Ochodek
Modern, high-performance .NET SDK for Polish government public APIs.
MIT
https://github.com/milten89/OpenUrzednik.NET
@@ -27,6 +28,9 @@
$(WarningsNotAsErrors);NU1900;NU1901;NU1902;NU1903;NU1904;NU1905
README.md
+ icon.png
+
+ icon.png
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 8e7aced..f17ee51 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,9 @@
-# OpenUrzednik.NET 💼🇵🇱
+
+
+
+
+
+
🇵🇱 [Wersja polska (skrócona)](README.pl.md)
diff --git a/README.pl.md b/README.pl.md
index 5754632..7caeb57 100644
--- a/README.pl.md
+++ b/README.pl.md
@@ -1,4 +1,9 @@
-# OpenUrzednik.NET 💼🇵🇱
+
+
+
+
+
+
🇬🇧 Pełna dokumentacja jest po angielsku: [README.md](README.md). Ta wersja zawiera tylko część informacyjną. Jeśli wersje się różnią, obowiązuje angielska.
diff --git a/assets/branding/BRANDING.md b/assets/branding/BRANDING.md
new file mode 100644
index 0000000..7bd9bb5
--- /dev/null
+++ b/assets/branding/BRANDING.md
@@ -0,0 +1,161 @@
+# OpenUrzędnik.NET branding
+
+The spec for the project's icons and logo. `branding.toml` holds every number, color, label and tagline, and `generate.py` builds every asset from it. This file explains the design; when a number here and in `branding.toml` disagree, `branding.toml` wins.
+
+## Idea
+
+The icon is a **queue ticket** (*numerek*), the slip you pull when you enter a Polish government office. The number on it is `200`, the HTTP status for success. It's a small joke: with this library you're served on the first call. The provider icons put the provider's short name where the number is, so the NuGet listing shows which office you are queuing at.
+
+The design is flat on purpose: no gradients, shadows or outlines. It has to read at 64 px on nuget.org.
+
+**Never use the Polish state emblem (the white eagle) or anything resembling it.** Polish law restricts its use, and it would suggest an official project. This project is unofficial.
+
+## Files
+
+| File | Content | Used by |
+|---|---|---|
+| `icon.svg`, `icon.png` | Base ticket: `OU.NET` / `200` | NuGet icon of Core, Extensions.Logging, Diagnostics |
+| `icon-.svg`, `icon-.png` | Base ticket with the provider name in place of `200` | NuGet icon of the provider and its integration packages (`icon-nbp` → `Nbp`, `Nbp.DependencyInjection`) |
+| `favicon.svg`, `favicon-32.png` | Ticket without text | Anything at 32 px or less |
+| `header-light.svg`, `header-dark.svg` | Ticket + wordmark | Root `README.md` / `README.pl.md` |
+| `social-preview.svg`, `social-preview.png` | 1280×640 card, Polish tagline | GitHub repository social preview (uploaded by hand) |
+| `social-preview-en.svg`, `social-preview-en.png` | 1280×640 card, English tagline | The same, for an English-facing upload |
+| `branding.toml` | Every number, color, label and tagline | `generate.py` |
+| `generate.py` | Generator script | Builds every SVG and PNG above |
+
+`branding.toml` is the source: `generate.py` writes each SVG from it and renders each PNG from its SVG. Text in every SVG is converted to outlines (paths), so the files never depend on installed fonts.
+
+### Package → icon
+
+| Package | Icon |
+|---|---|
+| `OpenUrzednik.Core`, `.Extensions.Logging`, `.Diagnostics` | `icon.png` |
+| `OpenUrzednik.Nbp`, `OpenUrzednik.Nbp.DependencyInjection` | `icon-nbp.png` |
+
+A provider gets its icon when it becomes ready (ADR-0010), by adding its entry to `branding.toml`.
+
+Rule: shared infrastructure gets the base icon. A provider package and every package built on it (`*.DependencyInjection`, future adapters) get that provider's icon. The root `Directory.Build.props` packs the file named by the `OpenUrzednikPackageIcon` property as `icon.png` (default `icon.png`). A provider csproj and each package built on it set `icon-.png `. When you add a provider icon, also add its PNG to `pathFilters` in `version.json` and to the `paths` filter of `.github/workflows/nuget-pre-release.yml` (ADR-0008).
+
+## Colors
+
+| Token | Hex | Where |
+|---|---|---|
+| Ticket purple | `#512BD4` | Ticket body, `.NET` in the light wordmark. The .NET brand purple |
+| Polish red | `#C8102E` | Strip at the top of the ticket, stripe on the social preview |
+| Lilac | `#D9D2FA` | `OU.NET` on the ticket |
+| White | `#FFFFFF` | `200` / provider name, perforation |
+| Ink | `#1F1F1F` | Wordmark on light backgrounds |
+| Grey | `#5F5E5A` | Tagline on the social preview |
+| Paper | `#FAFAF7` | Social preview background |
+| Dark ink | `#E6EDF3` | Wordmark on dark backgrounds (GitHub dark text color) |
+| Dark purple | `#9B87F5` | `.NET` on dark backgrounds (`#512BD4` is too dark on `#0D1117`) |
+
+The ticket itself keeps the same colors in light and dark mode.
+
+## Typography
+
+One family everywhere: **Cascadia Mono** (Microsoft, SIL Open Font License 1.1, ). It's a monospace font from the .NET world, and it has Polish glyphs (`ę` in the wordmark). The current files were made from the variable font `CascadiaMono.ttf` 2102.025, instanced at the weight shown below.
+
+| Text | Weight | Size (in its own coordinate system) |
+|---|---|---|
+| `OU.NET` on the ticket | 700 | 15 |
+| `200` / provider name | 700 | 30 (see label rules) |
+| Wordmark `OpenUrzędnik.NET` | 700 | 40 (header), 72 (social preview) |
+| Tagline | 400 | 30 (social preview) |
+
+Metrics: 2048 units per em, advance width 1200 for every glyph (0.586 em), cap height 1420 (0.693 em). Text has no letter-spacing and no kerning.
+
+The wordmark is written **`OpenUrzędnik.NET`**, with `ę`. Package IDs and code stay ASCII (`OpenUrzednik`). In the wordmark, `OpenUrzędnik` uses the ink color and `.NET` the purple, with no space between them.
+
+## Icon geometry
+
+Coordinate system: `viewBox="0 0 120 120"`, exported at 128×128. Every number below is in viewBox units.
+
+```
+ x: 22 98
+y14 ┌──────────────────────┐ red strip y 14–20 (#C8102E)
+ ├──────────────────────┤
+ │ OU.NET │ baseline 44, size 15, centered on x 60 (#D9D2FA)
+y50 │ │
+ ( - - - - - - - - - - ) notches: r 8, centers (22,58) and (98,58)
+y66 │ │ perforation y 58, x 32–88, 2 wide, dash 4 gap 4 (#FFF)
+ │ 200 │ baseline 94, size 30, centered on x 60 (#FFF)
+y106└──────────────────────┘
+```
+
+Ticket outline (fill `#512BD4`):
+
+```
+M22 14 H98 V50 A8 8 0 0 0 98 66 V106 H22 V66 A8 8 0 0 0 22 50 Z
+```
+
+- Ticket: 76 wide, 92 tall, square corners. The two semicircular notches (radius 8) cut into the sides at mid-height (y 50–66).
+- Red strip: `rect x=22 y=14 width=76 height=6`, drawn over the ticket.
+- Perforation: line from (32, 58) to (88, 58), white, stroke width 2, `stroke-dasharray="4 4"`, which gives 7 dashes.
+- Text is centered horizontally on x = 60 by its advance width: `x_start = 60 − (characters × 0.586 × size) / 2`. Monospace, so the advance width equals the box.
+- Margins: 22 left and right, 14 top and bottom. Keep them, don't scale the ticket up to the edge. NuGet and GitHub crop or round corners.
+
+### Label rules (bottom half)
+
+The bottom label must stay **at most 56 units wide**, which leaves at least 10 units to each side of the ticket.
+
+| Characters | Size | Width |
+|---|---|---|
+| 1–3 (`200`, `NBP`, `MF`) | 30 | ≤ 52.7 |
+| 4 (e.g. `KSeF`) | 23.5 | 55.1 |
+| 5 (e.g. `CEIDG`) | 19 | 55.7 |
+
+Formula: `size = min(30, 56 / (characters × 0.586))`, rounded down to 0.5. The baseline stays at 94 for every size. Uppercase is the default. Use the institution's own casing when it has one (`KSeF`). No more than 5 characters: use the institution's common abbreviation, never a shortened word.
+
+### Favicon (≤ 32 px)
+
+At 32 px and below, text turns to noise, so the favicon is just the shape. It uses a slightly bigger ticket and a thicker strip so it stays readable:
+
+```
+viewBox 0 0 120 120
+ticket M14 10 H106 V48 A12 12 0 0 0 106 72 V110 H14 V72 A12 12 0 0 0 14 48 Z #512BD4
+strip rect x=14 y=10 width=92 height=16 #C8102E
+```
+
+No perforation, no text. Every package and provider shares the same favicon.
+
+## README header
+
+- Canvas height 96. Width = 104 + wordmark width + 4, rounded up to a whole unit (483 for the current wordmark).
+- Ticket: the full icon group scaled by 0.8 at the origin, so it is about 61×74 inside x 17.6–78.4, y 11.2–84.8.
+- Wordmark: starts at x 104, baseline 62, size 40, weight 700.
+- No background (transparent). Two files: `header-light.svg` (ink `#1F1F1F`, `.NET` `#512BD4`) and `header-dark.svg` (ink `#E6EDF3`, `.NET` `#9B87F5`), switched with `` and `prefers-color-scheme`.
+- No tagline: the header is shared by the Polish and the English README.
+
+## Social preview (1280×640)
+
+- Background `#FAFAF7`. A white band (y 0–16) and a red band (y 16–32) at the top: the Polish flag as a thin stripe.
+- Ticket: the base icon group, `translate(64 176) scale(2.4)` (ticket body at x 116.8–299.2, y 209.6–430.4).
+- Wordmark: x 392, baseline 322, size 72, weight 700, ink + purple.
+- Tagline: `Klienci .NET do polskich API publicznych`, x 392, baseline 392, size 30, weight 400, `#5F5E5A`.
+- Two cards, identical except for the tagline: `social-preview` (Polish, above) and `social-preview-en` (`.NET clients for Polish public APIs`, for an English-facing upload). The maintainer chooses which one to upload.
+- GitHub crops the edges in some views: keep content at least 40 px from every edge.
+
+## Adding a provider icon
+
+1. Add an entry to `[icons]` in `branding.toml`: `file = "icon-"` (lowercase) and `label`, following the label rules. The script sizes and centers the label and sets `` to `OpenUrzędnik.NET `.
+2. Run `uv run assets/branding/generate.py`. It writes `icon-.svg` and renders `icon-.png` at 128×128.
+3. Check it at 128, 64 and 32 px next to `icon.png`: the label must not touch the notches.
+4. Wire it as described in "Package → icon": `OpenUrzednikPackageIcon` in the csproj, the PNG in `pathFilters` in `version.json` and in the `paths` filter of `nuget-pre-release.yml`, and the row in the table there.
+
+## Generating the assets
+
+```bash
+uv run assets/branding/generate.py # write every SVG and PNG listed in branding.toml
+uv run assets/branding/generate.py --check # generate into a temporary directory; exit 1 and list the files that differ
+```
+
+The script is a uv script with pinned dependencies, locked with hashes in `generate.py.lock`: `fonttools` outlines the text and `resvg-py` (resvg) renders the PNGs. It downloads the font archive named in `branding.toml` once into a cache outside the repository (`%LOCALAPPDATA%\openurzednik-branding` on Windows, `$XDG_CACHE_HOME/openurzednik-branding` or `~/.cache/openurzednik-branding` elsewhere), checks its SHA-256, and fails if the font version differs from the one pinned there. Output is deterministic: running it twice gives the same bytes, so `--check` passes on an unchanged tree. Render sizes: icons 128, favicon 32, social preview 1280. SVGs are UTF-8 with LF line endings (`` contains `ę`).
+
+## Don'ts
+
+- No gradients, shadows, glows, rounded ticket corners or outlines around the ticket.
+- Don't recolor the ticket per provider. The name is the only difference.
+- Don't put live `` in an SVG. Outline it.
+- Don't add the state emblem, the flag as a shape, or the logos of the institutions (NBP, GUS, …). Their names as plain text are fine.
+- Don't change `200` on the base icon or `OU.NET` on any icon.
diff --git a/assets/branding/branding.toml b/assets/branding/branding.toml
new file mode 100644
index 0000000..2530174
--- /dev/null
+++ b/assets/branding/branding.toml
@@ -0,0 +1,148 @@
+# The single source of truth for the OpenUrzędnik.NET branding assets.
+# Edit this file, then run: uv run assets/branding/generate.py
+# BRANDING.md explains the design; the numbers live here.
+# All geometry is in SVG user units (the viewBox of the file it ends up in).
+
+# Name shown in every and drawn as the wordmark.
+[brand]
+name = "OpenUrzędnik.NET"
+# The wordmark is drawn in two parts: the first in the ink color, the second in the accent color.
+wordmark = ["OpenUrzędnik", ".NET"]
+
+# Cascadia Mono (SIL OFL 1.1). The archive is downloaded once into a cache outside the
+# repository and checked against the SHA-256 below; `version` must match the font's own version.
+[font]
+url = "https://github.com/microsoft/cascadia-code/releases/download/v2102.25/CascadiaCode-2102.25.zip"
+sha256 = "a64e8834f06ec9acca4a5307cb6cf35922021d4fd0ca19096f8bf6a3e2515ad3"
+member = "ttf/CascadiaMono.ttf"
+version = "2102.025"
+# Weights the variable font is instanced at.
+bold = 700
+regular = 400
+
+# Color tokens, referenced by name below.
+[colors]
+purple = "#512BD4" # ticket body, .NET in the light wordmark
+red = "#C8102E" # strip at the top of the ticket, stripe on the social preview
+lilac = "#D9D2FA" # OU.NET on the ticket
+white = "#FFFFFF" # label, perforation
+ink = "#1F1F1F" # wordmark on light backgrounds
+grey = "#5F5E5A" # tagline on the social preview
+paper = "#FAFAF7" # social preview background
+dark_ink = "#E6EDF3" # wordmark on dark backgrounds
+dark_purple = "#9B87F5" # .NET on dark backgrounds
+
+# The queue ticket shared by every icon, the headers and the social preview.
+# viewBox 0 0 120 120. The notches are semicircles cut into both sides, centered on notch_y.
+[ticket]
+viewbox = 120
+x = 22
+y = 14
+width = 76
+height = 92
+notch_radius = 8
+notch_y = 58
+fill = "purple"
+strip_height = 6
+strip_fill = "red"
+# Perforation: a dashed line along notch_y.
+perforation_x1 = 32
+perforation_x2 = 88
+perforation_width = 2
+perforation_dash = "4 4"
+perforation_stroke = "white"
+
+# "OU.NET" in the top half of every ticket, centered on center_x.
+[ticket.top_text]
+text = "OU.NET"
+center_x = 60
+baseline = 44
+size = 15
+color = "lilac"
+
+# The label in the bottom half ("200" or the provider name), centered on center_x.
+# size = min(max_size, max_width / text width at size 1), rounded down to size_step.
+[ticket.label]
+center_x = 60
+baseline = 94
+max_size = 30
+max_width = 56
+size_step = 0.5
+max_chars = 5
+color = "white"
+
+# Icons: one SVG and one PNG (render_size × render_size) per entry, file name without extension.
+# label_in_title = false keeps the label out of the (the base icon is just the brand).
+# base = true marks the one icon whose ticket the headers and social cards reuse (exactly one).
+[icons]
+render_size = 128
+
+[[icons.icon]]
+file = "icon"
+label = "200"
+label_in_title = false
+base = true
+
+[[icons.icon]]
+file = "icon-nbp"
+label = "NBP"
+
+# Favicon: a bigger ticket with a thicker strip, no perforation, no text. SVG + PNG.
+[favicon]
+file = "favicon"
+png = "favicon-32"
+render_size = 32
+x = 14
+y = 10
+width = 92
+height = 100
+notch_radius = 12
+notch_y = 60
+strip_height = 16
+
+# README headers: the base icon scaled by icon_scale at the origin, then the wordmark.
+# Width = wordmark_x + wordmark width + padding_right, rounded up to a whole unit. SVG only.
+[header]
+height = 96
+icon_scale = 0.8
+wordmark_x = 104
+wordmark_baseline = 62
+wordmark_size = 40
+padding_right = 4
+
+[[header.variant]]
+file = "header-light"
+ink = "ink"
+accent = "purple"
+
+[[header.variant]]
+file = "header-dark"
+ink = "dark_ink"
+accent = "dark_purple"
+
+# Social preview cards (1280×640): flag stripe, base icon, wordmark and a tagline. SVG + PNG.
+# The maintainer uploads one of them to GitHub by hand.
+[social]
+width = 1280
+height = 640
+background = "paper"
+# Bands at the top, drawn in order: [y, height, color].
+bands = [[0, 16, "white"], [16, 16, "red"]]
+icon_translate = [64, 176]
+icon_scale = 2.4
+text_x = 392
+wordmark_baseline = 322
+wordmark_size = 72
+wordmark_ink = "ink"
+wordmark_accent = "purple"
+tagline_baseline = 392
+tagline_size = 30
+tagline_color = "grey"
+
+[[social.card]]
+file = "social-preview"
+tagline = "Klienci .NET do polskich API publicznych"
+
+[[social.card]]
+file = "social-preview-en"
+tagline = ".NET clients for Polish public APIs"
diff --git a/assets/branding/favicon-32.png b/assets/branding/favicon-32.png
new file mode 100644
index 0000000..9ad7434
Binary files /dev/null and b/assets/branding/favicon-32.png differ
diff --git a/assets/branding/favicon.svg b/assets/branding/favicon.svg
new file mode 100644
index 0000000..0745136
--- /dev/null
+++ b/assets/branding/favicon.svg
@@ -0,0 +1 @@
+OpenUrzędnik.NET
diff --git a/assets/branding/generate.py b/assets/branding/generate.py
new file mode 100644
index 0000000..adc6c70
--- /dev/null
+++ b/assets/branding/generate.py
@@ -0,0 +1,444 @@
+# /// script
+# requires-python = ">=3.12"
+# dependencies = [
+# "fonttools==4.67.0",
+# "resvg-py==0.5.0",
+# ]
+#
+# [tool.uv]
+# exclude-newer = "2026-10-30T00:00:00Z"
+# ///
+"""Generate the OpenUrzędnik.NET branding assets from branding.toml.
+
+ uv run assets/branding/generate.py # write every SVG and PNG
+ uv run assets/branding/generate.py --check # exit 1 if a working-tree file differs
+
+The font archive is downloaded once into a cache outside the repository
+(%LOCALAPPDATA%\\openurzednik-branding on Windows, otherwise
+$XDG_CACHE_HOME/openurzednik-branding or ~/.cache/openurzednik-branding)
+and verified against the SHA-256 pinned in branding.toml.
+"""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import io
+import math
+import os
+import sys
+import tempfile
+import tomllib
+import urllib.request
+import zipfile
+from collections import Counter
+from pathlib import Path
+from xml.sax.saxutils import escape
+
+import resvg_py
+from fontTools.pens.svgPathPen import SVGPathPen
+from fontTools.pens.transformPen import TransformPen
+from fontTools.ttLib import TTFont
+from fontTools.varLib.instancer import instantiateVariableFont
+
+HERE = Path(__file__).resolve().parent
+CONFIG = HERE / "branding.toml"
+SVG_NS = "http://www.w3.org/2000/svg"
+
+
+def num(value: float) -> str:
+ """Format a number with at most two decimals and no trailing zeros."""
+ text = f"{value:.2f}".rstrip("0").rstrip(".")
+ return "0" if text == "-0" else text
+
+
+# --- font -------------------------------------------------------------------
+
+
+def cache_dir() -> Path:
+ if os.name == "nt" and os.environ.get("LOCALAPPDATA"):
+ base = Path(os.environ["LOCALAPPDATA"])
+ else:
+ base = Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache")
+ return base / "openurzednik-branding"
+
+
+def sha256(path: Path) -> str:
+ return hashlib.sha256(path.read_bytes()).hexdigest()
+
+
+def font_archive(cfg: dict) -> Path:
+ """Return the cached font archive, downloading it when missing or corrupt."""
+ directory = cache_dir()
+ archive = directory / cfg["url"].rsplit("/", 1)[-1]
+ expected = cfg["sha256"].lower()
+ if archive.exists() and sha256(archive) == expected:
+ return archive
+ directory.mkdir(parents=True, exist_ok=True)
+ print(f"Downloading {cfg['url']}", file=sys.stderr)
+ partial = archive.with_name(archive.name + ".part")
+ request = urllib.request.Request(
+ cfg["url"], headers={"User-Agent": "openurzednik-branding"}
+ )
+ try:
+ with (
+ urllib.request.urlopen(request, timeout=120) as response,
+ partial.open("wb") as out,
+ ):
+ while chunk := response.read(1 << 20):
+ out.write(chunk)
+ actual = sha256(partial)
+ if actual != expected:
+ sys.exit(
+ f"SHA-256 mismatch for {cfg['url']}: expected {expected}, got {actual}"
+ )
+ partial.replace(archive)
+ finally:
+ partial.unlink(missing_ok=True)
+ return archive
+
+
+class Font:
+ """One instance of the variable font, able to turn text into SVG path data."""
+
+ def __init__(self, data: bytes, weight: int) -> None:
+ self.ttf = instantiateVariableFont(TTFont(io.BytesIO(data)), {"wght": weight})
+ self.glyphs = self.ttf.getGlyphSet()
+ self.cmap = self.ttf.getBestCmap()
+ self.upm = self.ttf["head"].unitsPerEm
+
+ def _glyph(self, char: str):
+ if ord(char) not in self.cmap:
+ sys.exit(f"The font has no glyph for {char!r}")
+ return self.glyphs[self.cmap[ord(char)]]
+
+ def width(self, text: str, size: float) -> float:
+ """Advance width of the text at the given size (no kerning, no letter-spacing)."""
+ return sum(self._glyph(c).width for c in text) * size / self.upm
+
+ def path(
+ self, text: str, x: float, baseline: float, size: float
+ ) -> tuple[str, float]:
+ """Outline the text starting at x; return the path data and the x after it."""
+ k = size / self.upm
+ pen = SVGPathPen(self.glyphs, ntos=num)
+ for char in text:
+ glyph = self._glyph(char)
+ glyph.draw(
+ TransformPen(pen, (k, 0, 0, -k, x, baseline))
+ ) # font units are y-up
+ x += glyph.width * k
+ return pen.getCommands(), x
+
+
+def load_fonts(cfg: dict) -> dict[int, Font]:
+ with zipfile.ZipFile(font_archive(cfg)) as archive:
+ data = archive.read(cfg["member"])
+ fonts = {weight: Font(data, weight) for weight in (cfg["bold"], cfg["regular"])}
+ version = f"{fonts[cfg['bold']].ttf['head'].fontRevision:.3f}"
+ if version != cfg["version"]:
+ sys.exit(
+ f"Font version {version} does not match branding.toml ({cfg['version']})"
+ )
+ return fonts
+
+
+# --- SVG building -----------------------------------------------------------
+
+
+def element(name: str, **attrs: str | float) -> str:
+ """A self-closing element; attributes keep the order given (trailing _ and _ → -)."""
+ parts = [name]
+ for key, value in attrs.items():
+ key = key.rstrip("_").replace("_", "-")
+ parts.append(
+ f'{key}="{num(value) if isinstance(value, (int, float)) else value}"'
+ )
+ return "<" + " ".join(parts) + "/>"
+
+
+def rect(x: float, y: float, width: float, height: float, fill: str) -> str:
+ attrs: dict[str, str | float] = {}
+ if x:
+ attrs["x"] = x
+ if y:
+ attrs["y"] = y
+ return element("rect", **attrs, width=width, height=height, fill=fill)
+
+
+def svg(width: float, height: float, viewbox: str, title: str, body: list[str]) -> str:
+ head = (
+ f'{escape(title)} '
+ )
+ return head + "".join(body) + " \n"
+
+
+def ticket_outline(
+ x: float, y: float, width: float, height: float, r: float, notch_y: float
+) -> str:
+ right, bottom, top_notch, bottom_notch = (
+ x + width,
+ y + height,
+ notch_y - r,
+ notch_y + r,
+ )
+ a = f"A{num(r)} {num(r)} 0 0 0"
+ return (
+ f"M{num(x)} {num(y)}H{num(right)}V{num(top_notch)}{a} {num(right)} {num(bottom_notch)}"
+ f"V{num(bottom)}H{num(x)}V{num(bottom_notch)}{a} {num(x)} {num(top_notch)}Z"
+ )
+
+
+class Generator:
+ def __init__(self, config: dict, fonts: dict[int, Font]) -> None:
+ self.c = config
+ self.bold = fonts[config["font"]["bold"]]
+ self.regular = fonts[config["font"]["regular"]]
+
+ def color(self, token: str) -> str:
+ return self.c["colors"][token]
+
+ def label_size(self, label: str) -> float:
+ rule = self.c["ticket"]["label"]
+ if not 1 <= len(label) <= rule["max_chars"]:
+ sys.exit(f"Label {label!r} must have 1 to {rule['max_chars']} characters")
+ size = min(rule["max_size"], rule["max_width"] / self.bold.width(label, 1))
+ return math.floor(size / rule["size_step"]) * rule["size_step"]
+
+ def centered(
+ self, font: Font, text: str, center_x: float, baseline: float, size: float
+ ) -> str:
+ return font.path(text, center_x - font.width(text, size) / 2, baseline, size)[0]
+
+ def ticket(self, label: str) -> list[str]:
+ t, top, lab = (
+ self.c["ticket"],
+ self.c["ticket"]["top_text"],
+ self.c["ticket"]["label"],
+ )
+ outline = ticket_outline(
+ t["x"], t["y"], t["width"], t["height"], t["notch_radius"], t["notch_y"]
+ )
+ perforation = f"M{num(t['perforation_x1'])} {num(t['notch_y'])}H{num(t['perforation_x2'])}"
+ top_path = self.centered(
+ self.bold, top["text"], top["center_x"], top["baseline"], top["size"]
+ )
+ label_path = self.centered(
+ self.bold, label, lab["center_x"], lab["baseline"], self.label_size(label)
+ )
+ return [
+ element("path", fill=self.color(t["fill"]), d=outline),
+ rect(
+ t["x"],
+ t["y"],
+ t["width"],
+ t["strip_height"],
+ self.color(t["strip_fill"]),
+ ),
+ element(
+ "path",
+ stroke=self.color(t["perforation_stroke"]),
+ stroke_width=t["perforation_width"],
+ stroke_dasharray=t["perforation_dash"],
+ d=perforation,
+ ),
+ element("path", fill=self.color(top["color"]), d=top_path),
+ element("path", fill=self.color(lab["color"]), d=label_path),
+ ]
+
+ def base_ticket(self) -> list[str]:
+ """The ticket of the icon marked base = true, reused by headers and cards."""
+ base = next(icon for icon in self.c["icons"]["icon"] if icon.get("base"))
+ return self.ticket(base["label"])
+
+ def wordmark(
+ self, x: float, baseline: float, size: float, ink: str, accent: str
+ ) -> tuple[list[str], float]:
+ body = []
+ for part, token in zip(self.c["brand"]["wordmark"], (ink, accent), strict=True):
+ d, x = self.bold.path(part, x, baseline, size)
+ body.append(element("path", fill=self.color(token), d=d))
+ return body, x
+
+ def files(self) -> dict[str, tuple[str, tuple[str, int] | None]]:
+ """Map each SVG file name (no extension) to its content and its PNG (name, width), if any."""
+ c, name = self.c, self.c["brand"]["name"]
+ box = num(c["ticket"]["viewbox"])
+ viewbox = f"0 0 {box} {box}"
+ out: dict[str, tuple[str, tuple[str, int] | None]] = {}
+
+ size = c["icons"]["render_size"]
+ for icon in c["icons"]["icon"]:
+ title = (
+ f"{name} {icon['label']}" if icon.get("label_in_title", True) else name
+ )
+ out[icon["file"]] = (
+ svg(size, size, viewbox, title, self.ticket(icon["label"])),
+ (icon["file"], size),
+ )
+
+ f, t = c["favicon"], c["ticket"]
+ favicon = [
+ element(
+ "path",
+ fill=self.color(t["fill"]),
+ d=ticket_outline(
+ f["x"],
+ f["y"],
+ f["width"],
+ f["height"],
+ f["notch_radius"],
+ f["notch_y"],
+ ),
+ ),
+ rect(
+ f["x"],
+ f["y"],
+ f["width"],
+ f["strip_height"],
+ self.color(t["strip_fill"]),
+ ),
+ ]
+ out[f["file"]] = (
+ svg(f["render_size"], f["render_size"], viewbox, name, favicon),
+ (f["png"], f["render_size"]),
+ )
+
+ h = c["header"]
+ group = (
+ f''
+ + "".join(self.base_ticket())
+ + " "
+ )
+ for variant in h["variant"]:
+ words, end = self.wordmark(
+ h["wordmark_x"],
+ h["wordmark_baseline"],
+ h["wordmark_size"],
+ variant["ink"],
+ variant["accent"],
+ )
+ width = math.ceil(round(end + h["padding_right"], 6))
+ body = [group, *words]
+ out[variant["file"]] = (
+ svg(width, h["height"], f"0 0 {width} {num(h['height'])}", name, body),
+ None,
+ )
+
+ s = c["social"]
+ tx, ty = s["icon_translate"]
+ group = (
+ f''
+ + "".join(self.base_ticket())
+ + " "
+ )
+ words, _ = self.wordmark(
+ s["text_x"],
+ s["wordmark_baseline"],
+ s["wordmark_size"],
+ s["wordmark_ink"],
+ s["wordmark_accent"],
+ )
+ for card in s["card"]:
+ tagline, _ = self.regular.path(
+ card["tagline"], s["text_x"], s["tagline_baseline"], s["tagline_size"]
+ )
+ body = [
+ rect(0, 0, s["width"], s["height"], self.color(s["background"])),
+ *(
+ rect(0, y, s["width"], height, self.color(token))
+ for y, height, token in s["bands"]
+ ),
+ group,
+ *words,
+ element("path", fill=self.color(s["tagline_color"]), d=tagline),
+ ]
+ viewbox_s = f"0 0 {num(s['width'])} {num(s['height'])}"
+ out[card["file"]] = (
+ svg(s["width"], s["height"], viewbox_s, name, body),
+ (card["file"], s["width"]),
+ )
+ return out
+
+
+def render_png(svg_text: str, width: int) -> bytes:
+ return bytes(resvg_py.svg_to_bytes(svg_string=svg_text, width=width))
+
+
+def validate(config: dict) -> None:
+ """Fail on definitions that can't produce a consistent set of files."""
+ icons = config["icons"]["icon"]
+ bases = [icon["file"] for icon in icons if icon.get("base")]
+ if len(bases) != 1:
+ sys.exit(f"Exactly one icon must have base = true, found {len(bases)}: {bases}")
+ favicon = config["favicon"]
+ names = [
+ *(f"{icon['file']}.{ext}" for icon in icons for ext in ("svg", "png")),
+ f"{favicon['file']}.svg",
+ f"{favicon['png']}.png",
+ *(f"{variant['file']}.svg" for variant in config["header"]["variant"]),
+ *(
+ f"{card['file']}.{e}"
+ for card in config["social"]["card"]
+ for e in ("svg", "png")
+ ),
+ ]
+ counts = Counter(name.lower() for name in names)
+ duplicates = sorted(name for name, count in counts.items() if count > 1)
+ if duplicates:
+ sys.exit(
+ f"Duplicate output file names in branding.toml: {', '.join(duplicates)}"
+ )
+
+
+def build(directory: Path) -> dict[str, bool]:
+ """Write every asset into directory; map each file name to whether its bytes changed."""
+ with CONFIG.open("rb") as stream:
+ config = tomllib.load(stream)
+ validate(config)
+ generator = Generator(config, load_fonts(config["font"]))
+ result = {}
+ for stem, (text, png) in generator.files().items():
+ outputs = {f"{stem}.svg": text.encode("utf-8")}
+ if png is not None:
+ outputs[f"{png[0]}.png"] = render_png(text, png[1])
+ for file_name, data in outputs.items():
+ target = directory / file_name
+ result[file_name] = not target.exists() or target.read_bytes() != data
+ if result[file_name]:
+ target.write_bytes(data)
+ return result
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
+ parser.add_argument(
+ "--check",
+ action="store_true",
+ help="generate into a temporary directory; fail if a working-tree file differs",
+ )
+ args = parser.parse_args()
+ if not args.check:
+ for file_name, changed in build(HERE).items():
+ print(f"{'wrote ' if changed else 'unchanged'} {file_name}")
+ return 0
+ with tempfile.TemporaryDirectory() as temp:
+ names = list(build(Path(temp)))
+ changed = [
+ n
+ for n in names
+ if not (HERE / n).exists()
+ or (HERE / n).read_bytes() != (Path(temp) / n).read_bytes()
+ ]
+ for file_name in changed:
+ print(f"differs: {file_name}")
+ generated = {p.name for p in HERE.iterdir() if p.suffix in (".svg", ".png")}
+ for file_name in sorted(generated - set(names)):
+ print(f"warning: no entry in branding.toml produces {file_name}")
+ print(f"{len(names) - len(changed)} of {len(names)} files up to date")
+ return 1 if changed else 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/assets/branding/generate.py.lock b/assets/branding/generate.py.lock
new file mode 100644
index 0000000..1e4dec4
--- /dev/null
+++ b/assets/branding/generate.py.lock
@@ -0,0 +1,124 @@
+version = 1
+revision = 5
+requires-python = ">=3.12"
+
+[options]
+exclude-newer = "2026-10-30T00:00:00Z"
+
+[manifest]
+requirements = [
+ { name = "fonttools", specifier = "==4.67.0" },
+ { name = "resvg-py", specifier = "==0.5.0" },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.67.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/36/102e180f8f5dbaee88b26595b01ca8aa80bf4e62128d9aa94265b3996c96/fonttools-4.67.0.tar.gz", hash = "sha256:3cb57e6600ca77c0b1729cf8adc23bc0652633a37f18cfa934d9c7bc3de25519", size = 3750028, upload-time = "2026-10-14T13:20:28.294Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/50/f674402869f11a89868c4755ae86cd2fcfd67ca6193c6f5d1b479b1267b9/fonttools-4.67.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcb9743140419410161acfe7ec205fb0a8a703acfccb85b586becb5a97c047c9", size = 3108316, upload-time = "2026-10-14T13:18:39.162Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/c8/5963603c5f9bbc28bde3a29dd7cdbe0bfcbee414b0f7eccec04ae477e1b6/fonttools-4.67.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ad813967410ba6d24a52850df59b164ee17883f17b96a91b4b0ac6e9d7b5a118", size = 2598450, upload-time = "2026-10-14T13:18:42.136Z" },
+ { url = "https://files.pythonhosted.org/packages/25/6d/f8e5924917a6b5c0296fb507f748c139a34972f66e91d89159d5c98e27b2/fonttools-4.67.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:768a33bbe6ec5ba8f19979f938752f06d4e614cb554fd47abd7830f2007660e3", size = 5427391, upload-time = "2026-10-14T13:18:44.248Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/e0/ec9e4cc868c514deb02233aa1047a6aeb9350d3ee012862f58eec10ef834/fonttools-4.67.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:eb3c98cac93aac4b9f6e3ce2008325340b234cc9b0338ca6b513f31962a1e278", size = 5408233, upload-time = "2026-10-14T13:18:46.616Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/4a/fe409cb3ab32f322de92e08e6362cd06bf6dd5f0cee5980d823849e9bd11/fonttools-4.67.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e0ca4c8438dd6320f5850c9bbee3b3980455ee3bac602a9a0299caf9e799a0e8", size = 5364778, upload-time = "2026-10-14T13:18:48.926Z" },
+ { url = "https://files.pythonhosted.org/packages/de/5b/2a8dede092113be56329dd210deb6b34c55df2f3d7270934ffece8c7d0bb/fonttools-4.67.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2a09d33a9264a6b29efca9dc633b53969aaedb250a9c8521d60f51280cef65ca", size = 5528522, upload-time = "2026-10-14T13:18:51.297Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/de/d3baf686e4ac5726a24819a670747c51571c774dcfa41cc0528e5e8c1a2d/fonttools-4.67.0-cp312-cp312-win32.whl", hash = "sha256:e8a8545cbd58bd29494ffe81e3cb35f8a29332a8e495c42bec334145ce8cd65b", size = 2446090, upload-time = "2026-10-14T13:18:53.379Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/3a/625a6dd0173e88dbea1826405b4bcbfa06c6ca095310ed720caba36b2e43/fonttools-4.67.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bfab2f5d1d255dec82f4bd082a1c10e77df808e42210890f50a9c30bf91570e", size = 2497985, upload-time = "2026-10-14T13:18:55.245Z" },
+ { url = "https://files.pythonhosted.org/packages/30/b4/cd473e0a48427003733e92bc3e8077081ba537eb33f7c658f2b7bef63776/fonttools-4.67.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8239e2ca24878715a19f061d065b5721e87da81d145e48b3418f771a469b5a24", size = 3106141, upload-time = "2026-10-14T13:18:57.238Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/36/04d74f0c71d93829657a703d680a54968253bbb5c93babc34378eae2087a/fonttools-4.67.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1be99c1f07fca59510d657ef3eae584b5273fa4e203aff2383b3520744e19536", size = 2598180, upload-time = "2026-10-14T13:18:59.443Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/e6/b0cbdedb363a49043d704d8c7903543fdd317596409fb8ac2cb604c1e73c/fonttools-4.67.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8b4f7c754a627e91908fa1a1ccc90b489cd2810c0ba16acd26ea2ff5273db7", size = 5400755, upload-time = "2026-10-14T13:19:01.557Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/26/939ae9874dd44116f2ecf61cb0caf029e3004ec1ed311a86389dee3450be/fonttools-4.67.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:50c41e30aa2e0130b80d1a58ac0f3ea7c02a854a70dbea1ff8d88e0ce524806f", size = 5382979, upload-time = "2026-10-14T13:19:03.726Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/d1/35a0a34ab74609d2e8dc7a1f45f6386c81942868fc4fdf8e873878f392fd/fonttools-4.67.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0781fe22583529e1e98bb8a3a33040632e202a4c427ed7e65412c41a21b8ebcb", size = 5343745, upload-time = "2026-10-14T13:19:06.055Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/90/293577941809c3ec5a7f0870c01b3729c682467a858b8978a5c3ea54c226/fonttools-4.67.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36f0fee56227b909c9d1392f17b23803616f1f04efbe020c176d9945cabc0be5", size = 5501573, upload-time = "2026-10-14T13:19:08.241Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/3c/4e25460f37840c51b3983a7a83ceef7a1efa9ea588aca6f0e3a852f4b120/fonttools-4.67.0-cp313-cp313-win32.whl", hash = "sha256:48696b630069e29b8aa5ea8b034e4f651a2e112073938ec16bd536dadde1debf", size = 2444457, upload-time = "2026-10-14T13:19:10.463Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/f6/39e9461211309965514642c005a8d51e866a1092f69f5f693b16de9c5395/fonttools-4.67.0-cp313-cp313-win_amd64.whl", hash = "sha256:7343cd0ef70edf8be7f4913cb9b55b992fb4e04055b47dcfecddcc2eb045a9d2", size = 2496147, upload-time = "2026-10-14T13:19:12.588Z" },
+ { url = "https://files.pythonhosted.org/packages/25/5b/c418f48918e40ef8c3f0f555567fe013c0c8058a8afa8040d6baeec80683/fonttools-4.67.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:846982e89b1861d6c9d7fcd6567aec3fa5a10ad313e7f2076045fcd339cfbd8e", size = 3110155, upload-time = "2026-10-14T13:19:14.877Z" },
+ { url = "https://files.pythonhosted.org/packages/30/18/49013c643c3d56fce1b7e909ef7c01c36a5bd906dfb58571c9dcdaa4dc38/fonttools-4.67.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:952eb091689545d86d16e40f719ed7bb086dd810a07dcc9ea2ca0a81004810a3", size = 2599657, upload-time = "2026-10-14T13:19:16.93Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/2c/b7f33fa3bd1e4afdf9bf93b760f22486350eda487ce76c47f5931f868957/fonttools-4.67.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b5d511ea012dce7bd6df12b279b7d7a5b01b019865717d03ae679f4b944fa5", size = 5385200, upload-time = "2026-10-14T13:19:18.868Z" },
+ { url = "https://files.pythonhosted.org/packages/79/fe/fef04b2cc2930edba11095f9e9b5c2797f8594fc54316195cc39d3c3bc63/fonttools-4.67.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:916836845e4b1c1447bb61390ffb3cb5f2940fd9f5d6de4685539a81806c7764", size = 5328754, upload-time = "2026-10-14T13:19:21.179Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/c6/41cd4f6137f61dd059cc0609b73d9556091ecfcc8cb4d3cc543129c8ec24/fonttools-4.67.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:775364ac079e2ea7a2eedb5f9172c57b059d638ff79e2bf8d4257e5805713f32", size = 5327159, upload-time = "2026-10-14T13:19:23.153Z" },
+ { url = "https://files.pythonhosted.org/packages/53/5c/08abd0a6d5c36624411e1b934745b4689d4309b03e98d8cf49f9469c63b6/fonttools-4.67.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b3ddf350e74508102b33dc6b32984b6dd751359a7c57732bcd39f9d7cb37d71e", size = 5460332, upload-time = "2026-10-14T13:19:25.454Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/0f/59e835023817fe3932653067fde74960a0800fb95535375d8206aa9ecd68/fonttools-4.67.0-cp314-cp314-win32.whl", hash = "sha256:72d6d316dffc92eadb771f697f289ea7b60f689580931328905a267bd170f93b", size = 2448714, upload-time = "2026-10-14T13:19:27.73Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/d3/5230265a5ff16aead01ce1a432a6b5bbdabe086f433988f41a1395e6dff8/fonttools-4.67.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e2c1586b5b6588a47d02e2588170eefdc996b708f2659c44dbe169bd6fcacb5", size = 2500790, upload-time = "2026-10-14T13:19:29.906Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/38/d899d7bbbe04d27dd509ac6b8f58f73fc240bb1dfe0ada9a9d33ad3bf9f2/fonttools-4.67.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:84a3aed005de106fb1794372dace82eca50859d52ae26da4bb6c602480a41250", size = 3183808, upload-time = "2026-10-14T13:19:32.015Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/f6/4f465a62972e383b3d82205841b93f625a4e5ece6e5693c5be2a691ffe6d/fonttools-4.67.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:64e56d0d6a39780fee86955c758674538387b18f911ea904a4aae8f8e30fa26f", size = 2632826, upload-time = "2026-10-14T13:19:33.854Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/91/ce1ae8f8baa75feb2320caf6f74d2c228eba210a13b3e0895c0403e5e987/fonttools-4.67.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c21073cfe7129aaa070d94f575c1e2a880ae4aae1dcffd5352f174b96d27d16", size = 5557857, upload-time = "2026-10-14T13:19:36.086Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/1c/495fe0a6bb8625e693c1417e178aeac42a11aa47e79efd7611c7bc5fb81e/fonttools-4.67.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:720bcf27727193b0fe1883c2e036dc88e37047916e977f5c3daf6ee4316e9656", size = 5364450, upload-time = "2026-10-14T13:19:38.5Z" },
+ { url = "https://files.pythonhosted.org/packages/19/9c/d9730d3dd32e39583d6db929d0867df02042539bb0ebc3ad3d92a52a6aaf/fonttools-4.67.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6c19a770a8d273371a37969003c143eaa629ab893c3db028af8b91d04c6f9a6d", size = 5425231, upload-time = "2026-10-14T13:19:40.659Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/c6/d41c1163431828b0fa2172e867798e0c4517ac6606e774b9175e048fb666/fonttools-4.67.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:13d7507252c5a5d7941a5fa1be27d335c378ef07983ea2bb24988bf600eadd5e", size = 5460574, upload-time = "2026-10-14T13:19:43.22Z" },
+ { url = "https://files.pythonhosted.org/packages/95/af/14885b78b1c1ff7219f890b79a5a6f76608d907c171b40e43839de995f54/fonttools-4.67.0-cp314-cp314t-win32.whl", hash = "sha256:07a2f36b3263faadf5b7b548f62fd3cac401e490189c82b16f7139ac0df91cd4", size = 2482468, upload-time = "2026-10-14T13:19:45.91Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/33/3d660eb850d24a81b4097ed46a1352c4ac0e4c10025526fa115e1871fc64/fonttools-4.67.0-cp314-cp314t-win_amd64.whl", hash = "sha256:fd79e36c2968e9fc3e1b082f2ba7dc63ae88a161a3d8ceaa0746b906455f3617", size = 2532326, upload-time = "2026-10-14T13:19:48.023Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/74/ebff33b3c6dfe77d86a1b67b470c3d817f044910203880a1f4e92a08bec2/fonttools-4.67.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:89ad62d116f45bb45873bb92fd69c14a720ba591cba488044731954a5565e194", size = 3104541, upload-time = "2026-10-14T13:19:50.418Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/f5/7b3b786447cdda91f8cd06e44bf3b906e71825118f5cbb9b69c099415152/fonttools-4.67.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:1671e5f368b0c136ed9fb62fef26c7e425b4ebb0bb669a1cb7ba453f5bba580b", size = 2598216, upload-time = "2026-10-14T13:19:52.388Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/c8/c0c08d8a76b2ed460bf8b63642d98445aa18179a14005cae617bfe9ec732/fonttools-4.67.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451077d2fc61a2a03f5dca54d84fbb01051ad781f48ea137eff35c775a4cb025", size = 5382399, upload-time = "2026-10-14T13:19:54.344Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/db/66b5ef9985c7d69f7b3521ee965c3093b1802322fb6c16e8c3da608b747e/fonttools-4.67.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f200cd2cf046a5a0b03babe84ebf8bbc12187d5d57f50bc03f24be89e7c1605", size = 5345746, upload-time = "2026-10-14T13:19:56.472Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/b0/77d22a73d5cfce9651909583ea3011c7ab26daf155b0eb21f7a3f02ac78a/fonttools-4.67.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bd3239e5709fd4c3343db67245ede46aece610d7f7ef61afb174718122479282", size = 5322591, upload-time = "2026-10-14T13:19:59.539Z" },
+ { url = "https://files.pythonhosted.org/packages/97/b8/d3e7b799186fc3213a31d0cfa2c553c5d8eed0a7c7960dc3cf7c0d0497fa/fonttools-4.67.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b274ed3106b8086f237b7dbb1529c28142ba10ae40b9d285be0ae6a44b2946d0", size = 5470360, upload-time = "2026-10-14T13:20:01.876Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/89/c9799e81e6de16196d4781dbb81136d354eaef07136607917275a5fe958f/fonttools-4.67.0-cp315-cp315-win32.whl", hash = "sha256:fc6b6b03aa44f504c8734e62ccc3e4dcda9f4b8213a85aa80742e4d1cc9d96ef", size = 2447522, upload-time = "2026-10-14T13:20:04.197Z" },
+ { url = "https://files.pythonhosted.org/packages/79/48/40f5591bd0e198d34ee3e25710e730c824750b3c822fc0a65b08e193de80/fonttools-4.67.0-cp315-cp315-win_amd64.whl", hash = "sha256:592d8f72024dea0408739a92599e4f839b960e1e887b25adc76dc87271fdac76", size = 2499770, upload-time = "2026-10-14T13:20:06.54Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/5c/f98ee788f76ffad100427c20abab3a6213b37c97575dc82e4ccfaaafbc55/fonttools-4.67.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:9c38fece8156cbda31b42d49c4a187858056a35932b88233b6fb31eaca5cf67f", size = 3175346, upload-time = "2026-10-14T13:20:08.7Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/b1/af3016813fd44c0ed32d37f3a12cb707efd99edd8205bd8b73aea1f0f542/fonttools-4.67.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3b34324deb3e09ad648039a0a86d945b83f23a44fe3da74a84e6ada71fe0b650", size = 2629083, upload-time = "2026-10-14T13:20:10.686Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/bc/13b45dec208145da2c49c063b6ce73ddb2e6e3bd137ba3613562d686a013/fonttools-4.67.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a19f6d5e1a373f2e4a5bdb9452c8ba212dd9f1e43df2fff042b896e28084e4a", size = 5535751, upload-time = "2026-10-14T13:20:13.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/8c/01f2f16066c802ad2cd6f3321c226240475b30ada91d69d493f7a40445a7/fonttools-4.67.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ccaa87b312219d02cf72a79f1eb2f3ce028882d6fd1b79336141005db83b84e", size = 5357413, upload-time = "2026-10-14T13:20:15.289Z" },
+ { url = "https://files.pythonhosted.org/packages/84/e6/d6dff534e9cb8688ec7ecddc353609bca580efef9967334e2289f56bd9da/fonttools-4.67.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:38fc772182ebff3e2ebba7886460476eb65842b601ca0b9221a6a5826136396e", size = 5403722, upload-time = "2026-10-14T13:20:17.535Z" },
+ { url = "https://files.pythonhosted.org/packages/39/c8/4de02224adea134666e6705b0137cd3df2df60a03ce100797b2b221a73dd/fonttools-4.67.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f672398385849ff79e7dd50c0a06efe110c8ba23d8890f9b45fbb922bc2f55f6", size = 5450104, upload-time = "2026-10-14T13:20:19.612Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/e1/3a32904bac7c3460e23a86e9e1529b40d0969a69bd4edefa31e2d2f1bae7/fonttools-4.67.0-cp315-cp315t-win32.whl", hash = "sha256:77e0d4096a2ac60aebe43928b5382766df2d148577db8e8ff79b6a50879a6c06", size = 2479606, upload-time = "2026-10-14T13:20:21.996Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/c5/8834cfb95383059addca24f591379d152f137689ff63766736c26b0f9b25/fonttools-4.67.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8c58a8a9ad447bead6f91e5f50b23c0e4988538cdbd9bf2f68952b39f5900a84", size = 2527993, upload-time = "2026-10-14T13:20:23.949Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/61/4161946319472aaa9b897bd18ad5108a5b10f5ebaa503d921a001ac4fff9/fonttools-4.67.0-py3-none-any.whl", hash = "sha256:4304f03ed7f4ba000a8dcc941ad854bfa52e2f3b6112b8f099b6f431cf98e701", size = 1213142, upload-time = "2026-10-14T13:20:26.258Z" },
+]
+
+[[package]]
+name = "resvg-py"
+version = "0.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/64/a24f8f29d8bf158e01f6ccad68a1366afd922dc0f0977cbd0c0aaa7a22f2/resvg_py-0.5.0.tar.gz", hash = "sha256:6d3bf8e866b4e129524d9432a809138b2d100931d8d635bc81294002abcdfd46", size = 1756395, upload-time = "2026-08-24T19:43:27.663Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/89/49f7c84a2a3fc3d2b9973134f72f95467a40acfbc7ca5d820aeb06af6e79/resvg_py-0.5.0-cp310-abi3-android_24_arm64_v8a.whl", hash = "sha256:2715f2b88ce2cf91f57ff37bb34c5909c8007de431d488e9c3ebf6cf2d69c91b", size = 1370989, upload-time = "2026-08-24T19:42:09.747Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/d1/09ebd099134589225861e0668c9fdff103b0450df0e399689787a0d8962f/resvg_py-0.5.0-cp310-abi3-android_24_x86_64.whl", hash = "sha256:9901e2f9ce53e7535d2676123c8d4894bff040f52821e54192605b5dd4fb5af9", size = 1366983, upload-time = "2026-08-24T19:42:11.479Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/36/3408156e9cba54d1ef5793377f39be4096660933cc6df155ba425315bf09/resvg_py-0.5.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9d3f5c2544d6b5f74847513e07e6ab6a70f9e7f0d8a141bc16bd4b0c555f4234", size = 1261946, upload-time = "2026-08-24T19:42:12.703Z" },
+ { url = "https://files.pythonhosted.org/packages/74/bf/4083b177388125e5ce2ab9fa4cd9efd881fa133dd97e5b2d4ca68e543256/resvg_py-0.5.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:7b43f942157f5d16126e108dab8ab37e4bc2b198099e5f6274b753a3b1ac7b6e", size = 1216814, upload-time = "2026-08-24T19:42:13.943Z" },
+ { url = "https://files.pythonhosted.org/packages/52/92/1dfd0d7b5f8dbb16f9c889bba0d7477ab514d1f2a81a5f904662215103bc/resvg_py-0.5.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e66216f78c84a27d34ce75f4535d8e26565771be4f1848ddc71e8a7ce78973a", size = 1414542, upload-time = "2026-08-24T19:42:15.538Z" },
+ { url = "https://files.pythonhosted.org/packages/13/99/a77f933e6cc355fd168f6eae2e23b5d361cb61531bfb83477f9816a62a49/resvg_py-0.5.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:977921f22b0a3283e6cd121339a2aff51d0df3b542ce7a5f96fa3a87f8d65106", size = 1321567, upload-time = "2026-08-24T19:42:17.142Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e0/a9d0cf6cee5fb1bf3abdba760821f76e1c979f81923c0bf54279dd1a285e/resvg_py-0.5.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da8e52d7d5d16b288aa47fa830fd66301a6b6f135f9f37fa9f4854b7a722e6d", size = 1501716, upload-time = "2026-08-24T19:42:18.482Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/f2/cf7390e196923a0f591981d3f2754f70825f0a8b206d0778866806ba1159/resvg_py-0.5.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:200baa4a01b6779d7b6f3fa31e5eabfc5ab594a1317d75853ee73c5229686599", size = 1450509, upload-time = "2026-08-24T19:42:19.601Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/08/217f2289ceb16a4eafd9c9c6f69aa3221ef047a6abe4ff1ce5c8d6be87d8/resvg_py-0.5.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84f2378ecc7a8e38b03429efaefc816daec1b1970114909a6f973393b297c91b", size = 1405807, upload-time = "2026-08-24T19:42:20.75Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/d6/b3b9411b5b812799621ee43844552cbf7c0ddc2d5a552f5e91ba808ac67c/resvg_py-0.5.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9ebcc40941811b49001ad4e721aa87f489b74c0132ff3fcbceed97305c944749", size = 1491386, upload-time = "2026-08-24T19:42:22.131Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/e6/5d8e0fac79e19ec95db6902ab03f3e681a69183a0a959fb081a7385c9e7e/resvg_py-0.5.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7c3e8c2324fc2bcf03c1010b7987adf5ff4ce43e8fc1a7c9b8271cbbcca6ba37", size = 1593186, upload-time = "2026-08-24T19:42:24.057Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/81/db56ea6225d0294dfc5e96fa18d16231e33cd1812a75c4cf03e8e17586cc/resvg_py-0.5.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a5db1a607059a48f5d4c20c7363e4888e001b11a04465d36e3ca77a511651eb", size = 1600258, upload-time = "2026-08-24T19:42:26.11Z" },
+ { url = "https://files.pythonhosted.org/packages/24/66/43c32a28e5d19ada46c8589cb3eaef5db0dc151be1aec0e86a68b9534ba7/resvg_py-0.5.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:d54a8c85e7d6f4ba55f39c2330c7830d8c98a7dc205ca3c2ca069f9b11cb01c4", size = 1653522, upload-time = "2026-08-24T19:42:27.674Z" },
+ { url = "https://files.pythonhosted.org/packages/65/01/91794e3dedcfaf93b780ecd4cf0061262fd2665034756773f7e768812389/resvg_py-0.5.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:feee1ee6c2c0b64018c046a7604240c233c6cf14496e475238370c7d9a9db455", size = 1622060, upload-time = "2026-08-24T19:42:28.858Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/20/a7d7371a4104fd733702b942c396fd898510431ee3a1d2f7b4462da65117/resvg_py-0.5.0-cp310-abi3-win32.whl", hash = "sha256:45b2e66f76e7649155dc768c3cd1f5a94907d0086c2bde22da14c9ecbf9eda9a", size = 1204672, upload-time = "2026-08-24T19:42:30.191Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/53/aa8f92ce6eb2f97095d8b6359a1613c5a5ee0aa9b1a33434df9294362979/resvg_py-0.5.0-cp310-abi3-win_amd64.whl", hash = "sha256:1f6b8956c4143dbfe107bcd35799d0dfd778a40a8cd537893c0bf489898a6c3c", size = 1242277, upload-time = "2026-08-24T19:42:31.42Z" },
+ { url = "https://files.pythonhosted.org/packages/56/64/e63614663df1404999802e82d462ffa534999c126067257bfba7d1de590c/resvg_py-0.5.0-cp310-abi3-win_arm64.whl", hash = "sha256:8016e2006c09953570af466e7674c398c1f255cb00022152b15e18f9e8ca3af8", size = 1153784, upload-time = "2026-08-24T19:42:32.607Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/1e/4e24cdabab6c4f9b2d1175fe6b57f07f24625a6a8e1ff331a2a4a28da3a6/resvg_py-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5547fc79ee600ee0e40ad01cdeb36140a74e85cfad2722973dae654db3667fcd", size = 1261363, upload-time = "2026-08-24T19:42:33.844Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/98/d4d0128dc2fd71eeaaa4e82d6c5a6213a89bcc96dd03b759fb7a5588c496/resvg_py-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4bd8da85e5332aded549894d7fe4aec6f19a681ecda3385acbfdf1a1c67bc8da", size = 1216436, upload-time = "2026-08-24T19:42:35.016Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/74/34fde2a05e81b6fd57445b18660fb7c3c2c988908cdf59f57f2481c98606/resvg_py-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1447c10535c4fa122bb20f702da5d23ad5053cdff831aa2d6f6b482ebab12485", size = 1413277, upload-time = "2026-08-24T19:42:36.295Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e0/0225387a65b51a9e2a5b6a11a517e777b82e887db4b67e6e44d44607cf18/resvg_py-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1ed526890579f8bf1afcf6c17f29c22659196c57b2c760e485f15dfc93dca64", size = 1320862, upload-time = "2026-08-24T19:42:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/49/25/033b4ff263788ea10ae8e5ab2c445e8dcf0d78941b322781f8abe323dc73/resvg_py-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfe18bc3d36cc885190f450d5f0d26473c5cecb86b28bcb714f7a025e1cfd5c2", size = 1500068, upload-time = "2026-08-24T19:42:38.978Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/5a/472629604d6d0fbae13648d3ef378727b533e71baeff3403367b5efa9ae2/resvg_py-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8481cdbaf7fea5dbf2bfe57e86201c6a40193c462e365729185c66849b5966a", size = 1449571, upload-time = "2026-08-24T19:42:40.263Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/50/9776c9a2181205a21f90c1a14cd1deeacccb66d19457cf9b9cc25ebba17d/resvg_py-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12cdd0349ebd8efaade78f74fc3fc08fcdd3f21f7152fb199a561176a64581bf", size = 1404321, upload-time = "2026-08-24T19:42:41.391Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ec/78f53523b7c387312b0b790d33373d502eaf41310953baf2b17e18812d36/resvg_py-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c184cad5c3593dbe655ef9f304068fa941646947d94767dbd1afcbf094c8dae5", size = 1490813, upload-time = "2026-08-24T19:42:42.766Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/4e/ac6077896efb94d8c7ebe08552da48c323c657554e715c80f4e8e8f30cca/resvg_py-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:75e6822b492c66d85f03a6f2510ac69902ff0509a2a86cd50ef30ebb73c714ea", size = 1591632, upload-time = "2026-08-24T19:42:44.129Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/49/7dfe358ac7d52849b16ef98ad2cd4a41f77a87cff96122da095861fbb070/resvg_py-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1abaadac95daa2907e3fa0f668c90a099d4bfffe0a6fa7cf36d30c607bfd5797", size = 1599831, upload-time = "2026-08-24T19:42:45.373Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/d2/f53350c3b2c512ae9d6ab4ae39db20bb573048bfcc60fdd5213ab7474d81/resvg_py-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92cdc56331224980b85c1604c7da9bef37737657cf123d88e920ca10a07c6a11", size = 1652833, upload-time = "2026-08-24T19:42:46.758Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/04/d958e02af538996ce963baa88d47667fc28c72b72aefea78546ab89a6288/resvg_py-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:403fed36186bbe4ef4eb3ec1d5fabe003a1c90ad71f145ca0e9b9f7f80e70696", size = 1620547, upload-time = "2026-08-24T19:42:48.173Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9d/8dc520a62512f309bc74dddee8309d8a940ae1ee80317825b5ef705d3b4e/resvg_py-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:c7fad8f8c28e770da8783dc429bfa0d71f2abe740be2f8d726308a669a392919", size = 1204113, upload-time = "2026-08-24T19:42:49.398Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/0b/8edd6a94ed6c9d8306008c277c0e5f0d74df87cd2109eaff86ded437fcad/resvg_py-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0284c50c7e3b009e97e5c64a2d31e02b8bb36cd066d947fda9e43f480ca19f7", size = 1240324, upload-time = "2026-08-24T19:42:50.87Z" },
+ { url = "https://files.pythonhosted.org/packages/76/78/bdeb2fc44497c53c9f53e05acf58a571dc2589465031e37b9de8c0e57044/resvg_py-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5c026b67b79604f32865e132ef68ff25633b8668e35b29ad412fcef906c0c39a", size = 1152505, upload-time = "2026-08-24T19:42:52.06Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/d1/2f85ce0ec44642a849a57a709e121dd2fa934ea1a54d77bb31e8f4aea7e8/resvg_py-0.5.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:51fa0564ad1a3e82307c1aed7222b66edeb3b8c595251709f929b0a819109da1", size = 1261198, upload-time = "2026-08-24T19:42:53.279Z" },
+ { url = "https://files.pythonhosted.org/packages/51/0c/b7af93cfd9bbcd83a4c8970e17dcf4917f12d3b88b695fa9a9b86913d375/resvg_py-0.5.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:1f91870d5315168093d546777fccece4406ad2053c1908f5ceab3c39f2c49e7c", size = 1216652, upload-time = "2026-08-24T19:42:54.876Z" },
+ { url = "https://files.pythonhosted.org/packages/85/e8/2d6dbd6cf5be1871248d9e6307b4f42e916a16d83c13590ace9dccb8f49f/resvg_py-0.5.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d597eef189a8e728c8026417ea51b61720c83d2ed262b508362c4309cd57bc8a", size = 1412990, upload-time = "2026-08-24T19:42:56.491Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/10/c10989f4eebd61242134a0bc1e26a2eaf618cf911115e447ea570bfd9bdb/resvg_py-0.5.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5befa08450f4248b9670e054f446065d0fc33c1a4ff302baaacc205ddee97b3c", size = 1320774, upload-time = "2026-08-24T19:42:57.697Z" },
+ { url = "https://files.pythonhosted.org/packages/de/ae/b6416f0d984a445d2ba962dd39750dd0f79c16346517f8f98b595bbdb37c/resvg_py-0.5.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17640c3bb2f4498a6aa61d256ec21257b32e68fd2564b509c4919f5171561b99", size = 1500202, upload-time = "2026-08-24T19:42:58.886Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f2/f44bc28c82e3f21065a0b721ddae31769420747f99b807df83560dc75697/resvg_py-0.5.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fca2d6b28938e7fa7553a7f9c5f330b908c7fd8c50f4fe3d175ddcc5e958879", size = 1449452, upload-time = "2026-08-24T19:43:00.044Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/c9/c4cbcbbe45d327a669c4c346cdef9253a50e052c102da4fc92576e407041/resvg_py-0.5.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c4cc14543c29b753db751eace1dadcf0d58d77aa58be5370393bcfbcc1cbc2f", size = 1404433, upload-time = "2026-08-24T19:43:01.57Z" },
+ { url = "https://files.pythonhosted.org/packages/60/03/7b7c89086cb7cbede4e21bcbbf2870d62564dcce71fc23fa97de67293ba5/resvg_py-0.5.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:869e4ab0b8f4a403d6fac93d2c4e3df79488b9f6fd053ba0f4fa4ed45d456fe5", size = 1490660, upload-time = "2026-08-24T19:43:03.026Z" },
+ { url = "https://files.pythonhosted.org/packages/99/07/4a9595a3c760c91006ac4753ced8daabd2c6d64024ca668e2867bb84a283/resvg_py-0.5.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f322d7bf0ddab60156d6cf1883718b726c1c210bd7623e253756986798c5c83e", size = 1591455, upload-time = "2026-08-24T19:43:04.404Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/7e/c2151824834b6df07083489a959cab2b37ba3b1834cc5e576aeb95e86e92/resvg_py-0.5.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:55d65708e2dee0de77cccc0d03d21cd148a491c2bc6ef25542081db8eee74923", size = 1599781, upload-time = "2026-08-24T19:43:05.666Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/ef/573c43420a5c39758f9e2cf67e8834ad430935eaecfb71c7ba73457b65c5/resvg_py-0.5.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:04b32b1e2d7a848124d9b96bc7446ceae71ea144d950007e93c4a382f7ee134c", size = 1652940, upload-time = "2026-08-24T19:43:06.993Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/76/68290af871f9347e1e8c7e14c8b09251362c74cff406b5f94c6711e8b6a2/resvg_py-0.5.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:7fc91829a4d12d80071e9f4f191a9f459adf310919cbdf1377711b30dfa996b5", size = 1620456, upload-time = "2026-08-24T19:43:08.422Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/af/28e4758e087c6d3a3e691ecd67fd1304074b9bca4b5f1563ab6d1336a6a4/resvg_py-0.5.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:f0c834262db96eac4d5767e1025c21efefa0ed0359bded8dfd4b79fc7549694f", size = 1204143, upload-time = "2026-08-24T19:43:09.706Z" },
+ { url = "https://files.pythonhosted.org/packages/73/5c/5b0e68ce15bd87eaee64501427437f57bece008e15bf40dd759563f0038a/resvg_py-0.5.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:011111a4c3f46d409e989fe88ec783a3ffae1f3877092ab0351aaf2167fe399d", size = 1240349, upload-time = "2026-08-24T19:43:11.397Z" },
+ { url = "https://files.pythonhosted.org/packages/62/e6/25b6616cebbce1412bb5fe8b037545f382b4da875bc614a97ff1ecd7aa28/resvg_py-0.5.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:66e5a7699f2b00024ed7e95ec53df05bb3da277ee5bc86f867d487e310e4d392", size = 1152399, upload-time = "2026-08-24T19:43:12.618Z" },
+]
diff --git a/assets/branding/header-dark.svg b/assets/branding/header-dark.svg
new file mode 100644
index 0000000..0deff68
--- /dev/null
+++ b/assets/branding/header-dark.svg
@@ -0,0 +1 @@
+OpenUrzędnik.NET
diff --git a/assets/branding/header-light.svg b/assets/branding/header-light.svg
new file mode 100644
index 0000000..748397f
--- /dev/null
+++ b/assets/branding/header-light.svg
@@ -0,0 +1 @@
+OpenUrzędnik.NET
diff --git a/assets/branding/icon-nbp.png b/assets/branding/icon-nbp.png
new file mode 100644
index 0000000..182042d
Binary files /dev/null and b/assets/branding/icon-nbp.png differ
diff --git a/assets/branding/icon-nbp.svg b/assets/branding/icon-nbp.svg
new file mode 100644
index 0000000..89579e3
--- /dev/null
+++ b/assets/branding/icon-nbp.svg
@@ -0,0 +1 @@
+OpenUrzędnik.NET NBP
diff --git a/assets/branding/icon.png b/assets/branding/icon.png
new file mode 100644
index 0000000..fc61710
Binary files /dev/null and b/assets/branding/icon.png differ
diff --git a/assets/branding/icon.svg b/assets/branding/icon.svg
new file mode 100644
index 0000000..0e61586
--- /dev/null
+++ b/assets/branding/icon.svg
@@ -0,0 +1 @@
+OpenUrzędnik.NET
diff --git a/assets/branding/social-preview-en.png b/assets/branding/social-preview-en.png
new file mode 100644
index 0000000..39f5390
Binary files /dev/null and b/assets/branding/social-preview-en.png differ
diff --git a/assets/branding/social-preview-en.svg b/assets/branding/social-preview-en.svg
new file mode 100644
index 0000000..29ee5ae
--- /dev/null
+++ b/assets/branding/social-preview-en.svg
@@ -0,0 +1 @@
+OpenUrzędnik.NET
diff --git a/assets/branding/social-preview.png b/assets/branding/social-preview.png
new file mode 100644
index 0000000..d23dd4a
Binary files /dev/null and b/assets/branding/social-preview.png differ
diff --git a/assets/branding/social-preview.svg b/assets/branding/social-preview.svg
new file mode 100644
index 0000000..15a7db0
--- /dev/null
+++ b/assets/branding/social-preview.svg
@@ -0,0 +1 @@
+OpenUrzędnik.NET
diff --git a/src/OpenUrzednik.Nbp.DependencyInjection/OpenUrzednik.Nbp.DependencyInjection.csproj b/src/OpenUrzednik.Nbp.DependencyInjection/OpenUrzednik.Nbp.DependencyInjection.csproj
index 0df5363..03cdadd 100644
--- a/src/OpenUrzednik.Nbp.DependencyInjection/OpenUrzednik.Nbp.DependencyInjection.csproj
+++ b/src/OpenUrzednik.Nbp.DependencyInjection/OpenUrzednik.Nbp.DependencyInjection.csproj
@@ -3,6 +3,7 @@
Unofficial Microsoft.Extensions.DependencyInjection registration for the OpenUrzednik.NET NBP clients: typed HttpClients, validated options, logging and tracing; ready for AddStandardResilienceHandler() on .NET 8 and later.
NBP;DependencyInjection;HttpClientFactory;API;Poland;Gov
+ icon-nbp.png
diff --git a/src/OpenUrzednik.Nbp/OpenUrzednik.Nbp.csproj b/src/OpenUrzednik.Nbp/OpenUrzednik.Nbp.csproj
index dcf2b25..3114fd0 100644
--- a/src/OpenUrzednik.Nbp/OpenUrzednik.Nbp.csproj
+++ b/src/OpenUrzednik.Nbp/OpenUrzednik.Nbp.csproj
@@ -3,6 +3,7 @@
Unofficial .NET client for the NBP (Narodowy Bank Polski) API: currency exchange rates, rate tables and gold prices. Not affiliated with NBP.
NBP;Currency;ExchangeRates;Gold;API;Poland;Gov
+ icon-nbp.png