# SEIHouse Display Engine

The shared Reader/Display engine builds a different display font ("cut") for
every album or project. Each cut keeps its own settings while inheriting the
current Reader letter rules, Latin/Vietnamese/Cyrillic/Greek repertoire and
OpenType features.

## Settings a cut controls

- **Shape:** thickness, width, lowercase height, ascender height, capital corners, lowercase
  roundness, straightness; soft or cut corners; round or flat ends; round or sharp joins;
  plain u or u with a foot (`uFoot`)
- **Pen:** oval pen or flat nib (`pen`), thick and thin (contrast), pen angle (where the
  thick and thin fall), slant;
  tilted upright letters or true italic forms (`italicForms`). True italic reuses the
  Reader's italic d, f, h, i, m, n and u (plus Cyrillic д, и, п, т) and keeps the cut's
  own roundness. The italic u always has its exit stroke, so `uFoot` applies only to
  upright forms. `italicForms` is recorded in spacing identity only when true, so cuts
  without it keep their existing spacing exports.
- **Spacing:** space between letters and words
- **Letters:** single/double-story a/g, R leg, K/k arms, M stems, W strokes,
  y tail, G spur, Q tail, open/closed 4 and 6/9. Eleven individual Lab controls
  persist in each cut's `alternates` JSON object. Eight named OpenType sets
  (`ss01`–`ss08`) select the other forms.

All four presets use **plain W/w**. Crossed uppercase W and its accented form
are opt-in through `ss04`; lowercase w stays plain. The Lab labels both W
choices and starts every preset with Plain W. `ss04` also switches M.

## Files

| File | What it is |
|---|---|
| `cuts/*.json` | One file per cut (Soft, Edge, Ink, Wide to start) |
| `fonts/<cut>/` | Full OTF/WOFF2 and six web subsets, family "SEIHouse Display <Cut>" |
| `fonts.css` | Per-cut web faces with exact `unicode-range`, relative to this directory |
| `lab/index.html` | The Display Lab: shape cuts live on an album cover, track list, poster and alphabet |
| `lab_server.py` | Local Lab server: the repository's files plus **Build font**, **Make these the shipped …** and motion review/approval jobs, for this computer only |
| `lab_jobs.py` / `motion_jobs.py` | The Lab's build/promote and motion review/approve jobs; each also runs from the command line |
| `novel-expanded/index.html` | Silent NovelExpanded homepage mock with four living cuts, refinement controls and a future menu-motion seam; see its [README](novel-expanded/README.md) |
| `../packages/living-titles/` | Installable private `@seihouse/living-titles` runtime, static faces, TypeScript interfaces and optional React component; the mock uses its public player |
| `build_living_package.py` / `pack_living_package.mjs` | Assemble certified shared source and compiled spacing, then produce the installable archive and provenance |
| `verify_living_package.mjs` / `verify_package_browser.py` | Clean packed-consumer imports, types, React bundle and browser lifecycle gates |
| `living*.js` / `living-panel.html` | Living Title timelines, isolated frame drawing and Lab authoring controls |
| `living-media.mjs` / `build_living_media.mjs` | Selected-clip decoding and timestamped video encoding, bundled locally |
| `motion_profiles.json` / `motion-certification.json` | Versioned motion limits and per-cut, source/fixture-bound certification |
| `verify_motion.py` / `verify_living.py` | All-glyph motion construction/topology and real browser SVG/video gates |
| `../engine.js` | Shared letter, mark, symbol and numeric rules, with Reader defaults and Display options |
| `../font_builder.py` | `FontBuilderCore`: shared export, outlines, spacing, hinting and OpenType layout |
| `make_display.py` | Thin cut loader using the same core as `../make_fonts.py` |
| `build_all_cuts.py` | Discovers every cut JSON and builds its declared production styles, subsets and specimens |
| `production.py` / `production-manifest.json` | Validated cut/style plans and the final delivery inventory with byte counts and SHA-256 hashes |
| `verify_production.py` | Shape/topology, inherited features, compiled spacing, subset/license and FontBakery gates for every built face |
| `specimens.py` / `specimen.css` / `specimen.js` | Generates native-font album cover, track list, poster and alphabet pages into `../site/display/` |
| `verify_specimens.py` | WOFF2 versus built-OTF native widths, style/feature controls and 360–430px layouts |
| `build_subsets.py` | Six Display deliveries using the Reader's subset routine and coverage policy |
| `build_display_page.py` | Builds the Lab page |
| `stroke_geometry.js` | Live oval-pen caps and joins, including perpendicular faces after slant, and the flat-nib sweep |
| `title_spacing.py` | Capital-height and mixed-case optical spacing, plus final-font GPOS/metric exports |
| `letter_alternates.py` | Stylistic sets and measured alternate pair classes |
| `spacing.js` | Applies compiled kerning classes, rounded advances and capital spacing in the Lab |
| `spacing_<cut>.json` | Final spacing data from each OTF, verified against its settings and font hash |
| `outline_cleanup.py` | Resolves overlaps and removes contour/point/spike debris before export |
| `../verify_display_shapes.py` | Renders every glyph at 400px and gates shapes, design preservation and web delivery |
| `../verify_shared_engine.py` | Gates Reader preservation and Display repertoire, language, mark and numeric shaping |
| `../verify_display_subsets.py` | Gates license parity, subset outlines/metrics/shaping, CSS and the 25 KB basic limit |
| `verify_lab.py` | All live glyphs and phone overflow at 360, 375, 390, 414 and 430px |

## Workflow

Work locally with the Lab server, from the repository root:

```sh
python -X utf8 display/lab_server.py
```

Open `http://localhost:8000/display/lab/`. It serves the same files as
`python -m http.server 8000`, plus three buttons that exist only on this computer:

1. **Build font from these sliders** turns the current settings into a real draft
   font in about half a minute, under `dist/lab-builds/<cut>/`. The **Built font
   file** panel then shows that font, offers its OTF/WOFF2, and the live drawings
   switch to its final spacing.
2. **Make these the shipped Ink** (or Soft, Edge, Wide) rewrites that cut's JSON,
   rebuilds only that cut's fonts, subsets, CSS, manifest and specimen, then the
   Lab. Git can undo it. New cut names still need the full release steps.
3. A rebuilt cut's **Living title** shows **Review motion**, then **Approve motion**;
   see [Living Titles](#living-titles).

The server listens on `127.0.0.1` only, answers only `localhost` host names, and
starts jobs only for its own page (a per-run token, same-origin requests). One job
runs at a time. The hosted website stays static: its Lab hides these buttons and
keeps **Save cut**, **Download JSON** and **Copy JSON**.

Without the server: **Download JSON** into `display/cuts/my-cut.json`, run
`python -X utf8 display/make_display.py display/cuts/my-cut.json`, then rebuild the
Lab with `python -X utf8 display/build_display_page.py`.

### Flat nib

**Pen → Flat nib** sweeps every stroke with a broad nib at the pen angle instead of
the oval. Strokes along the nib edge become hairlines, strokes across it are full,
and every stroke end is cut at the pen angle. The sweep is exact: each curve is
split where it runs parallel to a nib edge and the pieces are shifted copies of
the curve, in both the Lab (`stroke_geometry.js`) and the builder (`font_builder.py`).
**Ends** set the nib corners (flat = sharp chisel, round = slightly rounded); the
nib makes its own joins. The nib keeps the oval's perimeter, so switching pens
changes the character, not the overall weight; raise **Thick and thin** (up to 6)
for stronger contrast. Dots stay round. The engine also spaces and aligns letters
with the nib's footprint. No shipped cut uses the nib yet.

### Living Titles

Choose a shipped cut, enter a title, then use **Living title**. The current
letter designs and `cpsp` setting carry into the artwork. Motion settings stay
separate from cut JSON and browser drafts. Any draft can **preview** motion with
conservative limits (Ink's for calligraphic drafts); downloads stay locked until a
shipped cut's motion is approved.

Motion is approved per cut. A cut's certificate binds the shared drawing sources,
that cut's settings and motion profile, and its 11 reviewed witness files, so
rebuilding Ink leaves Soft, Edge and Wide approved. With the Lab server, a rebuilt
cut offers **Review motion**: it points the cut's motion profile at its new built
settings, re-runs the full probe (every glyph, 11 poses, 707 dense ordinal poses)
and lists every glyph whose witness changes. **Approve motion** is accepted only
for a clean review whose sources have not changed since; it adopts the witnesses
and certifies that cut. The same steps run as
`python -X utf8 display/motion_jobs.py review|approve --input request.json --result out.json`
with `{"cuts": ["ink"]}`, and `python -X utf8 display/verify_motion.py --cut ink`
re-checks one cut.

The initial preview is paused: **120 BPM, four beats, two seconds, 30 FPS**.
Select BPM/beat count or a local audio file and a **1–10 second** clip. Toggle
thickness, slant and pen angle, choose strength/colors, then **Prepare loop**.
Preparation yields to the browser, reports progress and supports cancellation.
Play/Pause, Restart and scrubbing use the prepared frames; audio playback uses
the audio clock. Changing the scene stops playback and invalidates its frames.

The reviewed envelopes stay within **+3% thickness, +1° slant and +2° pen angle**.
Ink uses the smaller **+1%, +1°, +0.25°** envelope to preserve its ordinal
counter clearance. Disabled axes retain their starting values. Starting glyph
origins, advances and compiled kerning stay fixed. A padded canvas contains the
entire loop, so the title never resizes or shifts with its changing ink bounds.
Wide has a circular pen; rotating that pen has no visible effect by itself.

**Download animated SVG** creates transparent, self-contained vector artwork
with discrete declarative frame animation, an accessible title and a static
reduced-motion fallback. It also plays when embedded as an image. **Check video
format** probes this browser for the selected size/audio, then shows the real
format and extension: MP4/H.264 (AAC with audio), or WebM/VP9 or VP8 (Opus).
Video uses the chosen solid background. Audio is **off by default**. Choose
**1080 px** on the longest edge, or **1920 px**; dimensions are rounded to even
pixels for encoders. If no suitable codec exists, the Lab reports it clearly.

Music stays on this device. The Lab reads container metadata and decodes only
the selected clip, then derives channel-aware RMS with 120 ms attack and 480 ms
release. Stereo phase differences retain their energy; silence rests. Motion
eases to rest at both loop edges. Serve the complete Lab over localhost or HTTPS
for the browser's media APIs. Mediabunny **1.61.3** is a local, separate
MPL-2.0 bundle, built with esbuild **0.28.2** from locked development dependencies:

```sh
npm ci --prefix display
node display/build_living_media.mjs
python -X utf8 display/build_display_page.py
python -m http.server 8000
```

Open `http://localhost:8000/display/lab/`. No media is uploaded. The font npm
package retains its existing dependency-free runtime exports and file list;
the four font cuts still build Regular only. Read the [Step 6 validation and
animated proofs](../docs/DISPLAY-LIVING-TITLES-STEP6.md) for certification and
delivery commands. A failed or stale profile certificate blocks its downloads.

For production, build the complete collection from the repository root:

```sh
python -X utf8 display/build_all_cuts.py
python -X utf8 display/verify_production.py
python -X utf8 display/verify_specimens.py
python -X utf8 display/build_display_page.py
node build_site.mjs
```

The batch discovers **every `cuts/*.json`**, with no fixed cut list. Current
Soft/Edge/Ink/Wide settings stay Regular-only. Each cut gets a finished-font
specimen in [`site/display/`](../site/display/index.html), with downloads,
capital spacing and all eight stylistic-set controls. These pages use the native
font's actual kerning and metrics, with font synthesis disabled.

`--output-dir dist/display-production` writes a self-contained candidate bundle
with `display/fonts/`, `display/fonts.css`, the manifest, `site/display/`, and
both license files. Candidate spacing lives next to its font. Serve that bundle
as a website root to inspect the specimens; it leaves committed fonts and Lab
spacing untouched. `--specimens-only` refreshes pages, inventory and CSS from
existing fonts without rebuilding outlines. Cut names use ASCII letters/digits,
single spaces or hyphens; duplicate slugs or PostScript identities fail before
building.

### Opt-in extra weights and Oblique

Add a `production` object to the individual cut JSON. For example, a Wide
variation can request:

```json
"production": {
  "weights": {"Light": 0.85, "Bold": 1.03},
  "oblique": 9
}
```

Weight values multiply **that cut's thickness**; all other drawing and alternate
settings stay the same. Weight names use the Reader's static weight classes
(Light 300, Medium 500, SemiBold 600, Bold 700, etc.). Lighter names require a
multiplier below 1; heavier names require one above 1. The accepted 0.5–1.5 range
is an input bound, and every result must still pass the shape gate.
`oblique` adds degrees to the cut's existing slant (the reviewed Ink example adds
3° to its 8° slant, giving 11°).
It retains upright letter skeletons and sets proper Oblique names, slope/caret
metadata, OS/2 oblique flags and a `slnt` STAT value. It does not select Reader
italic letter substitutions. The total angle is limited to 25°.

Extra fonts, subsets and spacing exports live in `fonts/<cut>/<Style>/`; CSS
uses their real weight and oblique angle. Regular paths stay compatible.
To experiment without changing committed defaults, copy the cut JSON files to
another directory, add production choices there, and pass `--cuts <directory>`
with a separate `--output-dir`. Each cut explicitly chooses its extra styles.

### CI and release gates

The **App distribution** GitHub Action runs the batch on pushes to `main`, pull
requests and manual runs. Every rebuilt face passes the shape gate, inherited
language/numeric/alternate features, final spacing, license and six-subset
checks, then FontBakery **OpenType + offline Universal**, with any WARN, FAIL,
ERROR or unfinished run blocking delivery. Every Latin-basic face stays strictly
under 25,000 bytes. The job also rebuilds Reader and checks its preserved tables,
outlines and metrics, plus the rebuilt Lab's title/alternate/phone gates.

Successful runs upload **`seihouse-display-fonts`** (fonts, web CSS, manifest,
native specimens and licenses), retained for 30 days. Validation reports and
FontBakery logs are in **`shared-builder-evidence`**, including failed runs.
Automatic and manual runs honor each cut JSON. Current files declare no extras.

Regular retains Step 1's immutable metrics, skeletons and topology witnesses.
Each extra style needs separately reviewed 400px topology at
`tests/fixtures/display-production-topology/<slug>-<style>.json`; a new cut's
Regular uses `<slug>.json`. The fixture records the exact drawing `settings`,
built spacing `variants`, `size_px`, and each glyph's fixed counter/witness
coordinates. A changed recipe requires new review. Every glyph still passes
outline, overlap, 6px counter/aperture clearance and OTF/WOFF2 raster checks.
The production gate reads fixed fixtures and never captures a candidate baseline.
Reviewed opt-in example JSONs and proofs are linked in
[Step 5 pipeline evidence](../docs/DISPLAY-PRODUCTION-STEP5.md).

Each cut build also writes `latin-basic`, `latin-extended`, `cyrillic`, `greek`,
`vietnamese` and `symbols` WOFF2 files. Default builds refresh the subsets for
every OTF included in `display/fonts.css` before writing that stylesheet, so it
cannot reference another cut's missing or stale subsets.
`--output-dir` keeps candidate fonts, subsets, spacing and CSS together without
changing shipped metadata. To regenerate subsets/CSS from the current OTFs,
run `python -X utf8 display/build_subsets.py`.

The hosted Lab previews and saves settings; font compilation uses the local Python
tools. See the root [homepage](../index.html) and [website guide](../docs/WEBSITE.md).

## License and web delivery

Every full font and web subset embeds the **SEIHouse Sans Ecosystem License,
version 1.0**, using exactly the Reader's name records: copyright (0), trademark
(7), manufacturer (8), designer (9), vendor/designer URLs (11/12), license
description (13) and license URL (14). `OS/2.fsType` is `0` and the vendor is
`SEIH`, matching Reader. The operative terms are in [LICENSE](../LICENSE);
installable document embedding follows those terms. Display family/style names
remain distinct from Reader.

Keep `fonts.css` next to the `fonts/` directory and include it in your site:

```html
<link rel="stylesheet" href="/display/fonts.css">
```

```css
.title {
  font-family: "SEIHouse Display Soft", sans-serif;
  font-weight: 400;
  font-kerning: normal;
  font-feature-settings: "cpsp" 1;
}
```

Like Reader, extended and script faces overlap Latin/mark coverage to preserve
complete grapheme clusters and title kerning. Later matching faces take browser
priority. A project requiring only basic Latin can use its cut's `latin-basic`
face block alone; all four basic files are strictly below **25,000 bytes**
(Soft 22,156; Edge 17,264; Ink 22,416; Wide 22,140). Subsets retain applicable
stylistic, numeric, local-form and mark features, including a visible `.notdef`.

The Lab fits long SVG words proportionally inside their containers on phones.
Its gate checks all four presets at five widths from 360–430px, with default
and long titles, crossed W and `cpsp`. It rejects both document overflow and
individual elements outside the viewport. Run `python -X utf8 display/verify_lab.py`;
`--lab` checks a candidate HTML file and `--url` checks
a served page. See the [pre-Step-5 fixes and current proofs](../docs/DISPLAY-PRE-STEP5.md).

## Shared engine and clean shapes

The forked `display/engine.js` has been removed. Both Labs and both builders use
the root engine. Reader defaults keep soft corners, round ends and joins, an
unrotated pen, and the existing true italic behavior. Cut settings select cut
corners, flat ends, sharp joins, pen angle and oblique slant; oblique slant keeps
upright letter forms.

Step 2's four cuts passed the 400px shape gate: **732 glyphs per cut, 2,928 outlines
and 5,856 OTF/WOFF2 renders, 0 flags**. Each cut passes FontBakery OpenType
and offline Universal with **0 FAIL / 0 WARN**. Step 4 expands each cut to 817
glyphs: **3,268 outlines and 6,536 renders, 0 flags**. Its separate proof reports
cover the new designs; both FontBakery profiles still pass with zero FAIL/WARN.
All ninety shipped Reader
files retain their latest 0.39 bytes; all ninety rebuilt deliveries match
normalized current tables and CFF outlines/hints. The ten styles also retain
every original 0.35 glyph outline and metric.

The shape gate also measures substantial white counters and letter/number
apertures. An immutable approved topology baseline rejects missing counters and
filled or sealed interiors, including cases that pass clearance-only analysis.
Heavy Display pens use shared counter compensation: vertical stems
keep the selected thickness, while horizontal diameter is bounded by x-height
(20% for round ends, 15% for extended flat ends). Both the live Lab and exported
fonts use the same calculation. Soft and Edge gain room in `Aa`, accented `a`,
`æ`, `e`, `g`, Greek epsilon and stacked symbols; Ink and Wide keep their pen
ratios. Saved cut JSON and the shared letter skeletons remain unchanged.
See the [counter repair and before/after proofs](../docs/DISPLAY-COUNTER-REPAIR.md).

All four cuts inherit combining marks and `mark`/`mkmk`/`ccmp`, Turkish and
Romanian `locl`, the complete Latin Extended-A additions, modern Cyrillic and
monotonic Greek, Vietnamese tone stacks, decimal separator spacing and
`frac`/`tnum`/`sups`/`subs`. The shared 0.39 inventory contains 732 font glyphs:
696 encoded engine entries, 34 alternates, `.notdef` and space. Unicode/PUA
aliases reuse glyph IDs.

Adopting Reader's existing `a`/`e` widths and `i` bearings also updates twenty
old advances per cut. Cut JSON, every other old advance, encoded aliases and
typographic line spacing are retained. Edge's Windows clipping ascent grows
20 font units to fit the new Vietnamese tone stacks. The
[Step 2 notes](../docs/DISPLAY-SHARED-ENGINE-STEP2.md) record each inherited
change and its exact metric gate.

Flat faces end perpendicular to the stroke, including an angled pen and slant.
Attached terminals receive joins rather than protruding caps; sharp joins use
a bounded miter with bevel fallback. The automatic cleanup pass removes
micro-contours, near-duplicate points, spurs/notches and unresolved overlaps.
The live Lab uses the repaired terminal construction as well.

Run `python -X utf8 verify_display_shapes.py` and
`python -X utf8 verify_shared_engine.py` from the repo root; any flag fails.
Add `--proof-dir path/to/proofs --report path/to/report.json` to save every
400px glyph sheet and its results from the shape gate. The same gates run in
GitHub Actions. See the [shared engine and preservation evidence](../docs/DISPLAY-SHARED-ENGINE-STEP2.md).

[Step 1's construction details and before/after proofs](../docs/DISPLAY-SHAPES-STEP1.md)
record the earlier 241-glyph inventory, its 964 clean outlines and per-cut
FontBakery results. Those reports remain a frozen record of that build; Step 2
reports cover the expanded shared inventory.

## Version 0.1 notes

- Each cut has its own default letter designs and includes 85 alternate glyphs,
  including accents, shared script forms and numeric derivatives. Built sets
  combine with mark attachment, `cpsp` and numeric features. The Lab uses their
  compiled spacing, so switching a design preserves final width agreement.
  See the [Step 4 defaults, feature mapping, gates and native title proofs](../docs/DISPLAY-ALTERNATES-STEP4.md).

- Titles use each cut's capital-height spacing pass and optional `cpsp` capital
  spacing. The Lab reads the final OTF's kerning and advances; all 888 measured
  title widths differ by less than 0.065%. Unbuilt edits receive a draft-spacing
  label until their font and Lab are rebuilt. See the [Step 3 title proofs,
  spacing behavior and width gate](../docs/DISPLAY-TITLE-SPACING-STEP3.md).
