perf: subset tabler-icons font/CSS to used icons only (801KB -> 18KB woff2, 217KB -> 4KB CSS)

- Remove unused .ttf and .woff font files
- Fix preload URL mismatch causing double font download
- Remove content-visibility: auto (Firefox freeze on tab hover)
- Add font-display: swap for non-blocking font loading
- Add tools/subset-icons.py for future icon subsetting
This commit is contained in:
scriptos committed 2026-04-08 22:32:33 +02:00
1 parent 34ce8a8fc3
commit 8a10981ecc
8 files changed
+188 -3

No files matched your search

+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""
Tabler Icons Subset Tool for Keywarden
=======================================
Scans all HTML templates for used ti-* icon classes, then generates:
1. A subsetted woff2 font with only the needed glyphs
2. A minimal CSS file with only the matching icon rules
Prerequisites (one-time):
pip install fonttools brotli
Usage:
python tools/subset-icons.py
Source files (full Tabler Icons 3.6.0) are stored in tools/tabler-icons-full/.
Output goes directly to web/static/css/ and web/static/css/fonts/.
"""
import os
import re
import subprocess
import sys
# ── Paths (relative to project root) ──
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "web", "templates")
FULL_CSS = os.path.join(SCRIPT_DIR, "tabler-icons-full", "tabler-icons.min.css")
FULL_FONT = os.path.join(SCRIPT_DIR, "tabler-icons-full", "tabler-icons.woff2")
OUT_CSS = os.path.join(PROJECT_ROOT, "web", "static", "css", "tabler-icons.min.css")
OUT_FONT = os.path.join(PROJECT_ROOT, "web", "static", "css", "fonts", "tabler-icons.woff2")
def find_used_icons():
"""Scan all .html templates for ti-* class names."""
icons = set()
pattern = re.compile(r"ti-[a-z][a-z0-9-]+")
for root, _, files in os.walk(TEMPLATE_DIR):
for f in files:
if not f.endswith(".html"):
continue
with open(os.path.join(root, f), encoding="utf-8") as fh:
for match in pattern.finditer(fh.read()):
icons.add(match.group(0))
# ti-spin is a CSS animation class, not an icon glyph
icons.discard("ti-spin")
return sorted(icons)
def extract_codepoints(css_text, icons):
"""Extract Unicode codepoints from the full CSS for each icon."""
codepoints = []
missing = []
for icon in icons:
pat = re.escape("." + icon) + r':before\{content:"\\([0-9a-f]+)"\}'
m = re.search(pat, css_text)
if m:
codepoints.append(m.group(1))
else:
missing.append(icon)
return codepoints, missing
def build_subset_css(css_text, icons):
"""Build a minimal CSS containing only the @font-face, .ti base rule,
and the individual icon rules for the used icons."""
# Extract header comment + @font-face + .ti base rule
m = re.match(r'(/\*[\s\S]*?\*/)(@font-face\{[^}]+\})(\.ti\{[^}]+\})', css_text)
if not m:
print("ERROR: Could not parse base CSS rules from full source")
sys.exit(1)
# Patch @font-face: keep only woff2 and add font-display:swap
font_face = m.group(2)
# Remove woff and truetype sources, keep only woff2
font_face = re.sub(
r',url\("[^"]*\.woff\?[^"]*"\)\s*format\("woff"\)', '', font_face
)
font_face = re.sub(
r',url\("[^"]*\.ttf[^"]*"\)\s*format\("truetype"\)', '', font_face
)
# Add font-display:swap if not present
if "font-display" not in font_face:
font_face = font_face.replace(
"font-weight:400;",
"font-weight:400;font-display:swap;"
)
header = m.group(1) + font_face + m.group(3)
# Extract individual icon rules
rules = []
for icon in icons:
pat = re.escape("." + icon) + r':before\{content:"\\[0-9a-f]+"\}'
match = re.search(pat, css_text)
if match:
rules.append(match.group(0))
# Keep .ti-spin animation if present
result = header + "".join(rules)
spin_kf = re.search(r'@keyframes\s+spin\{[^}]+\{[^}]+\}\}', css_text)
spin_cls = re.search(r'\.ti-spin\{[^}]+\}', css_text)
if spin_kf:
result += spin_kf.group(0)
if spin_cls:
result += spin_cls.group(0)
return result, len(rules)
def subset_font(codepoints):
"""Run pyftsubset to create a woff2 with only the needed glyphs."""
unicodes = ",".join(f"U+{cp}" for cp in codepoints)
cmd = [
sys.executable, "-m", "fontTools.subset",
FULL_FONT,
f"--output-file={OUT_FONT}",
"--flavor=woff2",
"--no-layout-closure",
"--drop-tables+=GSUB,GPOS,GDEF",
f"--unicodes={unicodes}",
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print("ERROR: pyftsubset failed:")
print(result.stderr)
sys.exit(1)
def main():
# Verify source files exist
for path in (FULL_CSS, FULL_FONT):
if not os.path.exists(path):
print(f"ERROR: Source file missing: {path}")
print("These should be in tools/tabler-icons-full/")
sys.exit(1)
# Try importing fonttools
try:
import fontTools # noqa: F401
except ImportError:
print("ERROR: fonttools not installed. Run: pip install fonttools brotli")
sys.exit(1)
print("Scanning templates for icon usage...")
icons = find_used_icons()
print(f" Found {len(icons)} unique icons")
print("Reading full CSS source...")
with open(FULL_CSS, encoding="utf-8") as f:
full_css = f.read()
print("Extracting Unicode codepoints...")
codepoints, missing = extract_codepoints(full_css, icons)
if missing:
print(f" WARNING: No codepoint found for: {', '.join(missing)}")
print(f" Mapped {len(codepoints)} codepoints")
print("Subsetting font...")
subset_font(codepoints)
orig_size = os.path.getsize(FULL_FONT)
new_size = os.path.getsize(OUT_FONT)
print(f" {orig_size//1024} KB -> {new_size//1024} KB ({100-round(new_size/orig_size*100,1)}% smaller)")
print("Building subset CSS...")
css_out, rule_count = build_subset_css(full_css, icons)
with open(OUT_CSS, "w", encoding="utf-8") as f:
f.write(css_out)
orig_css_size = os.path.getsize(FULL_CSS)
new_css_size = os.path.getsize(OUT_CSS)
print(f" {rule_count} icon rules, {orig_css_size//1024} KB -> {new_css_size//1024} KB")
print("\nDone! Subsetted files written to:")
print(f" Font: {os.path.relpath(OUT_FONT, PROJECT_ROOT)}")
print(f" CSS: {os.path.relpath(OUT_CSS, PROJECT_ROOT)}")
if __name__ == "__main__":
main()
+4
View File
@@ -0,0 +1,4 @@
/*!
* Tabler Icons 3.6.0 by tabler - https://tabler.io
* License - https://github.com/tabler/tabler-icons/blob/master/LICENSE
*/@font-face{font-family:"tabler-icons";font-style:normal;font-weight:400;src:url("./fonts/tabler-icons.woff2?v3.6.0") format("woff2"),url("./fonts/tabler-icons.woff?") format("woff"),url("./fonts/tabler-icons.ttf?v3.6.0") format("truetype")}.ti{font-family:"tabler-icons" !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ti-a-b:before{content:"\ec36"}.ti-a-b-2:before{content:"\f25f"}.ti-a-b-off:before{content:"\f0a6"}.ti-abacus:before{content:"\f05c"}.ti-abacus-off:before{content:"\f3b6"}.ti-abc:before{content:"\f567"}.ti-access-point:before{content:"\ed1b"}.ti-access-point-off:before{content:"\ed1a"}.ti-accessible:before{content:"\eba9"}.ti-accessible-filled:before{content:"\f6ea"}.ti-accessible-off:before{content:"\f0a7"}.ti-activity:before{content:"\ed23"}.ti-activity-heartbeat:before{content:"\f0db"}.ti-ad:before{content:"\ea02"}.ti-ad-2:before{content:"\ef1f"}.ti-ad-circle:before{content:"\f79e"}.ti-ad-circle-filled:before{content:"\f7d3"}.ti-ad-circle-off:before{content:"\f79d"}.ti-ad-filled:before{content:"\f6eb"}.ti-ad-off:before{content:"\f3b7"}.ti-address-book:before{content:"\f021"}.ti-address-book-off:before{content:"\f3b8"}.ti-adjustments:before{content:"\ea03"}.ti-adjustments-alt:before{content:"\ec37"}.ti-adjustments-bolt:before{content:"\f7fb"}.ti-adjustments-cancel:before{content:"\f7fc"}.ti-adjustments-check:before{content:"\f7fd"}.ti-adjustments-code:before{content:"\f7fe"}.ti-adjustments-cog:before{content:"\f7ff"}.ti-adjustments-dollar:before{content:"\f800"}.ti-adjustments-down:before{content:"\f801"}.ti-adjustments-exclamation:before{content:"\f802"}.ti-adjustments-filled:before{content:"\f6ec"}.ti-adjustments-heart:before{content:"\f803"}.ti-adjustments-horizontal:before{content:"\ec38"}.ti-adjustments-minus:before{content:"\f804"}.ti-adjustments-off:before{content:"\f0a8"}.ti-adjustments-pause:before{content:"\f805"}.ti-adjustments-pin:before{content:"\f806"}.ti-adjustments-plus:before{content:"\f807"}.ti-adjustments-question:before{content:"\f808"}.ti-adjustments-search:before{content:"\f809"}.ti-adjustments-share:before{content:"\f80a"}.ti-adjustments-star:before{content:"\f80b"}.ti-adjustments-up:before{content:"\f80c"}.ti-adjustments-x:before{content:"\f80d"}.ti-aerial-lift:before{content:"\edfe"}.ti-affiliate:before{content:"\edff"}.ti-affiliate-filled:before{content:"\f6ed"}.ti-ai:before{content:"\fee7"}.ti-air-balloon:before{content:"\f4a6"}.ti-air-conditioning:before{content:"\f3a2"}.ti-air-conditioning-disabled:before{content:"\f542"}.ti-air-traffic-control:before{content:"\fb01"}.ti-alarm:before{content:"\ea04"}.ti-alarm-average:before{content:"\fc9e"}.ti-alarm-filled:before{content:"\f709"}.ti-alarm-minus:before{content:"\f630"}.ti-alarm-minus-filled:before{content:"\f70a"}.ti-alarm-off:before{content:"\f0a9"}.ti-alarm-plus:before{content:"\f631"}.ti-alarm-plus-filled:before{content:"\f70b"}.ti-alarm-snooze:before{content:"\f632"}.ti-alarm-snooze-filled:before{content:"\f70c"}.ti-album:before{content:"\f022"}.ti-album-off:before{content:"\f3b9"}.ti-alert-circle:before{content:"\ea05"}.ti-alert-circle-filled:before{content:"\f6ee"}.ti-alert-circle-off:before{content:"\fc65"}.ti-alert-hexagon:before{content:"\f80e"}.ti-alert-hexagon-filled:before{content:"\fa34"}.ti-alert-hexagon-off:before{content:"\fc66"}.ti-alert-octagon:before{content:"\ecc6"}.ti-alert-octagon-filled:before{content:"\f6ef"}.ti-alert-small:before{content:"\f80f"}.ti-alert-small-off:before{content:"\fc67"}.ti-alert-square:before{content:"\f811"}.ti-alert-square-filled:before{content:"\fa35"}.ti-alert-square-rounded:before{content:"\f810"}.ti-alert-square-rounded-filled:before{content:"\fa36"}.ti-alert-square-rounded-off:before{content:"\fc68"}.ti-alert-triangle:before{content:"\ea06"}.ti-alert-triangle-filled:before{content:"\f6f0"}.ti-alert-triangle-off:before{content:"\fc69"}.ti-alien:before{content:"\ebde"}.ti-alien-filled:before{content:"\f70d"}.ti-align-box-bottom-center:before{content:"\f530"}.ti-align-box-bottom-center-filled:before{content:"\f70e"}.ti-align-box-bottom-left:before{content:"\f531"}.ti-align-box-bottom-left-filled:before{content:"\f70f"}.ti-align-box-bottom-right:before{content:"\f532"}.ti-align-box-bottom-right-filled:before{content:"\f710"}.ti-align-box-center-bottom:before{content:"\facb"}.ti-align-box-center-middle:before{content:"\f79f"}.ti-align-box-center-middle-filled:before{content:"\f7d4"}.ti-align-box-center-stretch:before{content:"\facc"}.ti-align-box-center-top:before{content:"\facd"}.ti-align-box-left-bottom:before{content:"\f533"}.ti-align-box-left-bottom-filled:before{content:"\f711"}.ti-align-box-left-middle:before{content:"\f534"}.ti-align-box-left-middle-filled:before{content:"\f712"}.ti-align-box-left-stretch:before{content:"\face"}.ti-align-box-left-top:before{content:"\f535"}.ti-align-box-left-top-filled:before{content:"\f713"}.tLine truncated
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
/*!
* Tabler Icons 3.6.0 by tabler - https://tabler.io
* License - https://github.com/tabler/tabler-icons/blob/master/LICENSE
*/@font-face{font-family:"tabler-icons";font-style:normal;font-weight:400;src:url("./fonts/tabler-icons.woff2?v3.6.0") format("woff2"),url("./fonts/tabler-icons.woff?") format("woff"),url("./fonts/tabler-icons.ttf?v3.6.0") format("truetype")}.ti{font-family:"tabler-icons" !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ti-a-b:before{content:"\ec36"}.ti-a-b-2:before{content:"\f25f"}.ti-a-b-off:before{content:"\f0a6"}.ti-abacus:before{content:"\f05c"}.ti-abacus-off:before{content:"\f3b6"}.ti-abc:before{content:"\f567"}.ti-access-point:before{content:"\ed1b"}.ti-access-point-off:before{content:"\ed1a"}.ti-accessible:before{content:"\eba9"}.ti-accessible-filled:before{content:"\f6ea"}.ti-accessible-off:before{content:"\f0a7"}.ti-activity:before{content:"\ed23"}.ti-activity-heartbeat:before{content:"\f0db"}.ti-ad:before{content:"\ea02"}.ti-ad-2:before{content:"\ef1f"}.ti-ad-circle:before{content:"\f79e"}.ti-ad-circle-filled:before{content:"\f7d3"}.ti-ad-circle-off:before{content:"\f79d"}.ti-ad-filled:before{content:"\f6eb"}.ti-ad-off:before{content:"\f3b7"}.ti-address-book:before{content:"\f021"}.ti-address-book-off:before{content:"\f3b8"}.ti-adjustments:before{content:"\ea03"}.ti-adjustments-alt:before{content:"\ec37"}.ti-adjustments-bolt:before{content:"\f7fb"}.ti-adjustments-cancel:before{content:"\f7fc"}.ti-adjustments-check:before{content:"\f7fd"}.ti-adjustments-code:before{content:"\f7fe"}.ti-adjustments-cog:before{content:"\f7ff"}.ti-adjustments-dollar:before{content:"\f800"}.ti-adjustments-down:before{content:"\f801"}.ti-adjustments-exclamation:before{content:"\f802"}.ti-adjustments-filled:before{content:"\f6ec"}.ti-adjustments-heart:before{content:"\f803"}.ti-adjustments-horizontal:before{content:"\ec38"}.ti-adjustments-minus:before{content:"\f804"}.ti-adjustments-off:before{content:"\f0a8"}.ti-adjustments-pause:before{content:"\f805"}.ti-adjustments-pin:before{content:"\f806"}.ti-adjustments-plus:before{content:"\f807"}.ti-adjustments-question:before{content:"\f808"}.ti-adjustments-search:before{content:"\f809"}.ti-adjustments-share:before{content:"\f80a"}.ti-adjustments-star:before{content:"\f80b"}.ti-adjustments-up:before{content:"\f80c"}.ti-adjustments-x:before{content:"\f80d"}.ti-aerial-lift:before{content:"\edfe"}.ti-affiliate:before{content:"\edff"}.ti-affiliate-filled:before{content:"\f6ed"}.ti-ai:before{content:"\fee7"}.ti-air-balloon:before{content:"\f4a6"}.ti-air-conditioning:before{content:"\f3a2"}.ti-air-conditioning-disabled:before{content:"\f542"}.ti-air-traffic-control:before{content:"\fb01"}.ti-alarm:before{content:"\ea04"}.ti-alarm-average:before{content:"\fc9e"}.ti-alarm-filled:before{content:"\f709"}.ti-alarm-minus:before{content:"\f630"}.ti-alarm-minus-filled:before{content:"\f70a"}.ti-alarm-off:before{content:"\f0a9"}.ti-alarm-plus:before{content:"\f631"}.ti-alarm-plus-filled:before{content:"\f70b"}.ti-alarm-snooze:before{content:"\f632"}.ti-alarm-snooze-filled:before{content:"\f70c"}.ti-album:before{content:"\f022"}.ti-album-off:before{content:"\f3b9"}.ti-alert-circle:before{content:"\ea05"}.ti-alert-circle-filled:before{content:"\f6ee"}.ti-alert-circle-off:before{content:"\fc65"}.ti-alert-hexagon:before{content:"\f80e"}.ti-alert-hexagon-filled:before{content:"\fa34"}.ti-alert-hexagon-off:before{content:"\fc66"}.ti-alert-octagon:before{content:"\ecc6"}.ti-alert-octagon-filled:before{content:"\f6ef"}.ti-alert-small:before{content:"\f80f"}.ti-alert-small-off:before{content:"\fc67"}.ti-alert-square:before{content:"\f811"}.ti-alert-square-filled:before{content:"\fa35"}.ti-alert-square-rounded:before{content:"\f810"}.ti-alert-square-rounded-filled:before{content:"\fa36"}.ti-alert-square-rounded-off:before{content:"\fc68"}.ti-alert-triangle:before{content:"\ea06"}.ti-alert-triangle-filled:before{content:"\f6f0"}.ti-alert-triangle-off:before{content:"\fc69"}.ti-alien:before{content:"\ebde"}.ti-alien-filled:before{content:"\f70d"}.ti-align-box-bottom-center:before{content:"\f530"}.ti-align-box-bottom-center-filled:before{content:"\f70e"}.ti-align-box-bottom-left:before{content:"\f531"}.ti-align-box-bottom-left-filled:before{content:"\f70f"}.ti-align-box-bottom-right:before{content:"\f532"}.ti-align-box-bottom-right-filled:before{content:"\f710"}.ti-align-box-center-bottom:before{content:"\facb"}.ti-align-box-center-middle:before{content:"\f79f"}.ti-align-box-center-middle-filled:before{content:"\f7d4"}.ti-align-box-center-stretch:before{content:"\facc"}.ti-align-box-center-top:before{content:"\facd"}.ti-align-box-left-bottom:before{content:"\f533"}.ti-align-box-left-bottom-filled:before{content:"\f711"}.ti-align-box-left-middle:before{content:"\f534"}.ti-align-box-left-middle-filled:before{content:"\f712"}.ti-align-box-left-stretch:before{content:"\face"}.ti-align-box-left-top:before{content:"\f535"}.ti-align-box-left-top-filled:before{content:"\f713"}.tLine truncated
*/@font-face{font-family:"tabler-icons";font-style:normal;font-weight:400;font-display:swap;src:url("./fonts/tabler-icons.woff2?v3.6.0") format("woff2")}.ti{font-family:"tabler-icons" !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ti-alert-circle:before{content:"\ea05"}.ti-alert-triangle:before{content:"\ea06"}.ti-arrow-left:before{content:"\ea19"}.ti-bolt:before{content:"\ea38"}.ti-book:before{content:"\ea39"}.ti-brand-docker:before{content:"\edca"}.ti-brand-git:before{content:"\ef6f"}.ti-calendar:before{content:"\ea53"}.ti-calendar-week:before{content:"\fd30"}.ti-camera:before{content:"\ea54"}.ti-check:before{content:"\ea5e"}.ti-chevron-left:before{content:"\ea60"}.ti-chevron-right:before{content:"\ea61"}.ti-clock:before{content:"\ea70"}.ti-clock-check:before{content:"\f7c1"}.ti-clock-cog:before{content:"\f7c2"}.ti-clock-edit:before{content:"\f547"}.ti-clock-hour-1:before{content:"\f313"}.ti-clock-minus:before{content:"\f848"}.ti-clock-off:before{content:"\f0cf"}.ti-clock-pause:before{content:"\f548"}.ti-clock-play:before{content:"\f549"}.ti-clock-plus:before{content:"\f7c5"}.ti-copy:before{content:"\ea7a"}.ti-dashboard:before{content:"\ea87"}.ti-database-export:before{content:"\ee6e"}.ti-device-floppy:before{content:"\eb62"}.ti-download:before{content:"\ea96"}.ti-edit:before{content:"\ea98"}.ti-eye:before{content:"\ea9a"}.ti-eye-off:before{content:"\ecf0"}.ti-file-download:before{content:"\ea9d"}.ti-file-import:before{content:"\edea"}.ti-folder-cog:before{content:"\f910"}.ti-folder-minus:before{content:"\eaaa"}.ti-folder-plus:before{content:"\eaab"}.ti-folders:before{content:"\eaae"}.ti-folders-off:before{content:"\f133"}.ti-history:before{content:"\ebea"}.ti-hourglass:before{content:"\ef93"}.ti-info-circle:before{content:"\eac5"}.ti-key:before{content:"\eac7"}.ti-key-off:before{content:"\f14b"}.ti-list:before{content:"\eb6b"}.ti-list-details:before{content:"\ef40"}.ti-loader:before{content:"\eca3"}.ti-lock:before{content:"\eae2"}.ti-lock-access:before{content:"\eeaa"}.ti-lock-check:before{content:"\f926"}.ti-lock-exclamation:before{content:"\f92b"}.ti-lock-open:before{content:"\eae1"}.ti-login:before{content:"\eba7"}.ti-logout:before{content:"\eba8"}.ti-mail:before{content:"\eae5"}.ti-mail-check:before{content:"\f939"}.ti-mail-cog:before{content:"\f93b"}.ti-mail-forward:before{content:"\eeac"}.ti-mail-off:before{content:"\f15a"}.ti-menu-2:before{content:"\ec42"}.ti-mood-empty:before{content:"\eeb5"}.ti-moon:before{content:"\eaf8"}.ti-palette:before{content:"\eb01"}.ti-photo:before{content:"\eb0a"}.ti-player-pause:before{content:"\ed45"}.ti-player-play:before{content:"\ed46"}.ti-plug:before{content:"\ebd9"}.ti-plus:before{content:"\eb0b"}.ti-refresh:before{content:"\eb13"}.ti-repeat:before{content:"\eb72"}.ti-send:before{content:"\eb1e"}.ti-send-off:before{content:"\f429"}.ti-server:before{content:"\eb1f"}.ti-server-cog:before{content:"\f321"}.ti-server-off:before{content:"\f19e"}.ti-settings:before{content:"\eb20"}.ti-shield:before{content:"\eb24"}.ti-shield-check:before{content:"\eb22"}.ti-shield-cog:before{content:"\f9c3"}.ti-shield-lock:before{content:"\ed58"}.ti-shield-off:before{content:"\ecf8"}.ti-shield-plus:before{content:"\f9cb"}.ti-sun:before{content:"\eb30"}.ti-terminal-2:before{content:"\ebef"}.ti-trash:before{content:"\eb41"}.ti-upload:before{content:"\eb47"}.ti-user:before{content:"\eb4d"}.ti-user-check:before{content:"\eb49"}.ti-user-edit:before{content:"\f7cc"}.ti-user-minus:before{content:"\eb4a"}.ti-user-off:before{content:"\ecf9"}.ti-user-plus:before{content:"\eb4b"}.ti-users:before{content:"\ebf2"}.ti-world:before{content:"\eb54"}
+2 -2
View File
@@ -8,7 +8,7 @@
<title>{{.Title}} - {{appName}}</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<!-- Preload icon font to prevent re-decode lag on tab restore -->
<link rel="preload" href="/static/css/fonts/tabler-icons.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/static/css/fonts/tabler-icons.woff2?v3.6.0" as="font" type="font/woff2" crossorigin>
<!-- Resolve theme BEFORE loading CSS to prevent FOUC (flash of unstyled content) -->
<script>
(function() {
@@ -160,7 +160,7 @@
[data-bs-theme="dark"] .page-body { background: #0F1829; }
html[data-bs-theme="dark"],
html[data-bs-theme="dark"] body { background-color: #0F1829 !important; }
.page-body { content-visibility: auto; contain-intrinsic-size: auto 500px; }
/* content-visibility removed – causes Firefox to freeze/re-layout on tab hover */
/* ── Narrower dashboard stat cards ── */
.stat-card-narrow { max-width: 220px; }