diff --git a/aifont.py b/aifont.py
deleted file mode 100755
index 5898faa..0000000
--- a/aifont.py
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/env python3
-import subprocess, sys, os
-
-# download MesloLGS NF
-meslo = "/tmp/MesloLGS NF Regular.ttf"
-if not os.path.exists(meslo):
- meslo_url = "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/Meslo.tar.xz"
- subprocess.run(["curl", "-sL", "-o", "/tmp/Meslo.tar.xz", meslo_url], check=True)
- subprocess.run(["tar", "xf", "/tmp/Meslo.tar.xz", "-C", "/tmp", "MesloLGSNerdFont-Regular.ttf"], check=True)
- os.rename("/tmp/MesloLGSNerdFont-Regular.ttf", meslo)
-
-# download SVG icons
-base = "https://git.syui.ai/ai/app/raw/branch/main/icon"
-for name in ["ai.svg", "syui.svg", "bluesky.svg"]:
- subprocess.run(["curl", "-sL", "-o", f"/tmp/{name}", f"{base}/{name}"], check=True)
-
-subprocess.run(["fontforge", "-script", "/dev/stdin"], input=b"""
-import fontforge
-
-icons = fontforge.font()
-icons.em = 1024
-
-scale = 200
-
-glyph = icons.createChar(0xE001, "ai")
-glyph.importOutlines("/tmp/ai.svg")
-bb = glyph.boundingBox()
-glyph.transform((scale/100.0, 0, 0, scale/100.0, 0, 0))
-glyph.width = 1024
-
-glyph = icons.createChar(0xE002, "syui")
-glyph.importOutlines("/tmp/syui.svg")
-glyph.transform((scale/100.0, 0, 0, scale/100.0, 0, 0))
-glyph.width = 1024
-
-glyph = icons.createChar(0xE003, "bluesky")
-glyph.importOutlines("/tmp/bluesky.svg")
-# center vertically: move to baseline
-bb = glyph.boundingBox()
-yshift = -bb[1] # move bottom to y=0
-glyph.transform((1, 0, 0, 1, 0, yshift))
-s = scale / 100.0
-glyph.transform((s, 0, 0, s, 0, 0))
-glyph.width = 1024
-
-icons.generate("/tmp/icons.ttf")
-icons.close()
-
-font = fontforge.open("/tmp/MesloLGS NF Regular.ttf")
-font.selection.select(0xE001)
-font.clear()
-font.selection.select(0xE002)
-font.clear()
-font.selection.select(0xE003)
-font.clear()
-font.selection.none()
-
-font.mergeFonts("/tmp/icons.ttf")
-
-font.fontname = "aifont"
-font.familyname = "aifont"
-font.fullname = "aifont"
-font.appendSFNTName("English (US)", "Family", "aifont")
-font.appendSFNTName("English (US)", "SubFamily", "Regular")
-font.appendSFNTName("English (US)", "UniqueID", "aifont-regular")
-font.appendSFNTName("English (US)", "Fullname", "aifont")
-font.appendSFNTName("English (US)", "PostScriptName", "aifont")
-font.appendSFNTName("English (US)", "Preferred Family", "aifont")
-font.appendSFNTName("English (US)", "Preferred Styles", "Regular")
-
-font.generate("/tmp/aifont.ttf")
-font.close()
-print("done: /tmp/aifont.ttf")
-""", check=True)
diff --git a/aifont.ttf b/aifont.ttf
index 2b3a5e7..c69e14b 100644
Binary files a/aifont.ttf and b/aifont.ttf differ
diff --git a/py/arch_cut.py b/py/arch_cut.py
new file mode 100644
index 0000000..65669f7
--- /dev/null
+++ b/py/arch_cut.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env fontforge
+"""
+Apply arch cut to uppercase A-Z and digit 2-9.
+Arch = planet rising from below. The arc curves UPWARD into the bottom of the glyph.
+The cut area is removed (transparent), not filled.
+"""
+import fontforge
+import os
+import math
+
+SRC_DIR = "/tmp/aifont_meslo"
+DST_DIR = "/tmp/aifont_arch"
+os.makedirs(DST_DIR, exist_ok=True)
+
+chars = []
+for c in range(ord('A'), ord('Z')+1):
+ chars.append(chr(c))
+for c in range(ord('0'), ord('9')+1):
+ chars.append(chr(c))
+
+# Skip arch for round-bottom characters
+skip_arch = set()
+
+WIDTH = 602 # Meslo monospace width
+
+workfont = fontforge.font()
+workfont.em = 1000
+workfont.encoding = "UnicodeFull"
+
+for g in chars:
+ cp = ord(g)
+ src = os.path.join(SRC_DIR, g + ".svg")
+ if not os.path.exists(src):
+ continue
+
+ glyph = workfont.createChar(cp, g)
+ glyph.importOutlines(src)
+ glyph.width = WIDTH
+
+ if g not in skip_arch:
+ bb = glyph.boundingBox()
+ xmin, ymin, xmax, ymax = bb
+ h = ymax - ymin
+ w = xmax - xmin
+
+ # How much of the glyph bottom to cut (15%)
+ cut_depth = h * 0.07
+
+ # Large arc radius - the "planet" rising from below
+ # Larger radius = gentler curve
+ radius = w * 2.5
+
+ # The arc center is below the glyph
+ cx = (xmin + xmax) / 2.0
+ # Position so the top of the arc reaches cut_depth into the glyph
+ cy = ymin - radius + cut_depth
+
+ # We need to intersect the glyph with the area ABOVE the arc.
+ # Strategy: create a large clockwise (additive) shape representing
+ # "everything above the arc line", then intersect with the glyph.
+
+ # Step 1: Clear glyph, we'll rebuild via intersection
+ # Save current contours by using a temp approach
+
+ # Better strategy using fontforge:
+ # 1. Create a second glyph with a huge filled area that has the arc as its bottom edge
+ # 2. Use intersect to keep only where glyph and mask overlap
+
+ # Create mask glyph: a large rectangle with arc-shaped bottom
+ mask_cp = 0xE900
+ mask = workfont.createChar(mask_cp, "mask")
+ mask.clear()
+
+ margin = 200
+ left = xmin - margin
+ right = xmax + margin
+ top = ymax + margin
+
+ # The arc: we need points along the arc from left to right
+ # Arc equation: (x - cx)^2 + (y - cy)^2 = radius^2
+ # y = cy + sqrt(radius^2 - (x-cx)^2) (upper half of circle)
+
+ # Calculate arc endpoints
+ def arc_y(x):
+ dx = x - cx
+ if abs(dx) > radius:
+ return ymin
+ return cy + math.sqrt(radius * radius - dx * dx)
+
+ # Generate points along the arc
+ n_points = 40
+ arc_points = []
+ for idx in range(n_points + 1):
+ x = left + (right - left) * idx / n_points
+ y = arc_y(x)
+ arc_points.append((x, y))
+
+ pen = mask.glyphPen()
+ # Draw clockwise: top-left -> top-right -> arc from right to left -> close
+ pen.moveTo((left, top))
+ pen.lineTo((right, top))
+ # Go down right side to arc start
+ pen.lineTo((right, arc_points[-1][1]))
+ # Follow arc from right to left (reverse order)
+ for idx in range(n_points - 1, -1, -1):
+ pen.lineTo(arc_points[idx])
+ pen.closePath()
+ pen = None
+
+ # Now intersect: keep only the part of glyph inside the mask
+ glyph.correctDirection()
+ mask.correctDirection()
+
+ # Copy mask into glyph's layer and intersect
+ workfont.selection.select(mask_cp)
+ workfont.copy()
+ workfont.selection.select(cp)
+ workfont.pasteInto()
+ glyph.intersect()
+
+ # Clean up mask
+ mask.clear()
+
+ dst = os.path.join(DST_DIR, g + ".svg")
+ glyph.export(dst)
+ print("Done: " + g)
+
+print("\nAll done! Results: " + DST_DIR)
diff --git a/py/build_font.py b/py/build_font.py
new file mode 100644
index 0000000..2c92f61
--- /dev/null
+++ b/py/build_font.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env fontforge
+"""
+Build aifont.ttf:
+- Base: Meslo LGS NF Regular
+- Replace A-Z with arch-cut versions
+- Replace 0-9 with arch-cut versions
+- Replace a, i, o, s with custom ai-world designs
+- Keep everything else (other lowercase, symbols, nerd font icons)
+"""
+import fontforge
+import os
+
+BASE_FONT = "/tmp/MesloLGS NF Regular.ttf"
+UPPER_DIR = "/tmp/aifont_build/upper"
+DIGIT_DIR = "/tmp/aifont_build/digit"
+LOWER_DIR = "/tmp/aifont_build/lower"
+OUTPUT = "/tmp/aifont.ttf"
+
+font = fontforge.open(BASE_FONT)
+
+# Replace uppercase A-Z
+for c in range(ord('A'), ord('Z')+1):
+ g = chr(c)
+ svg = os.path.join(UPPER_DIR, g + ".svg")
+ if os.path.exists(svg):
+ glyph = font[c]
+ w = glyph.width
+ glyph.clear()
+ glyph.importOutlines(svg)
+ glyph.width = w
+ glyph.correctDirection()
+ print("Upper: " + g)
+
+# Replace digits 0-9
+for c in range(ord('0'), ord('9')+1):
+ g = chr(c)
+ svg = os.path.join(DIGIT_DIR, g + ".svg")
+ if os.path.exists(svg):
+ glyph = font[c]
+ w = glyph.width
+ glyph.clear()
+ glyph.importOutlines(svg)
+ glyph.width = w
+ glyph.correctDirection()
+ print("Digit: " + g)
+
+# Replace lowercase a, i, o, s
+# These have different coordinate systems, so center them after import
+import psMat
+
+for g in ['a', 'i']:
+ c = ord(g)
+ svg = os.path.join(LOWER_DIR, g + ".svg")
+ if os.path.exists(svg):
+ glyph = font[c]
+ target_w = glyph.width
+ glyph.clear()
+ glyph.importOutlines(svg)
+ glyph.correctDirection()
+
+ # Get bounding box and center horizontally
+ bb = glyph.boundingBox()
+ xmin, ymin, xmax, ymax = bb
+ glyph_w = xmax - xmin
+ glyph_h = ymax - ymin
+
+ # Scale per glyph
+ if g == 'i':
+ target_h = 1500
+ elif g == 'a':
+ target_h = 1400
+ else:
+ target_h = 1200
+ if glyph_h > 0:
+ scale = target_h / glyph_h
+ glyph.transform(psMat.scale(scale))
+ bb = glyph.boundingBox()
+ xmin, ymin, xmax, ymax = bb
+ glyph_w = xmax - xmin
+
+ # Center horizontally within the glyph width
+ x_offset = (target_w - glyph_w) / 2.0 - xmin
+ # Align baseline
+ y_offset = -ymin
+ glyph.transform(psMat.translate(x_offset, y_offset))
+ glyph.width = target_w
+
+ # Thin down a, o, s (not i) - scale horizontally to 75%
+ if g in ['a', 'o', 's']:
+ bb = glyph.boundingBox()
+ xmin, ymin, xmax, ymax = bb
+ cx = (xmin + xmax) / 2.0
+ # Scale X to 75%, keep Y
+ glyph.transform(psMat.compose(
+ psMat.translate(-cx, 0),
+ psMat.compose(
+ psMat.scale(0.75, 1.0),
+ psMat.translate(cx, 0)
+ )
+ ))
+ # Re-center
+ bb = glyph.boundingBox()
+ xmin, ymin, xmax, ymax = bb
+ glyph_w = xmax - xmin
+ x_offset = (target_w - glyph_w) / 2.0 - xmin
+ glyph.transform(psMat.translate(x_offset, 0))
+ glyph.width = target_w
+
+ print("Lower: " + g)
+
+# Re-import custom icons at E001-E003 (same approach as original aifont.py)
+# Build icon font separately, then merge
+ICON_DIR = "/tmp/aifont_build"
+scale = 200 # same as original
+
+icons_font = fontforge.font()
+icons_font.em = 1024
+
+for cp, name, has_yshift in [(0xE001, "ai.svg", False), (0xE002, "syui.svg", False), (0xE003, "bluesky.svg", True)]:
+ svg = os.path.join(ICON_DIR, name)
+ if os.path.exists(svg):
+ glyph = icons_font.createChar(cp, name.replace(".svg", ""))
+ glyph.importOutlines(svg)
+ if has_yshift:
+ bb = glyph.boundingBox()
+ yshift = -bb[1]
+ glyph.transform((1, 0, 0, 1, 0, yshift))
+ s = scale / 100.0
+ glyph.transform((s, 0, 0, s, 0, 0))
+ glyph.width = 1024
+ print("Icon: " + name)
+
+icons_font.generate("/tmp/aifont_icons.ttf")
+icons_font.close()
+
+# Clear existing icon slots and merge
+for cp in [0xE001, 0xE002, 0xE003]:
+ font.selection.select(cp)
+ font.clear()
+font.selection.none()
+font.mergeFonts("/tmp/aifont_icons.ttf")
+
+# Set font metadata
+font.fontname = "aifont"
+font.familyname = "aifont"
+font.fullname = "aifont"
+font.appendSFNTName("English (US)", "Family", "aifont")
+font.appendSFNTName("English (US)", "SubFamily", "Regular")
+font.appendSFNTName("English (US)", "UniqueID", "aifont-regular")
+font.appendSFNTName("English (US)", "Fullname", "aifont")
+font.appendSFNTName("English (US)", "PostScriptName", "aifont")
+font.appendSFNTName("English (US)", "Preferred Family", "aifont")
+font.appendSFNTName("English (US)", "Preferred Styles", "Regular")
+
+font.generate(OUTPUT)
+print("\nDone: " + OUTPUT)
diff --git a/svg/digit/0.svg b/svg/digit/0.svg
new file mode 100644
index 0000000..bb0585b
--- /dev/null
+++ b/svg/digit/0.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/svg/digit/1.svg b/svg/digit/1.svg
new file mode 100644
index 0000000..7c3fc4e
--- /dev/null
+++ b/svg/digit/1.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/svg/digit/2.svg b/svg/digit/2.svg
new file mode 100644
index 0000000..20fe9e8
--- /dev/null
+++ b/svg/digit/2.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/svg/digit/3.svg b/svg/digit/3.svg
new file mode 100644
index 0000000..db41e16
--- /dev/null
+++ b/svg/digit/3.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/svg/digit/4.svg b/svg/digit/4.svg
new file mode 100644
index 0000000..1b50021
--- /dev/null
+++ b/svg/digit/4.svg
@@ -0,0 +1,6 @@
+
+
+
diff --git a/svg/digit/5.svg b/svg/digit/5.svg
new file mode 100644
index 0000000..c2fbd33
--- /dev/null
+++ b/svg/digit/5.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/svg/digit/6.svg b/svg/digit/6.svg
new file mode 100644
index 0000000..0eaab68
--- /dev/null
+++ b/svg/digit/6.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/svg/digit/7.svg b/svg/digit/7.svg
new file mode 100644
index 0000000..a893510
--- /dev/null
+++ b/svg/digit/7.svg
@@ -0,0 +1,6 @@
+
+
+
diff --git a/svg/digit/8.svg b/svg/digit/8.svg
new file mode 100644
index 0000000..5ee31c1
--- /dev/null
+++ b/svg/digit/8.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/svg/digit/9.svg b/svg/digit/9.svg
new file mode 100644
index 0000000..550be1a
--- /dev/null
+++ b/svg/digit/9.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/svg/digit/Q.svg b/svg/digit/Q.svg
new file mode 100644
index 0000000..e032853
--- /dev/null
+++ b/svg/digit/Q.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/svg/digit/U.svg b/svg/digit/U.svg
new file mode 100644
index 0000000..0327b37
--- /dev/null
+++ b/svg/digit/U.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/svg/lower/a.svg b/svg/lower/a.svg
new file mode 100644
index 0000000..ebdc0b7
--- /dev/null
+++ b/svg/lower/a.svg
@@ -0,0 +1,19 @@
+
diff --git a/svg/lower/i.svg b/svg/lower/i.svg
new file mode 100644
index 0000000..586ee07
--- /dev/null
+++ b/svg/lower/i.svg
@@ -0,0 +1,20 @@
+
diff --git a/svg/lower/o.svg b/svg/lower/o.svg
new file mode 100644
index 0000000..b8d3da6
--- /dev/null
+++ b/svg/lower/o.svg
@@ -0,0 +1,17 @@
+
diff --git a/svg/lower/s.svg b/svg/lower/s.svg
new file mode 100644
index 0000000..0e92b81
--- /dev/null
+++ b/svg/lower/s.svg
@@ -0,0 +1,32 @@
+
+
+
diff --git a/svg/upper/0.svg b/svg/upper/0.svg
new file mode 100644
index 0000000..bb0585b
--- /dev/null
+++ b/svg/upper/0.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/svg/upper/6.svg b/svg/upper/6.svg
new file mode 100644
index 0000000..0eaab68
--- /dev/null
+++ b/svg/upper/6.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/svg/upper/8.svg b/svg/upper/8.svg
new file mode 100644
index 0000000..5ee31c1
--- /dev/null
+++ b/svg/upper/8.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/svg/upper/9.svg b/svg/upper/9.svg
new file mode 100644
index 0000000..550be1a
--- /dev/null
+++ b/svg/upper/9.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/svg/upper/A.svg b/svg/upper/A.svg
new file mode 100644
index 0000000..a839f20
--- /dev/null
+++ b/svg/upper/A.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/svg/upper/B.svg b/svg/upper/B.svg
new file mode 100644
index 0000000..1159e11
--- /dev/null
+++ b/svg/upper/B.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/svg/upper/C.svg b/svg/upper/C.svg
new file mode 100644
index 0000000..5873f4f
--- /dev/null
+++ b/svg/upper/C.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/svg/upper/D.svg b/svg/upper/D.svg
new file mode 100644
index 0000000..44c085a
--- /dev/null
+++ b/svg/upper/D.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/svg/upper/E.svg b/svg/upper/E.svg
new file mode 100644
index 0000000..1c58b88
--- /dev/null
+++ b/svg/upper/E.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/svg/upper/F.svg b/svg/upper/F.svg
new file mode 100644
index 0000000..ea2c9d1
--- /dev/null
+++ b/svg/upper/F.svg
@@ -0,0 +1,6 @@
+
+
+
diff --git a/svg/upper/G.svg b/svg/upper/G.svg
new file mode 100644
index 0000000..9d4b1c9
--- /dev/null
+++ b/svg/upper/G.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/svg/upper/H.svg b/svg/upper/H.svg
new file mode 100644
index 0000000..964cf6d
--- /dev/null
+++ b/svg/upper/H.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/svg/upper/I.svg b/svg/upper/I.svg
new file mode 100644
index 0000000..3c12064
--- /dev/null
+++ b/svg/upper/I.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/svg/upper/J.svg b/svg/upper/J.svg
new file mode 100644
index 0000000..ad586f1
--- /dev/null
+++ b/svg/upper/J.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/svg/upper/K.svg b/svg/upper/K.svg
new file mode 100644
index 0000000..3d78fe8
--- /dev/null
+++ b/svg/upper/K.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/svg/upper/L.svg b/svg/upper/L.svg
new file mode 100644
index 0000000..4c17464
--- /dev/null
+++ b/svg/upper/L.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/svg/upper/M.svg b/svg/upper/M.svg
new file mode 100644
index 0000000..f12b362
--- /dev/null
+++ b/svg/upper/M.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/svg/upper/N.svg b/svg/upper/N.svg
new file mode 100644
index 0000000..dcb8576
--- /dev/null
+++ b/svg/upper/N.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/svg/upper/O.svg b/svg/upper/O.svg
new file mode 100644
index 0000000..7d5ae41
--- /dev/null
+++ b/svg/upper/O.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/svg/upper/P.svg b/svg/upper/P.svg
new file mode 100644
index 0000000..b066463
--- /dev/null
+++ b/svg/upper/P.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/svg/upper/Q.svg b/svg/upper/Q.svg
new file mode 100644
index 0000000..e032853
--- /dev/null
+++ b/svg/upper/Q.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/svg/upper/R.svg b/svg/upper/R.svg
new file mode 100644
index 0000000..5740ab8
--- /dev/null
+++ b/svg/upper/R.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/svg/upper/S.svg b/svg/upper/S.svg
new file mode 100644
index 0000000..da4512d
--- /dev/null
+++ b/svg/upper/S.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/svg/upper/T.svg b/svg/upper/T.svg
new file mode 100644
index 0000000..69a5444
--- /dev/null
+++ b/svg/upper/T.svg
@@ -0,0 +1,6 @@
+
+
+
diff --git a/svg/upper/U.svg b/svg/upper/U.svg
new file mode 100644
index 0000000..0327b37
--- /dev/null
+++ b/svg/upper/U.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/svg/upper/V.svg b/svg/upper/V.svg
new file mode 100644
index 0000000..adc96e2
--- /dev/null
+++ b/svg/upper/V.svg
@@ -0,0 +1,6 @@
+
+
+
diff --git a/svg/upper/W.svg b/svg/upper/W.svg
new file mode 100644
index 0000000..35f0d75
--- /dev/null
+++ b/svg/upper/W.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/svg/upper/X.svg b/svg/upper/X.svg
new file mode 100644
index 0000000..10d1866
--- /dev/null
+++ b/svg/upper/X.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/svg/upper/Y.svg b/svg/upper/Y.svg
new file mode 100644
index 0000000..279acb6
--- /dev/null
+++ b/svg/upper/Y.svg
@@ -0,0 +1,6 @@
+
+
+
diff --git a/svg/upper/Z.svg b/svg/upper/Z.svg
new file mode 100644
index 0000000..dfa6a59
--- /dev/null
+++ b/svg/upper/Z.svg
@@ -0,0 +1,8 @@
+
+
+