aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* feat(theme-studio): scenes for emms, org-roam, hl-todo, symbol-overlayCraig Jennings5 days5-1/+136
| | | | emms shows its three UIs (browser tree, playlist, metaplaylist); org-roam the backlinks buffer plus the dailies calendar mark; hl-todo comment keywords and the flymake diagnostic; symbol-overlay all eight pin faces on one defun plus the at-point default. All four join the face-coverage gate.
* feat(theme-studio): minibuffer-stack scenes on one shared sessionCraig Jennings5 days5-1/+213
| | | | One C-x b consult-buffer scene drawn jointly by vertico, marginalia, consult, embark, and orderless; each app's preview shows the shared session with its own faces at work, then extends it with the states only it owns (vertico multiline, marginalia's full annotation catalog, consult line/grep/async, embark act + collect, orderless component cycling). All five join the face-coverage gate, so every one of their 72 faces appears verbatim in the renderers.
* feat(theme-studio): web-mode scene covering all 81 faces + HTML sampleCraig Jennings5 days7-3/+118
| | | | The web-mode preview is one mixed document: markup with every tag/attr variant, an inline CSS part, a generic template block (engine-agnostic on purpose), and a script part carrying a JSON island, JSX depths, nested template literals, SQL-in-a-string, a PHP preprocessor island, and JSDoc annotations. The realism gate now covers it, so all 81 faces are exercised. SAMPLES gains an HTML language, which also enriches the syntax and auto-dim previews.
* feat(theme-studio): realistic scenes for company, transient, and friendsCraig Jennings5 days5-3/+224
| | | | Five daily-driver apps leave the generic face-name list: company (a completion popup with every row variant, inline ghost preview, tooltip search, and the echo frontend), company-box (the icons variant), transient (a magit-commit-style panel exercising every key class), magit-section (status-buffer headings with child counts and the highlighted section), and rainbow-delimiters (buffer-honest nesting depths 1-9 plus the cycle past 9 and both error faces). A new test gates realism: every face of each covered app must appear verbatim in its renderer, so a scene can't silently skip faces.
* feat(theme-studio): pin retired packages so themes keep covering themCraig Jennings5 days4-7/+106
| | | | PINNED_PACKAGE_FACES is the curated record of packages retired from the config (ghostel, all-the-icons). A pinned package survives inventory regeneration, shows a 'not loaded' label + hover in the app dropdown, and keeps refreshing its face list from the live inventory while that still carries it. An attempted live regen made the fragility concrete: today's daemon session was missing 142 faces from lazily-loaded packages, so regen-from-live is inherently lossy and the pin is the only durable record.
* chore(theme): WIP theme snapshotCraig Jennings5 days2-156/+339
|
* feat(theme-studio): screenshot harness + ecosystem coverage policyCraig Jennings5 days4-0/+108
| | | | Two speedrun-enabling pieces. A #preview=<app>&theme=<json> hash handler plus screenshot-previews.sh shoot any app's face table and live preview headlessly under a real theme (WIP.json by default), so preview work can be verified without a human clicking through the studio. The README gains the coverage policy: the studio themes popular packages even when uninstalled, pinning their faces rather than dropping them, and unloaded packages' previews matter more, not less.
* feat(theme-studio): register the ai-term agent-color facesCraig Jennings5 days4-3/+52
| | | | The nine ai-term faces (the bypass-banner accent plus one per Claude Code /color name) join the studio as a bespoke app, seeded with their dupre hues. The preview mirrors what they paint: the banner line and a mock input box per session color. Tuning one here recolors live agents on their next redraw.
* feat(ai-term): auto-set each project's session color on fresh launchCraig Jennings5 days5-20/+313
| | | | Every project now maps to a stable Claude Code session color: an override alist wins, else a character-sum hash of the project basename picks one of the eight names. When a fresh tmux session is created (never on reattach), a poller waits for the TUI to boot and types /color <name> itself, with the Enter deferred a beat so the slash-command menu can't swallow it. Two refusals keep the injection safe: the bypass banner must be on screen (a bare shell never gets typed into) and the prompt line must still be empty (typed-ahead input is never corrupted).
* feat(ai-term): render Claude Code session colors in dupre huesCraig Jennings5 days3-47/+113
| | | | Claude Code's /color picks a session accent from eight names, each emitted as a fixed xterm-256 index (probed against v2.1.198 by cycling /color in a scratch tmux session and reading the SGR codes). Agent terminals now pin all eight indices plus the bypass banner to dupre faces, so any /color choice renders in the theme's palette instead of stock xterm hues. dupre has no orange or pink, so those borrow red+1 and magenta+1. If a Claude Code update moves an index, the stock hue comes back (the alist docstring carries the re-probe note).
* feat(ai-term): paint the agent accent dupre blueCraig Jennings5 days3-0/+142
| | | | Claude Code draws its accent (the bypass-permissions banner, borders, spinner) with xterm-256 palette codes, and the stock rose red is palette index 211. eat resolves those codes through a per-terminal face vector, so agent terminals now point index 211 at the new cj/ai-term-accent face (dupre blue #67809c) at creation. Every other eat terminal keeps the true palette. Per-project colors can later ride the same per-terminal mechanism.
* feat(buffer-file): make the disk-changed diff review navigableCraig Jennings5 days2-30/+427
| | | | Pressing d in the C-x C-s conflict menu (and the save-some loop) now enters a modal review instead of a peek-and-return toggle: point lands on the first hunk, arrows and TAB move through the changes, and the menu keys act from inside the diff. difftastic gets --context 1 and an explicit --width, since as a subprocess it can't detect the terminal and wrapped at 80 columns. A new m choice resolves the conflict in ediff. I kept the post-merge save re-asking once, so an abandoned merge can't silently overwrite the disk version.
* fix(org-capture): block global popup keys while a capture is openCraig Jennings5 days2-0/+59
| | | | F1, F10, F11, F12, and M-SPC are global popup keys (dashboard sweep, music, dirvish-side, terminal, agent swap) and fired over an in-progress capture. org-capture-mode's keymap is active exactly for the capture's duration and shadows the global map, so those keys now hit a blocker there that names the way out (C-c C-c to finalize, C-c C-k to abort). The C-; a prefix stays unblocked: a deliberate two-chord sequence isn't a stray keypress.
* feat(mail): annotate the attachment picker with MIME type and sizeCraig Jennings5 days2-3/+70
| | | | cj/mu4e-save-attachment-here now completes through an annotated table (category mu4e-attachment), so marginalia shows each attachment's MIME type and decoded size beside the filename. Unknown candidates annotate as nil. The existing picker test queries the function table via all-completions instead of car-mapping the old alist.
* docs: sort three theme-studio specs into docs/specs with status headingsCraig Jennings5 days4-7/+31
| | | | First spec-sort pass under the docs-lifecycle convention: color-families -> READY (its status field carried the 2026-06-10 confirmation), palette-columns and palette-ramps -> DRAFT. Each got the authoritative status heading with an :ID: UUID; six inbound links rewritten. subr-mock-migration-spec is -spec-named but lacks the spec spine, so it stays put until it's renamed or retrofitted.
* feat(org): resolve org-id links into project spec docsCraig Jennings5 days4-0/+161
| | | | The docs-lifecycle convention gives every formal spec under a project's docs/specs/ an :ID: and links cross-project with [[id:...]], but org-id-locations only indexes agenda files and visited files, so a fresh spec's id never resolved on click. org-spec-links.el enumerates every project's docs/specs/*.org into org-id-extra-files once org-id loads (a literal file list; org-id doesn't glob), and cj/org-id-refresh-spec-locations re-scans and updates org-id-locations for immediacy after new specs land. Verified live against a known cross-project spec id.
* fix(ai-term): drop the Agent: echo after an agent swapCraig Jennings5 days2-7/+12
| | | | The message duplicated the modeline directly above it: the buffer name and the eat state icons already announce which agent is focused, so the echo just lingered as clutter. The no-other-agents echo stays, since it reports something the modeline can't.
* feat(modeline): eat state icons and info-left, systray-right layoutCraig Jennings5 days3-11/+191
| | | | | | The modeline now follows one layout rule: the left side is Emacs information (mode icon, eat state, modified/read-only, buffer name, @host, Narrow, VC branch, position, MACRO, process) and the right side is a systray for package indicators (recording, flycheck counts, misc-info). The VC branch and mode-line-process moved left to fit the rule. eat buffers trade the [semi-char]:run text for two icons beside the mode icon: a keyboard glyph for the input mode (quiet when semi-char, warning otherwise, hover text explains where keys go, mouse-1/2/3 switch modes mirroring eat's own bindings) and a green play / red power-off for the process state. eat-config clears eat's buffer-local mode-line-process so nothing renders twice.
* feat(markdown): start the preview server from F2 when it's downCraig Jennings5 days3-13/+93
| | | | cj/markdown-preview now brings up the simple-httpd listener itself instead of signaling a user-error pointing at cj/markdown-preview-server-start. The two-command flow guarded against a surprise network listener, but the preview is the only reason the listener exists, so the guard was just an extra step. I kept the start command for manual use.
* fix(keybindings): retire the two-column F2 and C-x 6 bindingsCraig Jennings5 days2-0/+32
| | | | A grazed F2 followed by a stray 2, s, or b invokes two-column mode, which replaces the buffer's mode-line-format with its own retro layout and spawns a 2C/ companion buffer. That's what mangled the modeline in an agent terminal: eat semi-char buffers don't forward F2, so the prefix was armed everywhere. Nothing of mine uses global F2 (markdown-mode's binding lives in its own map), so I unbound both routes to 2C-command. cj/modeline-reset repairs a hijacked buffer.
* feat(modeline): mode icons, status segments, and a repair commandCraig Jennings5 days7-49/+508
| | | | | | | | | | | | | I rebuilt the custom modeline as pure segment helpers with thin :eval wiring: - The nerd-icons mode icon replaces the mode-name text (cached per buffer, plain name on terminal frames), with the full mode name in the help-echo. - New left-side segments: modified dot / read-only lock (file buffers only), remote @host tag, Narrow tag that widens on click, point-based percentage, region selection info, and a MACRO tag while a keyboard macro records. - New right-side segments: mode-line-process (eat and compilation state was invisible) and flycheck per-severity counts with click-through to the error list, replacing the stock status text. Glyphs are nerd-icons private-use codepoints so emojify can't rewrite them, with text fallbacks when icons are unavailable. - cj/modeline-reset kills a hijacked buffer-local mode-line-format (two-column mode, ediff, calc). - Optional taller bar via cj/modeline-height-factor, a display height property on the padding space so the theme's mode-line faces stay untouched. - Housekeeping: the dead user-constants require and stale commentary are gone, cj/modeline-vc-faces left the risky-local-variable list, and the cache-key defun is cj/--modeline-vc-cache-key so it no longer shadows the same-named defvar. Percent signs from :eval strings go through the mode-line %-construct pass, so the position segment emits %% for a literal percent.
* feat(weather): show a three-day forecast in the mode-line tooltipCraig Jennings5 days1-0/+3
| | | | Sets wttrin-mode-line-tooltip-forecast-days to 3, the option added to wttrin in its 6c808ff (local checkout, release/0.4.0).
* refactor(ai-term): split into sessions, display, and EAT-backend layersCraig Jennings5 days5-797/+921
| | | | | | | | ai-term.el had grown to ~1,215 lines mixing project/tmux session discovery, window display policy, the EAT terminal backend, and the public commands, so a change to any one risked coupling to the others. I extracted three layers, following the calendar-sync split shape: ai-term-sessions.el (discovery, tmux naming and parsing, launch command, picker ordering), ai-term-display.el (display-buffer actions and rule, toggle state, server-window routing), and ai-term-backend-eat.el (terminal create/reattach, pty send, EAT keymap). The backend file is named for its backend so a future one lands as a sibling. ai-term.el stays the public face (options, commands, keymap, shutdown), every name unchanged, so existing (require 'ai-term) callers and all 30 test files work as before. The extracted layers forward-declare the face's defcustoms rather than requiring it, keeping the graph acyclic. I dropped the unused cl-lib and host-environment requires and added the three modules to the header-contract list.
* feat(buffer-file): confirmation policy for the destructive C-; b operationsCraig Jennings5 days3-49/+272
| | | | Delete file (D) ran with no confirmation at all; erase, clear-to-top/bottom, and revert were a single keystroke from destroying unsaved edits; and raw revert-buffer prompted even when there was nothing to lose. Policy now: delete always confirms, naming the file (the VC path keeps vc-delete-file's own prompt); erase/clear/revert confirm only when a file-visiting buffer has unsaved edits, and stay fast otherwise. The delete workhorse is split into an unconfirmed internal so its existing tests keep exercising the file mechanics; 13 new tests cover the policy.
* fix(mail): drop obsolete image vars, make the remote-image toggle report stateCraig Jennings5 days1-7/+13
| | | | The "block remote images" comment sat on mu4e-view-show-images / mu4e-view-image-max-width, which are obsolete since mu4e 1.7 and ignored by the shr view — the real gate is gnus-blocked-images. The comment now documents the actual policy (remote blocked, embedded inline), and cj/mu4e-toggle-remote-images echoes the effective state after each refresh instead of leaving you to guess what it did.
* fix(packages): verify signatures as allow-unsigned, keep GNU keys freshCraig Jennings5 days1-3/+20
| | | | package-check-signature was nil, skipping verification everywhere. allow-unsigned verifies signatures when an archive provides them while still accepting the unsigned local mirror and .localrepo packages. gnu-elpa-keyring-update installs at bootstrap (non-fatal on failure) so an expired GNU archive key stops being a reason to turn verification off.
* fix(treesit): prompt before grammar installs, add explicit bootstrapCraig Jennings5 days2-3/+48
| | | | treesit-auto-install was t, so opening a file could silently trigger a network download and compiler build mid-edit. It now prompts, and cj/install-treesit-grammars is the deliberate fresh-machine bootstrap that installs everything in one command.
* refactor(org-babel): move the babel-confirm toggle to the org menuCraig Jennings5 days2-5/+21
| | | | cj/org-babel-toggle-confirm landed on C-; k as a placeholder. It's an org-babel concern, so it now lives on the org menu as C-; O b, and C-; k is free again. The binding registers after org-config loads so a standalone load of this module still works.
* feat(ai-term): say so when M-SPC has no other agent to switch toCraig Jennings5 days2-6/+142
| | | | With a single agent open and focused, the rotation wrapped back to the same agent and echoed a misleading "Agent: <name>" as if it had swapped. Now it says there are no other ai-terms to switch to. A sole agent that is displayed but not selected still gets selected, and the no-agents picker fallback is unchanged.
* test(recording): record integration captures into a temp dirCraig Jennings5 days1-42/+48
| | | | The wf-recorder integration tests passed video-recordings-dir to the real capture pipeline, and the test file's defvar stub of that variable is clobbered by user-constants' defconst on load — so every suite run left ~0.5s screen captures in the real recordings directory. Pass an explicit make-temp-file dir and delete it in the unwind, matching what the capture probe already did.
* fix(native-comp): compile at speed 2 to preserve redefinition semanticsCraig Jennings5 days2-1/+42
| | | | At speed 3 the native compiler emits direct calls for functions in the same compilation unit, bypassing the symbol's function cell. Any cl-letf mock of a module's own helper then silently runs the real code: the recording tests' mocked wayland check and device validation were bypassed, and make test launched real wf-recorder screen captures. Speed 2 is the highest level that preserves redefinition semantics. A meta test now pins the setting; the local eln cache needs one flush so stale speed-3 artifacts recompile.
* fix(eat): guard against a nil charset wedging the terminalCraig Jennings6 days2-0/+70
| | | | | | EAT 0.9.4's parser accepts more charset-designation final bytes than its store step maps. A designation like ESC ( A (UK) isn't one of the two it handles ("0" and "B"), so it stores nil as that slot's charset. The next character written then fails (cl-assert charset) in eat--t-write. Since writes run off the output-queue timer, it repeats once per output chunk. An agent terminal that emits one of these bytes throws "cl-assertion-failed (charset)" hundreds of times and stops rendering. I added filter-args advice on eat--t-set-charset that coerces a nil charset to us-ascii before it's stored, so an unmapped designation falls back to plain ASCII instead of wedging. Patching the vendored pcase would be cleaner, but a package update reverts it. The advice loads with eat, since the target is an internal function.
* fix(recording): record audio-only to lossless FLAC, not AAC/M4ACraig Jennings6 days4-20/+29
| | | | | | Audio-only recordings were written as AAC in an MP4/.m4a container. The stop path SIGINTs ffmpeg, and if the MP4 muxer doesn't write its moov trailer before exit, the file has no moov atom and won't decode. ffmpeg and AssemblyAI both reject it. Three recordings were lost that way and had to be rebuilt with untrunc. The video path already avoids this by using Matroska, which needs no finalize pass. I switched the audio-only path to FLAC. FLAC frames are self-contained, so an abruptly stopped recording still decodes, with no trailer to miss at close. It's also lossless, dropping the 64k AAC encode that degraded speech before transcription. AssemblyAI recommends a lossless source and accepts FLAC directly. The transcription path passes audio files through untouched.
* chore: stop tracking the task archiveCraig Jennings6 days1-0/+3
|
* docs(calendar-sync): keep placeholder feed-url examplesCraig Jennings6 days2-2/+2
|
* fix(calendar-sync): skip overlapping syncs for the same calendarCraig Jennings7 days3-5/+112
| | | | | | A timer tick that fired while a calendar's previous fetch was still running launched a second concurrent sync for that calendar, wasting work and racing to write the same org file. The dispatcher now skips a calendar whose status is already syncing and logs the skipped tick. The sentinel resets the status on process exit, so the skip clears on its own. load-state also clears a stale syncing status left by a crash, so a calendar can't be skipped forever.
* fix(prog-shell): only auto-chmod scripts in prog-mode buffersCraig Jennings7 days2-1/+19
| | | | cj/make-script-executable runs from a global after-save-hook and set +x on any saved file whose first line was a shebang, in every buffer. A downloaded script you were reading, a template, or a shebang in a text or org file silently became executable. I gated it on derived-mode-p prog-mode, so it only acts on actual script buffers. Real scripts (sh-mode, python-mode) still get the fast path.
* fix(markdown): vendor strapdown.js instead of a plain-HTTP CDNCraig Jennings7 days3-3/+51
| | | | | | The live markdown preview pulled strapdown.js from http://ndossougbe.github.io over plain HTTP. That broke the preview with no network, loaded third-party JS over an unencrypted connection (mixed content, MITM), and trusted an unmaintained github.io page against the localhost preview. I vendored the self-contained bundle (jQuery, marked, bootstrap themes) into assets/strapdown.js and embed it inline. The whole preview now serves from localhost and works offline. cj/markdown-html reads the file once and caches it.
* chore: stop tracking local agent toolingCraig Jennings7 days1-5/+4
|
* fix(undead-buffers): drop cj/save-some-buffers name collisionCraig Jennings7 days3-14/+45
| | | | | | | | Emacs crashed at launch with wrong-number-of-arguments on cj/save-some-buffers, down the startup path (dashboard-only to kill-all-other-buffers to save). Two modules defined a function by that name: custom-buffer-file.el's legible save prompt (arg + pred), installed as an override on save-some-buffers, and undead-buffers.el's older one-arg wrapper that called save-some-buffers internally. custom-buffer-file loads first, undead-buffers second. The one-arg version won the shared symbol, so the override re-entered it with two args. I removed undead-buffers.el's wrapper. cj/kill-all-other-buffers-and-windows now calls the standard save-some-buffers with the undead predicate, which routes through the override when loaded and the built-in otherwise, so undead-buffers no longer depends on custom-buffer-file. The legible override keeps the cj/save-some-buffers name. A regression test loads both modules in launch order and guards the call and the arity so a one-arg shadow can't return.
* fix(calendar-sync): drop singly-declined recurring occurrencesCraig Jennings7 days3-3/+83
| | | | | | Declining one occurrence of a recurring meeting left it on the agenda. Google emits that decline as a RECURRENCE-ID override carrying the user's PARTSTAT=DECLINED. But calendar-sync--parse-exception-event never read the override's attendee block, so the occurrence kept the series' inherited "accepted" status and the declined filter never dropped it. The apply side already re-derives status from an override's attendees. The parse side just wasn't supplying them. The fix parses the override's ATTENDEE lines into :attendees, the same way parse-event does. A unit test covers the extraction. An integration test runs the full parse/apply/filter chain on a declined week.
* feat(theme-studio): surface the five remaining font-lock facesCraig Jennings7 days5-6/+221
| | | | | | Add the font-lock faces the syntax tier didn't cover (warning, doc-markup, negation-char, and the two regexp-grouping faces) as their own editable categories: warn, dmark, neg, rxgb, rxgc. Each maps 1:1 to its face, seeds from the stock defaults, and is exercised in the code-sample preview via a TODO comment, a docstring substitution, a string regexp, and a C negation. Injected only the five new faces into the default-faces snapshot rather than regenerating it wholesale. A full recapture pulls in unrelated package-inventory drift.
* chore(theme-studio): commit WIP theme snapshotCraig Jennings7 days2-20/+131
|
* docs: spec for migrating tests off mocking primitivesCraig Jennings7 days1-0/+158
| | | | Pull the long-term subr-mock migration out of the task list into a discussion spec: the native-comp origin, the stopgap already shipped (variadic sweep plus the arity meta-test), the real scope (the UI-prompt mocks, not the boundary mocks the test rules say to keep), and the open decisions to settle before any code moves.
* feat(buffer-file): legible save prompts for save-some-buffers and ↵Craig Jennings7 days4-7/+739
| | | | | | | | | | | | | | disk-changed saves Replace the cryptic single-key save prompts with read-multiple-choice menus whose options are labeled on screen instead of recalled as keys. save-some-buffers (C-x s and save-on-exit) is overridden with a reimplementation that reuses the stock candidate selection and abbrev-save tail but swaps map-y-or-n-p's terse key list for a labeled menu, and adds a clean-whitespace-and-save action. C-x C-s gains a wrapper that fast-paths normal saves to save-buffer; only when the buffer has unsaved edits and the file changed on disk does it show a labeled menu (save / diff / clean / revert / cancel) instead of the bare "Save anyway?" yes/no. Both menus share a diff toggle, and whitespace-only diffs route to a plain unified diff with trailing whitespace highlighted, since difftastic normalizes trailing whitespace and renders such changes blank. The interactive wrappers stay thin over pure, tested helpers: the save-loop planner, the key-to-action maps, the whitespace detector, and the renderer choice.
* chore: stop tracking the local task listCraig Jennings7 days1-0/+3
|
* feat(theme-studio): register nov-reading structural faces and color the previewCraig Jennings7 days3-9/+36
| | | | The nov reading view app gains six faces: a heading and link accent for each of the sepia, dark, and light palettes, seeded to match the module defaults. The book-page preview now draws each page's title in its palette's heading face and an inline phrase in its link face, iterating only the base palette faces so the structural faces color a page rather than getting blank pages of their own.
* feat(nov-reading): persist font size, add per-palette structural facesCraig Jennings7 days3-34/+277
| | | | | | Font size now carries across books and sessions. The +/-/= keys write the text-scale offset to data/nov-reading-text-scale and the offset is restored when a book opens, so a size I set sticks instead of resetting to the base height on every reopen. The = key returns to the base height and persists that reset. Each palette grows from a single bg/fg face into a bundle: :face plus optional :heading and :link. When a palette is active, its heading and link faces remap shr's h1-h6 and link faces buffer-local, so the EPUB hierarchy reads in the palette's accent. The remap stays buffer-local to the nov buffer, so HTML mail and eww keep the theme's normal shr colors.
* feat(theme-studio): render nov-reading preview as a book pageCraig Jennings7 days3-0/+48
| | | | The nov-reading app fell back to the generic preview (face names in their own colors), which doesn't show what a palette looks like. I added a renderer that draws a real book page per palette: a mock page of Hawthorne's "Dr. Heidegger's Experiment" in sepia, dark, and light, with a small-caps byline, a drop cap, and justified serif prose. Each page takes its background and foreground from its palette face, so tuning a palette repaints its page.
* feat(nov): reading-view theme layer with palettes and font sizingCraig Jennings7 days8-22/+267
| | | | | | | | EPUB reading prefs were scattered: a hardcoded Merriweather/180 font-remap in calibredb-epub-config's nov hook, no color control (the old sepia foreground had been stripped), and a frame-global EBook fontaine preset as the only way to size up. That preset resized the font in every buffer in the frame, not just the book. I pulled the reading view into its own layer, modules/nov-reading.el, on top of stock nov (no fork). It owns three things, all buffer-local: a reading palette (sepia/dark/light, each a face the dupre theme owns, sepia the default), the serif typography (family plus a defcustom base height replacing the hardcoded 180), and page font sizing (+/- bump the size live, = resets to the base). Width moves to { }. calibredb-epub-config keeps the library and width/centering layout. Its nov hook now calls into the layer. The three palette faces register as a nov-reading app in theme-studio (face_data.py), so they're tunable there like any other app. I dropped the EBook fontaine preset, since reading size is buffer-local now.