feat(docs/product): add university datasheet and GDS-textured header bars

Adds a second audience-specific config alongside the individual product
sheet, plus several fixes to the shared template and build.

datasheet-universities.yaml: written for a department buying for a cohort
rather than an individual. Leads on per-student cost with a worked class
example, carries testimonials from Murmann and Pretl, and answers the
procurement and academic-calendar questions an individual buyer never asks.
US institution list, education@tinytapeout.com as the contact.

Template gains optional quote band, footer contact block, and conditional
eyebrow / doc ref / photo caption so both sheets share one template. The
hero photo now derives its height from the text column instead of a fixed
44mm, so the two columns always end level whatever the copy length.

Header bars carry the chip-layout texture from the tt-datasheet-artwork
repo, renormalised from its native ~7% alpha and rebuilt as black traces:
they darken the bar, so contrast with the white type goes up rather than
down. Bar colour moves from #040371 to THEME_SKY_BLUE #0c66ab, where the
texture is actually legible; the config key is renamed navy -> brand to
match.

Font subset rebuilt from the full Archivo to include U+03A9 — page 2 has
used "< 500 Ω" since the first build and was silently falling back to a
system face. build.py now warns about any character the embedded font
cannot draw, and validates HTML in the config for unbalanced tags and
malformed hrefs, both of which had shipped unnoticed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Matthew Venn
2026-08-02 19:05:18 +02:00
co-authored by Claude Opus 5
parent 29a735c091
commit 1cb043a704
13 changed files with 445 additions and 22 deletions
+33
View File
@@ -173,6 +173,33 @@ def as_data_uri_payload(path: Path) -> str:
return base64.b64encode(path.read_bytes()).decode()
def check_glyph_coverage(content: dict, font: Path) -> None:
"""Warn about characters the embedded font cannot draw.
The font is subset, so a stray character silently falls back to whatever the
renderer has to hand — a different typeface mid-word, or a blank box in the
PDF. Needs fontTools; skipped quietly when it isn't installed, since it is
not a runtime dependency of the build.
"""
try:
from fontTools.ttLib import TTFont
except ImportError:
return
covered = set(TTFont(font).getBestCmap())
missing: dict[str, str] = {}
for path, text in walk_strings(content):
for char in text:
if ord(char) > 0x20 and ord(char) not in covered:
missing.setdefault(char, path)
for char, path in sorted(missing.items()):
logging.warning(
f"{path}: U+{ord(char):04X} {char!r} is not in the embedded font "
"and will fall back to another typeface"
)
def prepare_supporters(supporters: dict, assets: Path) -> None:
"""Inline each supporter logo and work out its per-item sizing."""
height = supporters.get("logo_height", "8.5mm")
@@ -321,8 +348,14 @@ def main() -> None:
prepare_table(key, content[key])
assets = HERE / "assets"
check_glyph_coverage(content, assets / content["assets"]["font"])
content["font_b64"] = as_data_uri_payload(assets / content["assets"]["font"])
content["photo_b64"] = as_data_uri_payload(assets / content["assets"]["photo"])
texture = content["theme"].get("bar_texture")
if texture:
content["bar_texture_b64"] = as_data_uri_payload(assets / texture)
content["theme"].setdefault("bar_texture_opacity", 0.55)
if "supporters" in content:
prepare_supporters(content["supporters"], assets)