# E440 Arc-Dial Scale Generator Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace the M1730 linear-strip scale generator with an E440-style round-dial (arc) scale generator, matching the hand-drawn reference `e440.svg`, across the web app, the three deliverable SVG files, and all docs/infra naming.

**Architecture:** `web/app.py`'s `generate_svg()` is rewritten around polar geometry (a fixed pivot point, fixed rim radius, fixed angular sweep) instead of the old Cartesian geometry (fixed x-range, fixed y-baseline). Ticks and labels are drawn as `<line>`/`<text>`/path elements positioned "as if angle=0" (straight up from the pivot) and then given an SVG `rotate(angle, PIVOT_X, PIVOT_Y)` transform — this reuses the reference file's own rotation trick but around an explicit pivot, which avoids inverse-rotation math. All four scale mappings (linear/√/log/1÷x), both output modes (paths/text), custom labels, and unit markup are ported by substituting angle for x. `range_label` is replaced by independent `left_label`/`right_label` fields (each up to 2 lines). The three deliverable SVGs, README, CLAUDE.md, Dockerfile-adjacent docs, and k8s manifests are all renamed/rewritten from M1730 to E440.

**Tech Stack:** Python 3 / Flask, fontTools (`TTFont`, `SVGPathPen`) for glyph-to-path conversion — both already in `web/requirements.txt`, no new dependencies.

## Global Constraints

- No build, lint, or test tooling exists in this repo (per `CLAUDE.md`) — do not add pytest or any test framework. Verification steps in this plan use plain `python3 -c` / heredoc scripts with `assert`, run directly via Bash.
- All canvas/geometry constants below are measured directly from `e440.svg` (or computed from measurements) — use these exact values, don't re-derive or approximate differently.
- Coordinate convention: all constants are expressed in the **pre-layer-translate local frame** (the same frame `e440.svg`'s own elements use), with the SVG output wrapped in `<g transform="translate(-60.717741,-107.14126)">`. This mirrors the existing M1730 code's convention (`ZERO_X`, `MAX_X`, etc. were also pre-translate).
- Canvas size is fixed: `width="93.824249mm" height="57.215687mm" viewBox="0 0 93.824249 57.215687"`.
- Colors/strokes: ink `#1a1a1a` for ticks/labels/unit/corner text (matches reference), body fill `#f9f9f9` with `#1a1a1a` stroke at width `0.184`, pivot guide circle stroke `#b3b3b3` at width `0.184`, rim arc stroke `#000000` at width `0.484`.
- Geometry constants (local frame, mm):
  - `PIVOT_X = 107.42845`, `PIVOT_Y = 162.62819`
  - `R0 = 43.842751` (rim / inner-tick radius)
  - `ANGLE_MIN = -45.667193`, `ANGLE_MAX = 45.667193` (degrees from vertical, clockwise-positive; low value on the left/negative side)
  - `MAJOR_LEN = 4.7839203`, `MAJOR_W = 0.79658329`
  - `MEDIUM_LEN = 3.1215858`, `MEDIUM_W = 0.37985462`
  - `SMALL_LEN = 1.8121802`, `SMALL_W = 0.27478597`
  - `LABEL_RADIUS = 49.862`
  - `UNIT_X = 107.03512`, `UNIT_Y = 136.64594`
  - `LEFT_X = 73.432251`, `LEFT_Y0 = 145.16051`
  - `RIGHT_X = 142.4485`, `RIGHT_Y0 = 145.32773`
  - `CORNER_LINE_SPACING = 4.4586`, `CORNER_FONT_SIZE = 3.56685`
  - `PIVOT_CIRCLE_R = 1.7287594`
- The `half_height_unit` feature (and its UI toggle) is dropped — it existed only to solve a crowding problem specific to M1730's narrow side panel, which doesn't exist in the E440 layout.
- Point-on-circle formula (verified against the reference to within 0.003mm): `x = PIVOT_X + radius * sin(radians(angle_deg))`, `y = PIVOT_Y - radius * cos(radians(angle_deg))`.

---

### Task 1: Clean up fonts directory and register the DIN font labels/default

**Files:**
- Modify: `fonts/` (remove stray Windows metadata files)
- Modify: `web/app.py:19-26` (`_FONT_LABELS` dict), `web/app.py:51` (`_DEFAULT_FONT`)

**Interfaces:**
- Consumes: nothing new — `_load_fonts()` (`web/app.py:29-47`) already globs `*.ttf` in `fonts/` and builds `_FontData` per file; unchanged.
- Produces: `_DEFAULT_FONT` now resolves to the DIN regular font's key, used by every later task's default `font_key` argument.

- [ ] **Step 1: Remove stray Zone.Identifier files**

```bash
rm -f "fonts/alte-din-1451-mittelschrift.regular.ttf:Zone.Identifier" \
      "fonts/alte-din-1451-mittelschrift.gepraegt.ttf:Zone.Identifier"
ls fonts/
```
Expected: only `.ttf` files remain (6 GOST-family + 2 DIN files).

- [ ] **Step 2: Install `defusedxml` into the venv for later verification steps**

This plan's well-formedness checks (Tasks 2 and 9) parse generated SVGs with `defusedxml` instead of the stdlib `xml.etree.ElementTree`, per this repo's security guidance (stdlib XML parsers are vulnerable to XXE/billion-laughs by default). It's not an `app.py` runtime dependency, just a verification-script tool, so it's installed into the venv but not added to `web/requirements.txt`.

```bash
cd /home/adebaumann/development/44-Scale
source .venv/bin/activate
pip install defusedxml
python3 -c "import defusedxml; print('OK')"
```
Expected: `OK`.

- [ ] **Step 3: Add DIN font display labels and change the default font**

In `web/app.py`, in the `_FONT_LABELS` dict, add two entries (key = filename stem, i.e. `Path('alte-din-1451-mittelschrift.regular.ttf').stem`):

```python
_FONT_LABELS = {
    'GOST_2.30481_type_A':                    'GOST Type A',
    'GOST_2.30481_type_A_Bold':                'GOST Type A Bold',
    'Gost2.30481TypeB':                        'GOST Type B',
    'ISOCPEUR':                                'ISOCPEUR',
    'SIEMENS_GOST_Type_A':                     'Siemens GOST Type A',
    'TGL_31034-1':                              'TGL 31 034',
    'alte-din-1451-mittelschrift.regular':      'Alte DIN 1451 Mittelschrift',
    'alte-din-1451-mittelschrift.gepraegt':     'Alte DIN 1451 Mittelschrift (Gepraegt)',
}
```

Change the default font constant:

```python
_DEFAULT_FONT = 'alte-din-1451-mittelschrift.regular'
```

- [ ] **Step 4: Verify the font loads and is the default**

```bash
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import _FONTS, _DEFAULT_FONT
assert _DEFAULT_FONT in _FONTS, _FONTS.keys()
assert _FONTS[_DEFAULT_FONT].label == 'Alte DIN 1451 Mittelschrift'
print('OK', len(_FONTS), 'fonts loaded')
"
```
Expected: `OK 8 fonts loaded`.

- [ ] **Step 5: Commit**

```bash
git add fonts/ web/app.py
git commit -m "Add DIN 1451 fonts as default; drop stray Zone.Identifier files"
```

---

### Task 2: Rewrite `generate_svg()` skeleton around arc geometry (body + pivot + rim, no ticks/labels yet)

**Files:**
- Modify: `web/app.py` (constants block `web/app.py:53-77`, `generate_svg()` `web/app.py:320-488`, route handler `web/app.py:498-523`)

**Interfaces:**
- Consumes: `_FONTS`, `_DEFAULT_FONT`, `_markup_unit_svg`, `_text_to_paths`, `_fmt` (all unchanged, from earlier in the file).
- Produces: new `generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, small_ticks, label_count, use_paths=True, scale_type='linear', custom_labels=None, label_size=5.1126, font_key=None)` signature that later tasks extend. Produces `_point_at(radius, angle_deg)` helper used by Tasks 3–4.

- [ ] **Step 1: Replace the linear geometry constants with arc constants**

Replace `web/app.py:53-77` (from `# SVG absolute coordinate constants...` through the `_UNIT_BOX_*` block) with:

```python
# SVG absolute coordinate constants (1 user unit = 1 mm), pre-layer-translate
# local frame — the <g> wrapper applies translate(-60.717741,-107.14126).
PIVOT_X, PIVOT_Y = 107.42845, 162.62819
R0 = 43.842751          # rim / inner-tick radius
ANGLE_MIN, ANGLE_MAX = -45.667193, 45.667193   # degrees from vertical, clockwise-positive

MAJOR_LEN,  MAJOR_W  = 4.7839203, 0.79658329
MEDIUM_LEN, MEDIUM_W = 3.1215858, 0.37985462
SMALL_LEN,  SMALL_W  = 1.8121802, 0.27478597

LABEL_RADIUS = 49.862

UNIT_X, UNIT_Y = 107.03512, 136.64594

LEFT_X, LEFT_Y0   = 73.432251, 145.16051
RIGHT_X, RIGHT_Y0 = 142.4485, 145.32773
CORNER_LINE_SPACING = 4.4586
CORNER_FONT_SIZE = 3.56685

PIVOT_CIRCLE_R = 1.7287594

BODY_D = (
    "m 62.162097,107.14126 c -0.800087,0 -1.444356,0.64428 -1.444356,1.44436 v 45.44839 "
    "c 0,0.80008 0.644269,1.44436 1.444356,1.44436 h 7.345288 c 0.902634,0 1.233517,1.12706 "
    "1.233517,1.12706 l 0.09043,3.03909 21.903573,-0.0904 2.226221,-5.98775 c 8.352594,-2.8884 "
    "16.624974,-2.8884 24.817094,0 l 2.22674,5.98775 21.90357,0.0904 0.0904,-3.03909 c 0,0 "
    "0.33088,-1.12706 1.23352,-1.12706 h 7.86567 c 0.80009,0 1.44384,-0.64428 1.44384,-1.44436 "
    "v -45.44839 c 0,-0.80008 -0.64375,-1.44436 -1.44384,-1.44436 z m 29.619898,45.85354 "
    "a 1.3990685,1.3990685 0 0 1 1.398881,1.39939 1.3990685,1.3990685 0 0 1 -1.398881,1.39888 "
    "1.3990685,1.3990685 0 0 1 -1.399398,-1.39888 1.3990685,1.3990685 0 0 1 1.399398,-1.39939 "
    "z m 31.220835,0 a 1.3990685,1.3990685 0 0 1 1.39888,1.39939 1.3990685,1.3990685 0 0 1 "
    "-1.39888,1.39888 1.3990685,1.3990685 0 0 1 -1.3994,-1.39888 1.3990685,1.3990685 0 0 1 "
    "1.3994,-1.39939 z"
)

_UNIT_BOX_X0, _UNIT_BOX_X1 = UNIT_X - 15.0, UNIT_X + 15.0
_UNIT_BOX_Y0, _UNIT_BOX_Y1 = UNIT_Y - 6.0, UNIT_Y + 6.0
_UNIT_MAX_FS = 9.46938021
_UNIT_SCALE_X = 1.0


def _point_at(radius: float, angle_deg: float):
    th = math.radians(angle_deg)
    return PIVOT_X + radius * math.sin(th), PIVOT_Y - radius * math.cos(th)
```

- [ ] **Step 2: Simplify `_markup_unit_svg` to drop the half-height box-override params**

Replace the `_markup_unit_svg` function (originally `web/app.py:248-268`) with:

```python
def _markup_unit_svg(unit: str, fd: _FontData, fill='#1a1a1a') -> str:
    nodes = _parse_markup(unit)
    x0, x1 = _UNIT_BOX_X0, _UNIT_BOX_X1
    y0, y1 = _UNIT_BOX_Y0, _UNIT_BOX_Y1
    xc, yc = (x0 + x1) / 2, (y0 + y1) / 2

    w1 = _measure(nodes, 1.0, fd)
    top1, bot1 = _vbounds(nodes, 1.0, fd)
    fs = _UNIT_MAX_FS
    if w1 > 0:
        fs = min(fs, (x1 - x0) / (w1 * _UNIT_SCALE_X))
    if bot1 - top1 > 0:
        fs = min(fs, (y1 - y0) / (bot1 - top1))

    w = _measure(nodes, fs, fd)
    top, bot = _vbounds(nodes, fs, fd)
    y_base = yc - (top + bot) / 2
    parts, _ = _draw_nodes(nodes, xc - w / 2, y_base, fs, fill, fd)
    cond = f'translate({xc:.5f},0) scale({_UNIT_SCALE_X},1) translate({-xc:.5f},0)'
    return f'<g transform="{cond}">\n{chr(10).join(parts)}\n</g>'
```

(This drops the `box_y0`/`box_y1` parameters and the module-level `_UNIT_BOX_HALF_Y0` constant — search `web/app.py` for `_UNIT_BOX_HALF_Y0` and `half_height_unit` and delete every reference; they no longer exist anywhere after this task.)

- [ ] **Step 3: Delete the now-unused `_unit_font_size` function**

Delete the `_unit_font_size` function entirely (originally `web/app.py:271-275`) — after Task 5 the unit is always rendered via `_markup_unit_svg`, so this fast-path helper becomes dead code.

- [ ] **Step 4: Replace `generate_svg()` with the new skeleton**

Replace the whole `generate_svg` function (originally `web/app.py:320-488`) with:

```python
def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, small_ticks,
                 label_count, use_paths=True, scale_type='linear', custom_labels=None,
                 label_size=5.1126, font_key=None):
    fd = _FONTS.get(font_key) or _FONTS.get(_DEFAULT_FONT) or next(iter(_FONTS.values()))

    font_face = (
        '<defs><style>'
        f'@font-face {{font-family:"{fd.label}";'
        f'src:url("data:font/truetype;base64,{fd.b64}") format("truetype");}}'
        '</style></defs>'
    )

    parts = [
        '<?xml version="1.0" encoding="UTF-8" standalone="no"?>',
        '<svg width="93.824249mm" height="57.215687mm"'
        ' viewBox="0 0 93.824249 57.215687" version="1.1"'
        ' xmlns="http://www.w3.org/2000/svg">',
    ]

    if use_paths:
        parts.append('  <g transform="translate(-60.717741,-107.14126)">')
    else:
        parts += [font_face, '  <g transform="translate(-60.717741,-107.14126)">']

    parts.append(
        f'    <path style="fill:#f9f9f9;stroke:#1a1a1a;stroke-width:0.184;'
        f'stroke-linecap:round;stroke-miterlimit:10" d="{BODY_D}" />'
    )

    p0 = _point_at(R0, ANGLE_MIN)
    pmid = _point_at(R0, 0.0)
    p1 = _point_at(R0, ANGLE_MAX)
    parts.append(
        f'    <path style="fill:none;stroke:#000000;stroke-width:0.484;'
        f'stroke-linecap:round;stroke-miterlimit:10"'
        f' d="M {p0[0]:.5f},{p0[1]:.5f} A {R0},{R0} 0 0 1 {pmid[0]:.5f},{pmid[1]:.5f}'
        f' A {R0},{R0} 0 0 1 {p1[0]:.5f},{p1[1]:.5f}" />'
    )

    parts.append(
        f'    <circle style="fill:none;stroke:#b3b3b3;stroke-width:0.184;'
        f'stroke-linecap:round;stroke-miterlimit:10"'
        f' cx="{PIVOT_X:.5f}" cy="{PIVOT_Y:.5f}" r="{PIVOT_CIRCLE_R}" />'
    )

    parts += ['  </g>', '</svg>']
    return '\n'.join(parts)
```

(Ticks, labels, unit, and corner text are added by Tasks 3–6; this step must produce a valid, renderable SVG containing just the case body, rim arc, and pivot guide circle.)

- [ ] **Step 5: Update the `/generate` Flask route to the new parameter set**

Replace the `/generate` route body (originally `web/app.py:498-523`) with:

```python
@app.route('/generate', methods=['POST'])
def generate():
    unit        = request.form.get('unit', '%')
    min_val     = float(request.form.get('min', 0))
    max_val     = float(request.form.get('max', 100))
    left_label  = request.form.get('left_label', '')
    right_label = request.form.get('right_label', '')
    big_ticks   = max(1, min(50, int(request.form.get('big_ticks', 10))))
    small_ticks = max(0, min(20, int(request.form.get('small_ticks', 4))))
    label_count = max(2, int(request.form.get('label_count', 6)))
    use_paths   = request.form.get('output_mode', 'paths') == 'paths'
    scale_type  = request.form.get('scale_type', 'linear')
    font_key    = request.form.get('font', _DEFAULT_FONT)
    if request.form.get('max_is_inf') == '1':
        max_val = math.inf

    custom_labels = [s.strip() for s in request.form.get('labels', '').split(',') if s.strip()] or None
    label_size    = max(0.5, min(25.0, float(request.form.get('label_size', 5.1126))))

    try:
        svg = generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks,
                           small_ticks, label_count, use_paths, scale_type, custom_labels,
                           label_size, font_key)
    except ValueError as err:
        return Response(str(err), status=400, mimetype='text/plain')
    return Response(svg, mimetype='image/svg+xml')
```

- [ ] **Step 6: Verify the skeleton renders a valid SVG**

```bash
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6)
assert svg.startswith('<?xml')
assert 'viewBox=\"0 0 93.824249 57.215687\"' in svg
assert 'translate(-60.717741,-107.14126)' in svg
assert 'cx=\"107.42845\"' in svg
from defusedxml import ElementTree as ET
ET.fromstring(svg)
print('OK, valid XML,', len(svg), 'bytes')
"
```
Expected: `OK, valid XML, ... bytes` with no exception. (Requires `defusedxml` — `pip install defusedxml` if not already available.)

- [ ] **Step 7: Commit**

```bash
git add web/app.py
git commit -m "Rewrite generate_svg skeleton around arc geometry (body/rim/pivot only)"
```

---

### Task 3: Implement tick generation (3-tier, all 4 scale mappings)

**Files:**
- Modify: `web/app.py` (inside `generate_svg`, after the pivot-circle `parts.append` from Task 2 Step 4, before `parts += ['  </g>', '</svg>']`)

**Interfaces:**
- Consumes: `_point_at`, `R0`, `ANGLE_MIN`, `ANGLE_MAX`, `MAJOR_LEN/W`, `MEDIUM_LEN/W`, `SMALL_LEN/W` from Task 2.
- Produces: `big_angles` (list[float], length `big_ticks + 1`) and `big_vals` (list[float], same length) — consumed by Task 4's label loop.

- [ ] **Step 1: Add the mapping functions (`value_to_angle` / `major_value`) and tick-line helper**

Insert immediately after the `fd = _FONTS.get(...)` line inside `generate_svg`:

```python
    if scale_type == 'log':
        if min_val <= 0 or max_val <= 0:
            raise ValueError("Logarithmic scale requires min and max values greater than 0.")
        lmin, lmax = math.log(min_val), math.log(max_val)
        lspan = lmax - lmin
        if lspan == 0:
            raise ValueError("Logarithmic scale requires min and max values to differ.")

        def value_to_angle(v):
            return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (math.log(v) - lmin) / lspan

        def major_value(i):
            return min_val * (max_val / min_val) ** (i / big_ticks)

    elif scale_type == 'sqrt':
        if min_val < 0:
            raise ValueError("Square-root scale requires min value ≥ 0.")
        span = max_val - min_val
        if span <= 0:
            raise ValueError("Square-root scale requires max value greater than min.")

        def value_to_angle(v):
            return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * math.sqrt((v - min_val) / span)

        def major_value(i):
            return min_val + span * (i / big_ticks) ** 2

    elif scale_type == 'reciprocal':
        if min_val <= 0:
            raise ValueError("Reciprocal scale requires min and max values greater than 0.")
        if not math.isinf(max_val) and max_val <= min_val:
            raise ValueError("Reciprocal scale requires max value greater than min.")
        if math.isinf(max_val):
            def value_to_angle(v):
                if math.isinf(v):
                    return ANGLE_MIN
                return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * min_val / v

            def major_value(i):
                if i == 0:
                    return math.inf
                return min_val * big_ticks / i
        else:
            if max_val <= 0:
                raise ValueError("Reciprocal scale requires min and max values greater than 0.")
            k = 1.0 / min_val - 1.0 / max_val

            def value_to_angle(v):
                return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (1.0 / v - 1.0 / max_val) / k

            def major_value(i):
                return 1.0 / (1.0 / max_val + i / big_ticks * k)

    else:
        span = max_val - min_val

        def value_to_angle(v):
            if span == 0:
                return ANGLE_MIN
            return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (v - min_val) / span

        def major_value(i):
            return min_val + span * i / big_ticks

    big_interval_angle = (ANGLE_MAX - ANGLE_MIN) / big_ticks
    big_angles = [ANGLE_MIN + i * big_interval_angle for i in range(big_ticks + 1)]
    big_vals = [major_value(i) for i in range(big_ticks + 1)]

    if custom_labels:
        label_count = max(1, min(len(custom_labels), big_ticks + 1))
        custom_labels = custom_labels[:label_count]
    else:
        label_count = max(2, min(label_count, big_ticks + 1))

    if label_count == 1:
        label_indices = [0]
    else:
        label_indices = [round(k * big_ticks / (label_count - 1)) for k in range(label_count)]

    def _tick_line(angle_deg, length, width):
        x = PIVOT_X
        y1 = PIVOT_Y - R0
        y2 = PIVOT_Y - R0 - length
        return (
            f'    <line x1="{x:.5f}" y1="{y1:.5f}" x2="{x:.5f}" y2="{y2:.5f}"'
            f' style="stroke:#1a1a1a;stroke-width:{width:.5f};stroke-linecap:butt"'
            f' transform="rotate({angle_deg:.5f},{PIVOT_X:.5f},{PIVOT_Y:.5f})" />'
        )
```

- [ ] **Step 2: Draw the ticks**

Insert immediately after the pivot-circle `parts.append(...)` block (still before `parts += ['  </g>', '</svg>']`):

```python
    for angle in big_angles:
        parts.append(_tick_line(angle, MAJOR_LEN, MAJOR_W))

    if small_ticks > 0:
        sub = small_ticks + 1
        medium_j = sub // 2 if small_ticks % 2 == 1 else None
        for i in range(big_ticks):
            for j in range(1, small_ticks + 1):
                if scale_type == 'sqrt':
                    angle = big_angles[i] + (j / sub) ** 2 * (big_angles[i + 1] - big_angles[i])
                elif scale_type == 'reciprocal' or math.isinf(big_vals[i]):
                    angle = big_angles[i] + (j / sub) * (big_angles[i + 1] - big_angles[i])
                else:
                    v = big_vals[i] + j * (big_vals[i + 1] - big_vals[i]) / sub
                    angle = value_to_angle(v)
                length, width = (MEDIUM_LEN, MEDIUM_W) if j == medium_j else (SMALL_LEN, SMALL_W)
                parts.append(_tick_line(angle, length, width))
```

- [ ] **Step 3: Verify tick angles for the reference case (0–50, 5 major, 9 minor)**

```bash
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, '', '', 5, 9, 6)
# major ticks at -45.66719, -27.40032, -9.13344, 9.13344, 27.40032, 45.66719
for a in ['-45.66719', '-27.40032', '-9.13344', '9.13344', '27.40032', '45.66719']:
    assert f'rotate({a},' in svg, a
# medium ticks (values 5,15,25,35,45) at the interval midpoints
for a in ['-36.53375', '-18.26688', '0.00000', '18.26688', '36.53375']:
    assert f'rotate({a},' in svg, a
assert svg.count('<line') == 6 + 5 + 40   # 6 major + 5 medium + 40 small (8 per interval * 5)
print('OK')
"
```
Expected: `OK` with no assertion error.

- [ ] **Step 4: Commit**

```bash
git add web/app.py
git commit -m "Implement arc tick generation with 3 tiers across all scale mappings"
```

---

### Task 4: Implement numeric label generation

**Files:**
- Modify: `web/app.py` (inside `generate_svg`, after the tick-drawing block from Task 3, before `parts += ['  </g>', '</svg>']`)

**Interfaces:**
- Consumes: `big_angles`, `big_vals`, `label_indices`, `custom_labels`, `label_size`, `_point_at`, `LABEL_RADIUS`, `_text_to_paths`, `_fmt` — all already available inside `generate_svg`.
- Produces: nothing new consumed by later tasks (labels are a leaf feature).

- [ ] **Step 1: Draw the labels**

Insert immediately after the tick small-ticks loop from Task 3 Step 2:

```python
    lx, ly = PIVOT_X, PIVOT_Y - LABEL_RADIUS
    for k, idx in enumerate(label_indices):
        angle = big_angles[idx]
        label = custom_labels[k] if custom_labels else _fmt(big_vals[idx])
        wrap = f'rotate({angle:.5f},{PIVOT_X:.5f},{PIVOT_Y:.5f})'
        if use_paths:
            parts.append('    ' + _text_to_paths(label, lx, ly, label_size, fd,
                                                  anchor='middle', wrap_transform=wrap))
        else:
            parts.append(
                f'    <text style="font-size:{label_size:.5f}px;font-family:\'{fd.label}\',monospace;fill:#1a1a1a"'
                f' text-anchor="middle" x="{lx:.5f}" y="{ly:.5f}" transform="{wrap}">{escape(label)}</text>'
            )
```

- [ ] **Step 2: Verify labels for the reference case**

```bash
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, '', '', 5, 9, 6, use_paths=False)
for val, angle in [('0','-45.66719'), ('10','-27.40032'), ('20','-9.13344'),
                    ('30','9.13344'), ('40','27.40032'), ('50','45.66719')]:
    assert f'rotate({angle},' in svg and f'>{val}</text>' in svg, (val, angle)
print('OK')

svg2 = generate_svg('uA', 0, 50, '', '', 5, 9, 3, custom_labels=['Lo','Mid','Hi'], use_paths=False)
for val in ('Lo', 'Mid', 'Hi'):
    assert f'>{val}</text>' in svg2, val
print('OK custom labels')
"
```
Expected: `OK` then `OK custom labels`.

- [ ] **Step 3: Commit**

```bash
git add web/app.py
git commit -m "Implement rotated numeric label generation for the arc scale"
```

---

### Task 5: Implement the unit label

**Files:**
- Modify: `web/app.py` (inside `generate_svg`, after the label loop from Task 4)

**Interfaces:**
- Consumes: `_markup_unit_svg` (Task 2 Step 2), `unit`, `fd`.
- Produces: nothing new consumed later.

- [ ] **Step 1: Draw the unit label**

Insert immediately after the label loop from Task 4 Step 1:

```python
    parts.append('    ' + _markup_unit_svg(unit, fd))
```

- [ ] **Step 2: Verify plain and markup units render**

```bash
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, '', '', 5, 9, 6)
assert svg.count('<path') >= 2   # body path + rim path + at least glyph paths
svg2 = generate_svg('cm^2', 0, 50, '', '', 5, 9, 6)
assert svg2 != svg
print('OK')
"
```
Expected: `OK`.

- [ ] **Step 3: Commit**

```bash
git add web/app.py
git commit -m "Wire up unit label rendering via markup-aware helper"
```

---

### Task 6: Implement left/right corner labels; fix `_text_to_paths` end-anchor support

**Files:**
- Modify: `web/app.py` (`_text_to_paths` function, originally `web/app.py:278-305`; and inside `generate_svg`, after the unit label from Task 5)

**Interfaces:**
- Consumes: `left_label`, `right_label` (strings, already threaded through the function signature and route since Task 2), `LEFT_X/Y0`, `RIGHT_X/Y0`, `CORNER_LINE_SPACING`, `CORNER_FONT_SIZE`.
- Produces: nothing new — this is the last content block before `</g></svg>`.

- [ ] **Step 1: Add `end` anchor support to `_text_to_paths`**

`_text_to_paths` currently only handles `anchor == 'middle'` for width-based offset (`start` is the implicit default with no offset). Right-anchored corner text needs a third case. Replace the anchor-handling block at the top of `_text_to_paths`:

```python
    if anchor == 'middle':
        total_w = sum(
            fd.hmtx.get(fd.cmap.get(ord(ch)), (0, 0))[0] * s
            for ch in text if fd.cmap.get(ord(ch))
        )
        x -= total_w / 2
```

with:

```python
    if anchor in ('middle', 'end'):
        total_w = sum(
            fd.hmtx.get(fd.cmap.get(ord(ch)), (0, 0))[0] * s
            for ch in text if fd.cmap.get(ord(ch))
        )
        x -= total_w / 2 if anchor == 'middle' else total_w
```

- [ ] **Step 2: Draw the corner labels**

Insert immediately after the unit-label line from Task 5 Step 1:

```python
    left_lines = left_label.split('\n')[:2]
    right_lines = right_label.split('\n')[:2]

    for i, line in enumerate(left_lines):
        if not line:
            continue
        y = LEFT_Y0 + i * CORNER_LINE_SPACING
        if use_paths:
            parts.append('    ' + _text_to_paths(line, LEFT_X, y, CORNER_FONT_SIZE, fd, anchor='start'))
        else:
            parts.append(
                f'    <text style="font-size:{CORNER_FONT_SIZE:.5f}px;font-family:\'{fd.label}\',monospace;fill:#1a1a1a"'
                f' x="{LEFT_X:.5f}" y="{y:.5f}">{escape(line)}</text>'
            )

    for i, line in enumerate(right_lines):
        if not line:
            continue
        y = RIGHT_Y0 + i * CORNER_LINE_SPACING
        if use_paths:
            parts.append('    ' + _text_to_paths(line, RIGHT_X, y, CORNER_FONT_SIZE, fd, anchor='end'))
        else:
            parts.append(
                f'    <text style="font-size:{CORNER_FONT_SIZE:.5f}px;font-family:\'{fd.label}\',monospace;fill:#1a1a1a"'
                f' text-anchor="end" x="{RIGHT_X:.5f}" y="{y:.5f}">{escape(line)}</text>'
            )
```

- [ ] **Step 3: Verify corner labels (2-line, start/end anchored) render correctly**

```bash
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6, use_paths=False)
assert '>Text</text>' in svg and '>Left</text>' in svg and '>Right</text>' in svg
assert 'text-anchor=\"end\"' in svg
svg_one_line = generate_svg('uA', 0, 50, 'OnlyLine', '', 5, 9, 6, use_paths=False)
assert '>OnlyLine</text>' in svg_one_line
print('OK')
"
```
Expected: `OK`.

- [ ] **Step 4: Commit**

```bash
git add web/app.py
git commit -m "Implement left/right corner labels; add end-anchor support to text-to-paths"
```

---

### Task 7: Update the web form (`web/templates/index.html`)

**Files:**
- Modify: `web/templates/index.html`

**Interfaces:**
- Consumes: the `/generate` route's new form fields (`left_label`, `right_label`) from Task 2 Step 5.
- Produces: nothing consumed by later tasks — this is the UI leaf.

- [ ] **Step 1: Update page title and header wordmark**

Replace:
```html
  <title>M1730 Scale Generator — Baumann Works</title>
```
with:
```html
  <title>E440 Scale Generator — Baumann Works</title>
```

Replace:
```html
      <span class="wordmark-sub">M1730 Scale Generator</span>
```
with:
```html
      <span class="wordmark-sub">E440 Scale Generator</span>
```

- [ ] **Step 2: Replace the Unit/Range-label row with Unit/Left-label/Right-label, dropping the half-height toggle**

Replace the entire `Labels` section block:
```html
      <div class="section">
        <div class="section-eye">Labels</div>
        <div class="row row-2">
          <div class="field">
            <label>Unit</label>
            <input name="unit" type="text" value="%" required>
            <span style="font-family:var(--f-mono);font-size:0.65rem;color:var(--muted)">Markup: cm^2 · H_2O · {1/2} fraction · nests, e.g. {m/s^2}</span>
            <div style="display:flex;align-items:center;gap:0.5rem;margin-top:0.2rem">
              <div class="mode-toggle" role="group" aria-label="Unit height">
                <button type="button" class="mode-btn active" data-unit-height="normal">Full height</button>
                <button type="button" class="mode-btn" data-unit-height="half"
                  title="Restrict unit label to the upper half of the panel — use when numeric labels visually crowd the unit">Half height</button>
              </div>
              <input type="hidden" name="half_height_unit" id="half-height-unit" value="0">
            </div>
          </div>
          <div class="field">
            <label>Range label</label>
            <input name="range_label" type="text" value="5mA" required>
          </div>
        </div>
      </div>
```
with:
```html
      <div class="section">
        <div class="section-eye">Labels</div>
        <div class="row row-2">
          <div class="field">
            <label>Unit</label>
            <input name="unit" type="text" value="uA" required>
            <span style="font-family:var(--f-mono);font-size:0.65rem;color:var(--muted)">Markup: cm^2 · H_2O · {1/2} fraction · nests, e.g. {m/s^2}</span>
          </div>
        </div>
        <div class="row row-2" style="margin-top:1.25rem">
          <div class="field">
            <label>Left label (up to 2 lines)</label>
            <textarea name="left_label" rows="2" style="background:var(--white);border:1px solid var(--rule);border-radius:1px;padding:0.5rem 0.7rem;font-family:var(--f-body);font-size:0.925rem;color:var(--ink);width:100%;outline:none;resize:vertical">Text
Left</textarea>
          </div>
          <div class="field">
            <label>Right label (up to 2 lines)</label>
            <textarea name="right_label" rows="2" style="background:var(--white);border:1px solid var(--rule);border-radius:1px;padding:0.5rem 0.7rem;font-family:var(--f-body);font-size:0.925rem;color:var(--ink);width:100%;outline:none;resize:vertical">Text
Right</textarea>
          </div>
        </div>
      </div>
```

- [ ] **Step 3: Update the default Label size value to match the new reference default**

Replace:
```html
            <input name="label_size" type="number" step="any" min="0.5" max="25" value="9" required>
```
with:
```html
            <input name="label_size" type="number" step="any" min="0.5" max="25" value="5.1126" required>
```

- [ ] **Step 4: Remove the now-dead unit-height JS handler and download filename**

Delete this block from the `<script>`:
```html
    document.querySelectorAll('[data-unit-height]').forEach(btn => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('[data-unit-height]').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        document.getElementById('half-height-unit').value = btn.dataset.unitHeight === 'half' ? '1' : '0';
        doGenerate();
      });
    });
```

Replace:
```html
        download: 'M1730-Scale.svg',
```
with:
```html
        download: 'E440-Scale.svg',
```

- [ ] **Step 5: Update the source link and version tag**

Replace:
```html
  <span class="version-tag">v{{ version }} — <a href="https://git.baumann.gr/adebaumann/M1730-Scale" target="_blank" rel="noopener">Source</a></span>
```
with:
```html
  <span class="version-tag">v{{ version }} — <a href="https://git.baumann.gr/adebaumann/E440-Scale" target="_blank" rel="noopener">Source</a></span>
```

- [ ] **Step 6: Run the app locally and verify the form generates a scale**

```bash
cd /home/adebaumann/development/44-Scale
source .venv/bin/activate 2>/dev/null || true
(cd web && python app.py &) 
sleep 2
curl -s -X POST http://localhost:5000/generate \
  -F unit=uA -F min=0 -F max=50 -F left_label=$'Text\nLeft' -F right_label=$'Text\nRight' \
  -F big_ticks=5 -F small_ticks=9 -F label_count=6 -F output_mode=paths -F scale_type=linear \
  -F label_size=5.1126 -F font=alte-din-1451-mittelschrift.regular \
  | head -c 300
echo
pkill -f "python app.py" || true
```
Expected: response starts with `<?xml version="1.0"...` and contains no error text. Then also open `http://localhost:5000/` in a browser (or note that a headless check isn't sufficient) to visually confirm the form renders and the light-table preview shows an arc dial, not a strip.

- [ ] **Step 7: Commit**

```bash
git add web/templates/index.html
git commit -m "Update web form for arc-dial fields: unit, left/right labels"
```

---

### Task 8: Manual visual verification against the reference

**Files:** none modified — verification only.

**Interfaces:**
- Consumes: the fully working generator from Tasks 1–7.

- [ ] **Step 1: Generate the reference-matching scale and save it**

```bash
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6,
                   use_paths=False, font_key='alte-din-1451-mittelschrift.regular')
open('/tmp/e440-generated-check.svg', 'w').write(svg)
print('written')
"
```

- [ ] **Step 2: Open both files in a browser or Inkscape and compare side-by-side**

```bash
xdg-open /home/adebaumann/development/44-Scale/e440.svg 2>/dev/null || true
xdg-open /tmp/e440-generated-check.svg 2>/dev/null || true
```

Check specifically:
- Tick marks form a symmetric fan matching the reference's angular spread (not compressed/stretched/mirrored).
- The rim arc (`<path>` with stroke `#000000`) follows the same curve as the ticks' base, with correct orientation (not flipped inside-out — if the arc bulges the wrong way, its sweep-flag in Task 2 Step 4 needs flipping from `0 0 1` to `0 0 0`).
- Numbers 0–50 sit just outside the tick tips, tilted to match their tick's angle, same reading direction as the reference (not upside-down).
- `uA` sits centered above the arc; `Text`/`Left` and `Text`/`Right` sit in the bottom corners matching the reference's placeholder positions.

If the rim arc orientation is wrong, fix it now in `web/app.py` (Task 2 Step 4's sweep-flag) and re-run Step 1.

- [ ] **Step 3: No commit for this task** (verification only; any fixes found get their own commit against the task that introduced the bug).

---

### Task 9: Produce the three deliverable SVG files; remove the old M1730 files

**Files:**
- Create: `E440-Scale_template.svg` (renamed/tidied from `e440.svg`)
- Create: `E440-Scale_paths.svg`
- Create: `E440-Scale_text.svg`
- Delete: `e440.svg`, `M1730-Scale_template.svg`, `M1730-Scale_paths.svg`, `M1730-Scale_text.svg`

**Interfaces:**
- Consumes: `generate_svg()` from Tasks 1–6, run with the reference parameters (unit `uA`, 0–50, 5 major/9 minor/6 labels, left `Text\nLeft`, right `Text\nRight`, font `alte-din-1451-mittelschrift.regular`).

- [ ] **Step 1: Create the template file**

```bash
cd /home/adebaumann/development/44-Scale
git mv e440.svg E440-Scale_template.svg
```

Edit `E440-Scale_template.svg`: change `sodipodi:docname="e440.svg"` to `sodipodi:docname="E440-Scale.svg"`.

- [ ] **Step 2: Generate the paths (outline) variant**

```bash
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6,
                   use_paths=True, font_key='alte-din-1451-mittelschrift.regular')
open('E440-Scale_paths.svg', 'w').write(svg + '\n')
"
```

- [ ] **Step 3: Generate the text (embedded-font) variant**

```bash
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6,
                   use_paths=False, font_key='alte-din-1451-mittelschrift.regular')
open('E440-Scale_text.svg', 'w').write(svg + '\n')
"
```

- [ ] **Step 4: Delete the old M1730 files**

```bash
cd /home/adebaumann/development/44-Scale
git rm M1730-Scale_template.svg M1730-Scale_paths.svg M1730-Scale_text.svg
```

- [ ] **Step 5: Verify all three new files are well-formed XML**

```bash
cd /home/adebaumann/development/44-Scale
python3 -c "
from defusedxml.ElementTree import parse
for f in ('E440-Scale_template.svg', 'E440-Scale_paths.svg', 'E440-Scale_text.svg'):
    parse(f)
    print(f, 'OK')
"
```
Expected: `OK` printed for all three files. (Requires `defusedxml` — `pip install defusedxml` if it's not already available; stdlib XML parsers are avoided here per repo security guidance since they're vulnerable to XXE/billion-laughs by default, even though these particular files are locally generated and trusted.)

- [ ] **Step 6: Commit**

```bash
git add E440-Scale_template.svg E440-Scale_paths.svg E440-Scale_text.svg
git commit -m "Replace M1730 deliverable SVGs with generated E440 template/paths/text files"
```

---

### Task 10: Rebrand infra and docs (Dockerfile check, k8s, README.md, CLAUDE.md, VERSION bump)

**Files:**
- Modify: `k8s/deployment.yaml`, `k8s/ingress.yaml`, `k8s/service.yaml`
- Modify: `README.md`, `CLAUDE.md`
- Modify: `web/app.py:12` (`VERSION`)

**Interfaces:** none — this is the final documentation/infra pass.

- [ ] **Step 1: Bump the app version**

In `web/app.py`, change:
```python
VERSION = "0.87"
```
to:
```python
VERSION = "1.0"
```

- [ ] **Step 2: Rename k8s resources from m1730 to e440**

In `k8s/deployment.yaml`, replace every occurrence of `m1730-generator` with `e440-generator`, and the image tag:
```yaml
          image: git.baumann.gr/adebaumann/m1730-generator:0.87
```
with:
```yaml
          image: git.baumann.gr/adebaumann/e440-generator:1.0
```

In `k8s/ingress.yaml`, replace every occurrence of `m1730-strip-prefix` with `e440-strip-prefix`, `m1730-generator` with `e440-generator`, and `/M1730-generator` with `/E440-generator`.

In `k8s/service.yaml`, replace every occurrence of `m1730-generator` with `e440-generator`.

- [ ] **Step 3: Verify no `m1730`/`M1730` strings remain in k8s manifests**

```bash
cd /home/adebaumann/development/44-Scale
grep -ri m1730 k8s/ && echo "FOUND STALE REFS" || echo "clean"
```
Expected: `clean`.

- [ ] **Step 4: Rewrite `README.md`**

Replace the full contents of `README.md` with:

```markdown
# E440-Scale

SVG scale artwork and web generator for E440-style round-dial analog panel
meters. Scales are typeset in **Alte DIN 1451 Mittelschrift**.

## Repository layout

```
E440-Scale_template.svg   Inkscape working file (case body, tick marks, labels)
E440-Scale_paths.svg      Paths-only export — text converted to outlines
E440-Scale_text.svg       Live-text export — embedded font, editable labels
fonts/                     TTF fonts (required for Docker build)
web/                       Flask web generator
k8s/                       Kubernetes manifests
Dockerfile                 Container build
```

## SVG files

The canvas is **93.824249 × 57.215687 mm**. The coordinate origin is
offset: layer 1 carries `transform="translate(-60.717741,-107.14126)"`, so
absolute SVG coordinates begin around x=61, y=107.

The dial's case geometry — body outline, mounting ears, pivot point
(local `107.42845, 162.62819`), rim radius (`43.842751` mm), and angular
sweep (`±45.667°` from vertical) — is fixed across every generated scale.
Only the tick marks, numeric labels, unit label, and corner text vary.

| File | Purpose |
|---|---|
| `E440-Scale_template.svg` | Master Inkscape file. Edit this one. |
| `E440-Scale_paths.svg` | All text converted to outlines. Use for cutting plotters, laser engravers, or any workflow where fonts must not be embedded as live text. |
| `E440-Scale_text.svg` | Live `<text>` nodes with the font embedded as base64. Smaller file; requires an SVG viewer that supports embedded fonts. |

Open files in **Inkscape** (developed with v1.4.3). There is no build step
— edit the SVG directly or use the web generator.

## Web generator

A Flask app that generates arc-dial scale SVGs from a form. Supports two
output modes switchable in the UI:

- **Paths** — glyphs rendered as outlines via fontTools; opens cleanly in
  Inkscape, Illustrator, and cutting software
- **Text** — live `<text>` elements with the selected font embedded as
  base64

### Run locally

```bash
source .venv/bin/activate
cd web && python app.py      # http://localhost:5000
```

Fonts are loaded at startup from the `fonts/` directory in the repo root.

### Form parameters

| Field | Description |
|---|---|
| Unit | Symbol shown centered above the arc (e.g. `uA`, `V`, `%`). Supports inline markup — see below |
| Left label / Right label | Up to 2 lines each, placed in the bottom-left/bottom-right corners of the dial |
| Mapping | **Linear**, **√** (square root), **Log**, or **1/x** (reciprocal). Major ticks are always evenly spaced angularly; the mapping controls their values and how minor ticks are placed within each interval. |
| Min / Max | Numeric range of the scale |
| Major intervals | Number of major tick intervals |
| Minor per interval | Minor ticks between each pair of major ticks. When odd, the exact midpoint subdivision is drawn as a taller "medium" tick, matching the reference dial's 3-tier tick scheme |
| Labels | Number of numeric labels; Labels − 1 should divide evenly into Major intervals |
| Custom labels | Optional comma-separated labels that override the calculated numbers |
| Label size (mm) | Font size of the numeric labels, in millimetres (default `5.1126`) |

### Scale mappings

| Mapping | Major tick values | Minor tick placement | Constraints |
|---|---|---|---|
| Linear | Evenly spaced | Uniform | None |
| √ | Quadratic (0, 1, 4, 9 … × max) — compresses low values | Crowded toward the low end of each interval | Min ≥ 0 |
| Log | Geometric (min × r⁰, r¹, r² …) — each interval spans the same ratio | Crowded toward the high end of each interval | Min and Max > 0 |
| 1/x | Harmonic (values decrease left → right, e.g. ∞, 10, 5, 2, 1) — compresses high values | Uniform (in angle space, i.e. proportional to 1/v) | Min and Max > 0; Max may be ∞ |

### Unit markup

The Unit field accepts a lightweight shorthand (no LaTeX engine — everything
is drawn as glyph paths and auto-sized to fit above the arc):

| Markup | Result |
|---|---|
| `cm^2` | superscript (`^{...}` for multiple characters) |
| `H_2O` | subscript (`_{...}` for multiple characters) |
| `{1/2}` | stacked fraction with a horizontal rule; a bare `/` stays inline |

Constructs nest: `{m/s^2}` is a fraction with a superscript in the
denominator, `x^{1/2}` a superscripted fraction.

## Container

```bash
docker build -t e440-scale:latest .
docker run -p 5000:5000 e440-scale:latest
```

Fonts are loaded from `fonts/` in the repo root; they are bundled into the
image at build time.

The production image is at `git.baumann.gr/adebaumann/e440-generator`.

## Kubernetes

Manifests in `k8s/` deploy to the `works` namespace with a Traefik v2
IngressRoute at `works.baumann.gr/E440-generator`.

```bash
# Managed by ArgoCD — push to git to deploy
git push
```

Resources: namespace, deployment (1 replica, gunicorn), ClusterIP service,
Traefik IngressRoute + stripPrefix middleware.

## License

GPL-3.0 — see [LICENSE](LICENSE).
```

- [ ] **Step 5: Rewrite `CLAUDE.md`**

Replace the full contents of `CLAUDE.md` with:

```markdown
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

SVG scale artwork for E440-style round-dial analog meters. The scales are designed in Inkscape (v1.4.3) using **Alte DIN 1451 Mittelschrift**, a German technical drafting standard typeface.

The canvas is **93.824249 mm × 57.215687 mm**. The coordinate origin is offset: `transform="translate(-60.717741,-107.14126)"` is applied to `layer1` in all files, so absolute SVG coordinates start around x=61, y=107 rather than 0,0.

## File Roles

The three SVG files represent layers of the same design, intended to be combined or used separately:

- **`E440-Scale_template.svg`** — Inkscape working template. Contains the case body path (with mounting ears and rim cutout), the pivot guide circle, tick marks, numeric labels `0`–`50`, the `uA` unit label, and `Text`/`Left` and `Text`/`Right` corner label placeholders.
- **`E440-Scale_paths.svg`** — Paths-only version: all tick marks and text as `<path>`/`<line>` elements, no live text. Used for cutting/printing workflows where fonts must not be embedded as live text.
- **`E440-Scale_text.svg`** — Text-overlay version: tick mark lines plus live `<text>` elements for numeric/unit/corner labels, font embedded as base64.

## Key Design Parameters

- Stroke colors: `#1a1a1a` (dark near-black) for tick marks, numeric/unit/corner text; fill `#f9f9f9` for the case body; `#000000` for the rim arc stroke; `#b3b3b3` for the pivot guide circle.
- Fixed case geometry (identical across every generated scale — only tick/label/text content varies):
  - Pivot point (local, pre-translate frame): `(107.42845, 162.62819)`
  - Rim / inner-tick radius: `43.842751` mm
  - Angular sweep: `±45.667193°` from vertical, clockwise-positive (low value on the left)
  - Tick radial lengths: major `4.7839203` mm, medium `3.1215858` mm, small `1.8121802` mm
  - Tick stroke widths: major `0.79658329`, medium `0.37985462`, small `0.27478597`
  - Label radius: `49.862` mm
- Tick tiers: major ticks at each labeled interval boundary; a medium tick appears only when the minor-tick count per interval is odd, at the exact midpoint subdivision; all other subdivisions render as small ticks.
- Numeric labels rotate to follow the arc (radial orientation), matching their tick's angle.

## Web Generator (`web/`)

A Flask app that generates scale SVGs from a form. The venv lives at the repo root.

```
source .venv/bin/activate
cd web && python app.py        # runs on http://localhost:5000
```

`app.py` has one route (`GET /`) serving the form and one (`POST /generate`) that returns SVG. All SVG geometry is computed in `generate_svg()` using the same coordinate constants as the hand-drawn files: ticks and labels are positioned "as if angle=0" (straight up from the pivot) and given an SVG `rotate(angle, PIVOT_X, PIVOT_Y)` transform. The form exposes: unit label, min/max values, scale mapping, major tick intervals, minor ticks per interval, number of numeric labels, custom labels, label size, font, output mode, and left/right corner labels (up to 2 lines each).

Fonts are loaded at startup from every `.ttf` in `fonts/` and embedded as base64 in every generated SVG (text output mode) so output files are self-contained. The default font is `alte-din-1451-mittelschrift.regular`.

## Container & Kubernetes

```
docker build -t e440-scale:latest .
```

`fonts/` must contain the required TTF files before building.

```
kubectl apply -f k8s/
```

`k8s/ingress.yaml` adds a Traefik `IngressRoute` for `works.baumann.gr/E440-generator` and a `stripPrefix` middleware. Adjust `certResolver` in the file if your Traefik instance uses a different name.

## Editing SVG files directly

Open files in **Inkscape**. There is no build, lint, or test tooling — changes are made directly to the SVG XML or via Inkscape's GUI. When modifying tick positions or labels, keep all three files consistent.
```

- [ ] **Step 6: Verify the Docker build still works with the renamed image tag**

```bash
cd /home/adebaumann/development/44-Scale
docker build -t e440-scale:latest . 2>&1 | tail -20
```
Expected: build completes successfully (`Successfully tagged e440-scale:latest` or equivalent final layer message). If Docker isn't available in this environment, skip this step and note it in the final report instead of claiming it was verified.

- [ ] **Step 7: Commit**

```bash
git add web/app.py k8s/ README.md CLAUDE.md
git commit -m "Rebrand infra and docs from M1730 to E440; bump version to 1.0"
```

---

## Final check

- [ ] Run `grep -ril m1730 --include='*.py' --include='*.html' --include='*.yaml' --include='*.md' .` from the repo root and confirm no output (aside from this plan/spec doc's historical references, which are expected to mention M1730 as the prior state).
- [ ] Confirm `git status` shows a clean tree with all changes committed.
- [ ] Confirm the three deliverable SVGs (`E440-Scale_template.svg`, `E440-Scale_paths.svg`, `E440-Scale_text.svg`) exist and the three old `M1730-Scale_*.svg` files are gone.
