#+TITLE: Theme-Studio Spec: Editable Face Height (absolute vs relative) #+DATE: 2026-07-02 #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED * DRAFT Theme-Studio Spec: Editable Face Height :PROPERTIES: :ID: acf7e080-d5ae-4500-b3fd-30dcaa0fb8de :END: - 2026-07-02 Thu @ 17:02:46 -0400 — stubbed from the nov-reading modeline-height bug discussion (Craig + session); DRAFT, needs a full read + decision pass before it is READY * Metadata - Status: Draft (stub — Decisions and phases sketched, not settled) - Source: 2026-07-02 diagnosis of "nov reading pane's modeline is suddenly huge." Root cause: the modeline height padding (=cj/--modeline-padding=, a =display '(height 1.15)= on a leading space) scales relative to the buffer's =default= face; =mode-line='s own =:height= is unspecified, so it inherits the enlarged reading font nov-reading remaps in. The minimal fix (seed an absolute =:height= on the mode-line faces in =face_data.py=) is being applied separately; this spec covers the *durable* half — making height a first-class editable attribute in the studio so it can be tuned and re-exported without hand-editing the generated theme. - Related: [[file:../design/theme-studio-face-rules.org][theme-studio face-rules]] (the "every themeable attribute is a real editable control" principle — this is why the mode-line box became a real =:box=), [[file:theme-studio-palette-columns-spec.org][palette-columns spec]] (adjacent studio attribute-model work) * Summary The studio already *stores* and *exports* per-face =:height= (=build-theme.el= emits it; =face_data.py= seeds it on org/shr headings), but there is no editable control for it — height is a hidden, seed-only attribute. This spec adds a real height control to the face editor, with an explicit absolute-vs-relative distinction: relative (a float multiplier, e.g. 1.3×) for text faces that should scale with the buffer, absolute (an integer in 1/10pt, e.g. 128) for structural chrome faces that must NOT track the default face height. The modeline preview reflects the chosen height. The abs/rel distinction is load-bearing: it is exactly what separates a heading that should scale from a modeline that must stay fixed, and getting it wrong reproduces the bug that motivated the spec. * Problem / Context Height is themeable in Emacs two ways with opposite intent, and the studio currently collapses them: - *Relative* (=:height 1.3=, a float) — multiply the inherited face height. Correct for headings (=org-level-1=, =shr-h1=): they should grow with text scaling. - *Absolute* (=:height 128=, an integer 1/10pt) — a fixed size independent of the inherited face. Correct for structural chrome (=mode-line=, =header-line=, =tab-bar=, =line-number=): they must not track a buffer's enlarged default. Today the studio has no height control at all, so the only way to set a modeline height is to hand-seed =face_data.py= or hand-edit the generated =WIP-theme.el= (which the next export overwrites — the same round-trip caveat as the ANSI-256 cube faces). And a naive "just add a height slider" that only offered a multiplier would let a user "fix" the modeline with a relative height that still scales — re-creating the exact bug. The studio's own face-rules principle says every attribute the theme controls should be a real, editable, previewed control; height is currently the exception. * Goals and Non-Goals ** Goals 1. A height control in the face editor, at least for the structural chrome faces (=mode-line=, =mode-line-inactive=, =header-line=, =tab-bar=, =tab-line=, =line-number= and its siblings). 2. Explicit absolute-vs-relative choice in the control, with sane per-face defaults (chrome → absolute, headings → relative). 3. The export (=build-theme.el=) emits an integer for absolute and a float for relative, unchanged in mechanism (it already writes whatever number is stored) — the studio just has to store the right *kind* of number. 4. The modeline (and other chrome) preview reflects the chosen height so the bar visibly gets taller/shorter. 5. Round-trips through save/load and re-export without loss (no repeat of the cube-face drop). ** Non-Goals - A height control on every one of the ~800 faces. Most faces should inherit; exposing height everywhere is noise. Scope to chrome + the heading faces that already carry a relative height in the seed. - The minimal seed fix for the current bug (absolute =:height= on the two mode-line faces in =face_data.py=). That ships separately and immediately; this spec does not block it. - Changing =cj/--modeline-padding= or =cj/modeline-height-factor= in =modeline-config.el= — the padding multiplier is fine once the mode-line face carries an absolute base; that is a config concern, not a studio one. * Decisions Open questions to settle before this leaves DRAFT. Each becomes a resolved decision (with rationale) once answered. ** TODO Decide the control's shape: one field with an abs/rel toggle, or two distinct controls? A single numeric field plus an "absolute (pt) / relative (×)" switch is compact but easy to misread; two labeled controls (a pt field for chrome, a × field for headings) are clearer but wider. Lean: one field + toggle, defaulting to the face's natural kind. ** TODO Decide which faces expose the control. Minimum is the chrome set (mode-line family, header-line, tab-bar/line, line-number). Question: also expose the relative control on the heading faces that already seed a multiplier (org-level-*, shr-h*, org-document-title), so those become editable too, or leave those seed-only for now? ** TODO Decide how absolute height is entered and displayed — raw 1/10pt (128) or points (12.8)? Emacs stores 1/10pt; users think in points. A pt field that stores ×10 is friendlier but adds a conversion the export must not double-apply. ** TODO Decide the preview surface for chrome height. The modeline is a =@ui= face with a preview; confirm the preview can render a variable-height bar, and whether header-line/tab-bar/line-number get the same treatment or a simpler swatch. ** TODO Confirm the =mode-line-inactive :height 2= artifact is cleaned up as part of this, not left for the seed fix. WIP-theme.el currently carries =:height 2= (0.2pt) on mode-line-inactive — a stray value. Decide whether the seed fix or this spec owns removing it (inactive should inherit mode-line's absolute height). * Implementation phases Sketch only — refine when the Decisions above are settled. ** TODO Phase 1 — attribute model: absolute vs relative height in the face object Extend the studio face model so a height value carries its kind (absolute integer vs relative float). Ensure =seedFace=, save/load, and the =build-theme.el= export preserve the kind (integer → absolute =:height=, float → relative). Characterization test on the export for both kinds. ** TODO Phase 2 — UI control on the face editor for the chrome-face set Add the height control (per the Decisions shape) to the face editor rows for the structural chrome faces. Default each face to its natural kind. No control on the long tail. ** TODO Phase 3 — preview reflects height Make the modeline (and any other chrome) preview render the chosen height so the change is visible in the studio. ** TODO Phase 4 — seed the chrome faces and reconcile the current theme Seed absolute heights for the chrome faces in =face_data.py=, remove the =mode-line-inactive :height 2= artifact, regenerate, and confirm the round-trip (studio → export → theme) holds and the nov modeline no longer scales. ** TODO Phase 5 — gates + docs Studio gen/test/check green; a coverage/round-trip test for height kinds; update the face-rules doc to note height as a first-class attribute. Full elisp suite green (theme build is elisp).