2026 10 05 one colour vocabulary design - SyberLabs/RISE GitHub Wiki
Status: Design record, build authorized for PR1. Date: 2026-10-05. For Mateo (ids and contracts) and Seth (the Reader). It builds Q6 of the Consolidated Reader decisions and §3 "One colour vocabulary" of the proposal. It delivers the first two acceptance lines of FND-008: the theme-to-engine map beside RISE_CURRENT_THEMES, and each engine rendering all nine themes. B2b, page chrome and the Settings accent, is the third line and follows as its own record. Ground: origin/main = fcc88280. Every code fact is path:line at that commit; nothing was run or rendered, and anything marked (inferred) or (computed) was reasoned from the code, not seen. The owner questions are in §8, each with its answer line.
Q6 as recorded (docs/product/discussions/2026-10-05-consolidated-reader-decisions.md:14): "The nine themes are the only colours a reader or a model sees. The Settings accent is removed and page chrome follows the reading's theme; the ambient drone is removed." Builds B2 and D2. The record adds (:22): "B2 adds its colour maps beside RISE_CURRENT_THEMES and never edits it."
FND-008 (docs/product/tasks/FND-008.json:13-17) has three acceptance lines. B2a delivers the first two:
- A theme-to-engine map sits beside
RISE_CURRENT_THEMES, keyed by the same nine ids;RISE_CURRENT_THEMESis unchanged and the FND-006 guards stay green. - Each engine renders all nine themes.
B2b (the third line: page chrome follows the reading's theme; the Settings accent is removed) is a second PR. B2a leaves it exactly one thing: the accessor sessionColorThemeId(session) (§4), and it does not touch the :root colour defaults the embed poster reads (src/live/host/LiveHost.css:122, :153, :159, per the chrome-accent map §7).
The one rule. A reading has one theme, on session.presentation (src/core/models.js:310-313), accepted only when its colours match (src/core/session-presentation.js:112-120). Every engine turns that theme into its own value at the moment it is built. An explicit author value wins; the engine's own "no choice" value takes the theme; with no theme, every engine behaves exactly as today. The "no choice" values are named once and tested once: attractor 'white' (the compiler fills it for every reading, src/core/session-compiler.js:264-269; normalizeFieldStyle too, src/core/visual-style-definitions.js:124), Klee 'random' (:133), Harmonograph climate 'auto' (:147), plate palette 'auto' (:150, :153).
What this changes for readers. Only readings that carry a theme today: Jev and Kev answers, rolls and Today, the two samples, and Composer. Composer renders byte-for-byte as today (its cues already carry explicit rows, src/core/rise-current.js:217, :237, and it runs no Gallery engine, :268); a test proves it for all nine themes. Library, Keystone, Chapel, Journey, Workshop and Reader-setup readings carry no theme and do not change.
Module. src/core/theme-engine-map.js, beside src/core/rise-current.js. Not inside it: the Worker imports rise-current.js to admit Currents (worker/mcp-server.mjs, worker/mcp-gate0.mjs; grep) and must not learn engine knowledge.
Imports, read-only. JEV_COLOR_THEMES (src/core/jev-color-themes.js:9) for key order; RISE_CURRENT_THEMES (src/core/rise-current.js:122-132) for the attractor and Genesis columns by reference, so the two tables cannot drift and Composer invariance holds by identity; validateFlameRecipe (src/core/flame-recipe.js:137) for the one derivation it owns. No visuals import. First load: rise-current.js is not in the shell (src/app.js:13 imports only session-compiler.js statically; rise-current.js is imported by src/live/** and the Worker only, grep), and every importer of the map is a lazy chunk (§4), so measure:first-load (scripts/measure-first-load.mjs:40, :47-58) should not move. CI is the check.
Rule for cells. Engine ids, plus an engine's own dial where the engine has no neutral native (two sat values for silver, one for classic). Never a hex colour: colours come from JEV_PALETTES through sessionColorTheme, as rise-current.js:118-121 already says.
One full entry.
jade: Object.freeze({
attractor: RISE_CURRENT_THEMES.jade.attractor, // { system: 'aizawa', palette: 'jade', form: 'bilateral' }, the same object
genesis: RISE_CURRENT_THEMES.jade.genesis, // { preset: 'gravitational' }, the same object; Klee Lines reads it too
turrell: 'ethereal', // a key of Turrell PALETTES (src/visuals/turrell.js:38-88)
neural: 'growth', // a key of NeuralNetwork.palettes (src/visuals/neural.js:26-72)
ostensoria: Object.freeze({ palette: 'teal' }), // Ostensoria generate options (src/visuals/ostensoria.js:369-382)
apparitio: Object.freeze({ palette: 'holo' }), // Apparitio generate options (src/visuals/apparitio.js:110-120)
livingFlame: Object.freeze({ composition: 'verdant-current' }) // the Flame look's opening composition (C1); hue is computed, §3
})The nine rows (attractor and genesis are always the RISE_CURRENT_THEMES rows):
| theme | turrell | neural | ostensoria | apparitio | livingFlame.composition |
|---|---|---|---|---|---|
| classic | dawn | organic | sepia | ember, sat 0.6 | ember-cathedral |
| amethyst | chamber | consciousness | lilac | holo | violet-nebula |
| prism | ganzfeld | consciousness | iris | prism | prismatic-knot |
| ember | ember | organic | ember | ember | ember-cathedral |
| cobalt | twilight | digital | ice | marian | glacial-silk |
| jade | ethereal | growth | teal | holo | verdant-current |
| rose | ganzfeld | organic | rose (new ramp, §3) | marian | solar-bloom |
| citrine | dawn | organic | verdant | ember | solar-bloom |
| silver | void | minimal | ice, sat 0.35 | prism, sat 0.25 | glacial-silk |
Fractal Flames, Harmonograph, Rock Garden, Focal and night streaks have no column: the first three derive from the reading's three colours, Focal already takes the accent, streaks have no theme map (docs/product/CONSOLIDATED-READER.md:295).
Exports.
-
THEME_ENGINE_MAP, frozen at every level. -
themeEngine(themeId, engine): the cell, ornullfor an unknown theme or missing key (own keys only, thejevPaletteshape atsrc/core/jev-palette.js:40-42). -
accentFlameAnchors(colors)lives insrc/core/conductor.js(factored out ofbuildAccentFlamePalette,:652-658, which keeps calling it); the map re-exports nothing of it. -
rockGardenInk(colors)→{ backgroundColor: ground, strokeColor: 'rgba(r, g, b, 0.8)' }from the ink, ornull. -
themedFlameRecipe(recipe, colors)→ the validated recipe withmacros.huerotated so the palette's saturation-weighted mean hue lands on the accent's hue (§3), or the input whencolorsis null. -
themedFlameLookup(lookup, colors)→id => themedFlameRecipe(lookup(id), colors), null-safe, identity when there is no theme. - Beside
sessionColorThemeinsrc/core/session-presentation.js:sessionColorThemeId(session)→session.presentation.colorThemewhensessionColorTheme(session)is non-null, elsenull. One gate, so engines and chrome always agree.
Guard test src/core/theme-engine-map.test.js (one it per line):
-
Object.keys(THEME_ENGINE_MAP)equals[...JEV_COLOR_THEMES]in order. - Every row has exactly the seven keys.
- For every id,
row.attractortoBeRISE_CURRENT_THEMES[id].attractorandrow.genesistoBeRISE_CURRENT_THEMES[id].genesis(identity; an edit to the contract table failsrise-current.test.js:367-381and this test together). -
row.turrellis a key ofnew Turrell(div).palettes(turrell.js:97);row.neurala key ofnew NeuralNetwork(null).palettes(neural.js:26);row.ostensoria.paletteandrow.apparitio.paletteare ids inOSTENSORIA_PALETTES/APPARITIO_PALETTESother thanauto(visual-style-definitions.js:43-62); anysatis in (0, 1];row.livingFlame.compositionis inFLAME_PRESET_IDS(flame-presets.js:144). - No cell is
'auto','random'or'white'. - The module source contains no
#rrggbb(read the file, regex). - Frozen at every level.
-
themedFlameRecipe: for every theme and every non-spectral composition, the saturation-weighted circular mean hue ofpaletteLut(result)(flame-math.js:209-222) is within 15° of the accent's hue;prismatic-knotkeepsmacros.hue0 under every theme;flameStructureKey(flame-recipe.js:178-181),idandnameare unchanged;probeRecipe(result).visibleis true (the pattern atflame-math.test.js:22); withcolorsnull the input is returned.
The B3 engine-lists guard (src/core/engine-lists.test.js:38-40) is not tripped: engine keys are unquoted property names and the module writes no quoted list of three ids.
Precedence everywhere: explicit value wins; the no-choice value takes the theme; no theme means today. "Where lowered" is the last hop, because that is the only place the no-choice value is still distinguishable from an author's choice (the compiler fills white, admission re-derives a decision byte for byte, src/app/jev-reading.js:60-64, and the Director keys on the authored cue, src/visuals/visual-field-director.js:35).
| engine | knob | the nine values | native or derived | where lowered | test |
|---|---|---|---|---|---|
| Attractor (both mounts) |
AttractorField system, palette, form (src/visuals/attractor.js:213-217; unknown palette falls to white, :216). Mounted at Chamber.mountVisualFieldCue (src/components/read/Chamber.js:2421-2427), the cortex's _syncAttractorField (src/visuals/visual-cortex.js:2084-2091, Reader setup's path) and Home's backdrop (src/components/reading-backdrop.js:34-38). |
the RISE_CURRENT_THEMES rows: classic aizawa/gold/mirror, amethyst thomas/purple/kaleido, prism halvorsen/neon/mirror, ember halvorsen/red/bilateral, cobalt thomas/blue/mirror, jade aizawa/jade/bilateral, rose aizawa/rose/kaleido, citrine thomas/citrine/bilateral, silver halvorsen/silver/kaleido (rise-current.js:123-131) |
native, by reference. The whole row, not the palette alone, and only when the palette is white or absent. Reason: the brightness ceiling is pinned per palette-and-form pair (rise-current.test.js:396-424); a theme palette on the config's own form is untested, and one case is brighter than anything shipped: neon on kaleido is about 6.27 against the kaleido ceiling 4.124 (computed by hand with the test's own formula from attractor.js:122-126, not run). When the palette is white, nobody chose the form either (jev-config's kaleido is a code default for "any other palette", src/core/jev-config.js:39; Reader setup's bench is withdrawn, src/core/visual-taxonomy.js:104-122 per the maps). An explicit non-white palette keeps system, palette, form, intensity, speed and streaks. |
mount: const look = !config.palette || config.palette === 'white' ? { ...config, ...themeEngine(theme, 'attractor') } : config at Chamber.js:2421, the same at visual-cortex.js:2084 from this.config.colorTheme, the same at reading-backdrop.js:36 from decision.config.colorTheme. Composer's cues carry explicit rows (rise-current.js:237), so the merge never fires; a no-theme Current has {} and no theme. |
new src/components/read/Chamber.theme-engines.test.js (field classes mocked as reading-backdrop.test.js:7-17 does): for each id, a themed session with { system: 'aizawa', palette: 'white', form: 'kaleido' } mounts the row; { palette: 'purple' } under cobalt stays purple/aizawa/kaleido; a compiled cobalt Current mounts thomas/blue/mirror with and without its theme; no theme + white stays white. visual-cortex.test.js: _syncAttractorField under colorTheme: 'jade' with attractor: { palette: 'white' } builds aizawa/jade/bilateral. reading-backdrop.test.js:56-69 unchanged (explicit blue); add white + cobalt → blue row. attractor.test.js:88-95 unchanged. |
| Genesis (KleeField) |
preset (src/visuals/klee-field.js:53; mounted at Chamber.js:2404). The preset fixes composition and the four stroke colours together (src/visuals/klee-enhanced.js:18-51; src/core/conductor.js:848-859). No colour-only knob survives a redraw (klee-enhanced.js:384, :403-404), and none is added. |
classic harmonic, amethyst chaotic, prism chaotic, ember twittering, cobalt architectural, jade gravitational, rose harmonic, citrine twittering, silver architectural (rise-current.js:123-131): five schemes for nine themes, exactly as Composer draws them |
native, by reference | mount: preset: config.preset && config.preset !== 'random' ? config.preset : (themeEngine(theme, 'genesis')?.preset ?? 'random') at Chamber.js:2404. A theme therefore locks the preset, so the per-episode picker (klee-field.js:140-148) never runs under a theme and colour cannot drift between episodes (R6). Composer's preset is explicit, so unchanged. |
Chamber.theme-engines.test.js: jade + random → gravitational; cobalt Current's architectural → architectural with and without the theme; no theme + random → random. klee-field.test.js:32-39 unchanged. |
| Klee Lines (cortex) |
config.kleePreset (visual-cortex.js:269), forwarded to kleeFlashes.configure (:2314-2318) and beginSession (:1007-1009); reset by every procedural klee cue to cue.config?.preset || 'random' (:623-625). |
the same nine presets | native, by reference | render, at the two hand-off points through one helper _kleePreset() = kleePreset !== 'random' ? kleePreset : (themed genesis preset ?? 'random'), so the cortex still records what the cue said and every Jev phase's normalised random (src/core/visual-program.js:113-114) means "the theme chooses". 'colorTheme' in nextConfig joins the forward condition at :2314 so a phase theme re-configures. No queueKleePreset guard: that path is dormant (FLASHING_ENABLED false, src/core/visual-presence.js:43 per the maps). |
visual-cortex.test.js beside :232-249: beginSessionVisualIdentity({ colorTheme: 'rose', kleePreset: 'random' }) → configure({ preset: 'harmonic', ... }); applyCue klee with preset: 'chaotic' → chaotic; colorTheme: null → random; after resetSessionVisualIdentity() → random. |
| Turrell | new: generate(paletteKey); const paletteKey = this.palettes[palette] ? palette : this.choose(Object.keys(this.palettes)) at src/visuals/turrell.js:292. One changed line; the plan already records the key (:316). |
classic dawn (core 38°), amethyst chamber (258°, "aligned to the app's own accent"), prism ganzfeld (326°), ember ember (32°), cobalt twilight (body 244°), jade ethereal (176°, the only cool-green), rose ganzfeld, citrine dawn, silver void (s16) | derived: nearest native core hue (turrell.js:38-88; no core lies between 60° and 160°). Two pairs share: classic/citrine, prism/rose. No new palettes: the unthemed random pool stays seven. |
render, per Gallery still: this.turrell.generate(this._themed('turrell')) at visual-cortex.js:1693. The export stage keeps generate() (seeded random, per the maps). |
turrell.test.js: generate('ethereal').palette === 'ethereal' for every mapped id; an unknown key still varies; :168-179 unchanged. visual-cortex.test.js:2181-2190 repeated with colorTheme: 'jade': the turrell spy receives 'ethereal'. |
| Fractal Flames |
FractalFlame.setColorTheme(colors) fed by flameColors (visual-cortex.js:2327-2328; src/app/chamber-session-factory.js:320; Home reading-backdrop.js:56); buildAccentFlamePalette (conductor.js:640-659). |
all nine from the reading's ground, accent and ink, as today | derived, unchanged | session entry and phase, as today. Only refactor: the five anchors at conductor.js:652-658 become exported accentFlameAnchors(colors), shared with the Harmonograph. |
conductor.test.js:419-433 unchanged; a new literal before/after check of palette[64] and palette[255] per theme, captured on the unrefactored code (§7, PR1). |
| Neural | new: generate(paletteKey); the key replaces the random pick at src/visuals/neural.js:89-91 when it is a key of this.palettes. One changed line. |
classic organic (nodeCore ~34°), amethyst consciousness (~249°), prism consciousness, ember organic, cobalt digital (~195°), jade growth (~160°), rose organic (340° is nearer 34° than 249°), citrine organic, silver minimal | derived: nearest native hue; organic carries four themes. Neural is demoted to Make (CONSOLIDATED-READER.md:294), so no look depends on it. |
render: this.neural.generate(this._themed('neural')) at visual-cortex.js:1726; export unchanged. |
new case in visual-cortex.test.js or a small neural.test.js: generate('growth') sets currentPalette to palettes.growth; sixty bare calls visit at least three palettes. |
| Rock Garden |
renderRockGarden options backgroundColor and strokeColor (src/visuals/rockgarden.js:368, :391); every caller passes constants (visual-cortex.js:1759-1763). No engine change. |
ground = the theme ground, stroke = the theme ink at 0.8: classic #08090F / rgba(244, 238, 228, 0.8), … silver #111215 / rgba(245, 246, 248, 0.8) (jev-palette.js:3-11); under a roll the ink is JEV_INKS (:15-25), the same ink the chrome shows |
derived from flameColors (the session's colours, not jevColors(id), so engine ink matches chrome ink under rolls, src/core/roll.js:161). Greyscale by design stays in spirit: every ink is a near-white tint. |
render: { ...(rockGardenInk(this.config.flameColors) ?? { backgroundColor: ASCII_BACKGROUND, strokeColor: 'rgba(232, 232, 236, 0.8)' }), brushStroke: true } at visual-cortex.js:1759-1763. |
visual-cortex.test.js: flameColors: JEV_PALETTES.ember → spy receives backgroundColor '#1C0B0A', strokeColor 'rgba(255, 240, 228, 0.8)'; null → today's two literals. |
| Iris Plates (ostensoria) | existing options.palette veto on generate and beginBake (src/visuals/ostensoria.js:186, :208, :369-371; it also pulls bands and sat from the preset, :371, :377-378) and the existing options.sat dial (:378). Plumbing that does not exist today: PlateField option look and setLook(look); cortex keys ostensoriaPalette and apparitioPalette; Ostensoria.setLook(options) for the Page-still queue (:271-293, bakes with no options at :282-283). |
classic sepia, amethyst lilac, prism iris, ember ember, cobalt ice, jade teal, rose rose, citrine verdant, silver { ice, sat 0.35 }
|
native ids. One addition: a rose ramp (RAMPS row and PALETTES preset in ostensoria.js:43-65, an OSTENSORIA_PALETTES entry at visual-style-definitions.js:43-54), because the Iris look is rose (CONSOLIDATED-READER.md:260) and no pink ramp exists. Proposed anchors, wrapping black to black like the others: [[0,[0,0,0]],[0.16,[0.35,0.05,0.18]],[0.36,[0.80,0.15,0.40]],[0.54,[1.0,0.40,0.60]],[0.70,[1.0,0.72,0.82]],[0.86,[1.0,0.93,0.96]],[1,[0,0,0]]], preset { hue: 0, bands: 1.25, sat: 0.88 }. Authored colour: rendered before merge (§7). Silver uses the sat dial, not a ramp. One ramp per theme, not a rotation set: variety survives through each plate's own ramp offset (:404, :503 per the maps) and a set would read as two colours (R6). |
bake: PlateField._startBake and _takeEngine pass this.look?.[id] || {} as the third argument (src/visuals/plate-field.js:324, :327, :361); setLook compares by JSON and, on change, _abortBake() then _startBake() when running (the setFamilies shape, :509-517), so a new palette lands on the next plate, not one plate late. The cortex's _syncPlateField calls setLook({ ostensoria: this._plateLook('ostensoria'), apparitio: this._plateLook('apparitio') }) after setFamilies (visual-cortex.js:2058); _plateLook(family) returns { palette } for an explicit key other than auto, else the themed cell, else null. _syncPlateField runs on every config change while the presentation is continuous (:2298-2301 → :1884 → :1913-1918). Page stills: updateConfig forwards _plateLook('ostensoria') to this.ostensoria.setLook, which flushes the queue on change (_generation++, queue = [], as beginSession does at :261-264) and bakes with the look at :283. Home's backdrop passes look from the decision's theme (reading-backdrop.js:43). |
ostensoria.test.js:37-62 unchanged; add setLook flushes the queue on change and keeps it on an equal look. plate-field.test.js (spies at :33-38): with look: { ostensoria: { palette: 'teal' } }, beginBake and generate receive it as the third argument on the first and every later bake; without look they receive {}; setLook with a new value aborts the pending bake and starts another for the same cursor; an equal value keeps it. visual-cortex.test.js: applyCue({ kind: 'procedural', collections: ['ostensoria'], config: { palette: 'ember' } }) sets config.ostensoriaPalette 'ember' and _plateLook('ostensoria') is { palette: 'ember' }; auto + colorTheme 'jade' → { palette: 'teal' }; neither → null; both resets clear the keys to 'auto'. |
| Spectral Plates (apparitio) | existing options.palette (src/visuals/apparitio.js:119; unknown ids become prism, :136) and options.sat (:116). Same PlateField and cortex plumbing as Iris; the still at visual-cortex.js:1745 passes this._plateLook('apparitio') || {}. The hue option is a shift along the ramp, not a hue rotation (:35-39), and is not used. |
classic { ember, sat 0.6 }, amethyst holo (its lavender stop ≈267° is amethyst's 265°), prism prism, ember ember, cobalt marian (the blue half), jade holo (mint stop), rose marian (its pink [1.0,0.45,0.60] ≈344° is rose's 340°), citrine ember (gold), silver { prism, sat 0.25 }
|
native ids, four ramps for nine themes: amethyst/jade share holo, cobalt/rose share marian, classic/ember/citrine share ember. Crown, sparkle and spine highlights stay near-white (apparitio.js:258-338 per the maps). Today every live Spectral plate is prism (the auto default), so the ember temper's plates change from prism to ember: that is Q6's point. |
bake, as Iris. The export stage already honours an explicit palette (src/core/render/chamber-stage.js:659-681 per the maps) and is left alone. |
apparitio.test.js (no palette assertion today, :9-38): add generate(null, 'SEED', { palette: 'ember' }).look.palette === 'ember', default stays prism. The plate-field and cortex tests above run with family apparitio. |
| Harmonograph | new: options.anchors on planHarmonograph (src/core/conductor.js:744-783): anchors: !requested && isAnchors(options.anchors) ? options.anchors : FLAME_PALETTES[paletteName] at :767. A replacement, never a mutation: plan.anchors points at the shared, unfrozen FLAME_PALETTES arrays (:577-590) that the flame and Klee also read. Harmonograph.generate already passes options through (src/visuals/harmonograph.js:61-63); the renderer reads colour only from plan.anchors stops 1-4 (:32-44, :126, :133, :150). HarmonographField gains getAnchors beside getClimate (src/visuals/harmonograph-field.js:46-48) and passes anchors: this.getAnchors?.() with the climate at :211-213. No rng draw is added, so the figure is unchanged. |
accentFlameAnchors(colors): five RGB triples from the reading's colours (conductor.js:652-658), the same anchors the flame burns |
derived, colour only. Never the climate: a climate pins the chord family (:714-721, :762) and changes the figure's later draws (:757-763), so amethyst would be tied to stormViolet's tritone weave, the opposite of the Nocturne look, and rose would draw amber; six climates cannot name nine themes; and the chord stays free for C4. An explicit climate stays a full veto of palette and chord (:753-754; harmonograph.test.js:67-89). |
render, per figure: the cortex supplies getAnchors: () => accentFlameAnchors(this.config.flameColors) at visual-cortex.js:2008-2015 and passes the same anchors on the still at :1731-1733; planHarmonograph ignores them under a requested climate. The ground stays KLEE_CHAMBER_BACKGROUND (harmonograph-field.js:131 per the maps). |
harmonograph.test.js beside :67-89: planHarmonograph(null, rng, { anchors }) gives plan.anchors toBe(anchors) and the same ratio, damping, rotary, rotation as the same seed without anchors; { climate: 'midnightWater', anchors } keeps midnightWater's anchors; FLAME_PALETTES.emberDawn is unchanged after the call. harmonograph-field.test.js (generate spy at :20): getAnchors reaches generate's options. visual-cortex.test.js: flameColors: JEV_PALETTES.amethyst → the harmonograph spy receives anchors equal to accentFlameAnchors(JEV_PALETTES.amethyst); null → undefined. |
| Living Flame | the recipe's macros.hue, −180..180 (src/core/flame-recipe.js:39), turned by rotateHue about the grey axis in paletteLut (src/visuals/living-flame/flame-math.js:192-222), which both the GPU and the CPU still sample. A cue carries the full recipe and nothing else (flame-recipe.js:203-227), so the theme is lowered where the cue is built: themedFlameRecipe(recipe, colors). |
hue = wrap(hue(accent) − saturation-weighted mean hue of recipe.palette), computed at runtime for whatever composition the director picks. On the default compositions the offsets are about: classic +12 (cathedral ≈25° → 37°), amethyst −2, prism 0, ember −2, cobalt +13 (glacial ≈193° → 206°), jade +18, rose −55 (solar ≈35° → 340°), citrine +21, silver +21 (computed by hand from the hex values; not rendered). A spectral palette (hue spread above 150°, the Prismatic Knot, flame-presets.js:137) keeps hue 0: rotating a spectrum does not make it rose. |
derived (hue); the default composition per theme is native (flame-presets.js:18-142): the proposal's six (CONSOLIDATED-READER.md:308) plus classic → ember-cathedral and cobalt → glacial-silk (nearest hue, smallest rotation) and rose → solar-bloom (−55 against the cathedral's −45; chosen so classic, ember and rose do not all open on one structure). A theme picks the look's opening composition (C1) and colours every composition; it never picks structure for the director, which is C4's (Q4). |
cue build, at the three builders, one function per reading: ensureDirector (src/core/passage-visuals/reading-state.js:105) becomes flameRecipe: themedFlameLookup(flamePreset, sessionColorTheme(session)); the Workshop export (Chamber.js:2133) and the Page fallback (Chamber.js:3984-3985) take the same lookup, so a saved project and a Page still are exact. Not at mount (Chamber.js:2463-2470): a saved cue must draw as saved (flame-recipe.js:198-202). Held, Lab and Workshop recipes are the author's and untouched. id, name and flameStructureKey are unchanged, so compileTreatmentCue's id guard (src/core/passage-visuals/treatments.js:81) and morphs (field.js:342-344) hold. Reader-visible today: nothing. Follow text is the default only on unthemed shelves (reading-state.js:18, :51-53 per the maps), so B2a readies the Flame look without changing any reader's flame. |
theme-engine-map.test.js (§2). reading-state.test.js: ensureDirector on a jade session yields a director whose flameRecipe('verdant-current').macros.hue is the jade offset and whose structure key equals the preset's; on an unthemed session it equals flamePreset(id). workshop-export.test.js:35 pattern: an exported project's flame cues carry the themed recipe. flame-math.test.js:14-33 unchanged (presets are not edited). |
| Focal (standard glyphs) | CSS: .focal-icon paints var(--color-threshold) with a glow from var(--color-accent-rgb) (src/components/read/Chamber.css:1240-1245 per the maps), both written from the theme accent by applySessionColors (Chamber.js:895-897). No JavaScript knob. |
the nine accents (jev-palette.js:3-11) |
native, already | mount (chrome), unchanged. The no-theme fallback once the Settings accent goes is B2b's. |
Chamber.jev-colors.test.js:9-21 gains a loop over the nine ids: --color-threshold equals JEV_PALETTES[id].accent. |
| Night streaks |
config.streaks === true on an attractor cue (Chamber.js:2411-2413); fixed neon in JS (src/visuals/night-streaks.js:15-18, :42) and CSS. |
none (CONSOLIDATED-READER.md:295) |
none | no change. Under the attractor rule, streaks come only with an explicit neon palette (jev-config.js:37-38; the sample), and an explicit palette keeps everything, so a themed row never carries streaks and streaks never sit under a non-neon filament. |
e2e/night-drive.spec.js:22-35 unchanged; Chamber.theme-engines.test.js: a prism session with the night-drive config mounts streaks and keeps speed 2.4. |
Today. The theme is one object, session.presentation = { colorTheme, textColor?, backgroundColor?, colors }, written by Jev and Kev (src/core/jev-config.js:57-65), rolls and Today (src/core/roll.js:161-162, :170), Composer (rise-current.js:270) and the samples; Reader setup carries it unread (ChamberOrbital, per the theme-flow map); the Session stores it as-is (models.js:310-313); the next Archive division keeps it (session-successor.js:57 per the maps). The Chamber accepts it only when colors equals jevColors(colorTheme, textColor, backgroundColor) (session-presentation.js:112-120), paints six variables on its pane (Chamber.js:882-900) and hands the colours to the fractal at session entry (chamber-session-factory.js:320 → visual-cortex.js:2327-2328). Why a cue refuses a theme: rise.experience-program.v1 admits only the keys a cue's kind allows (src/core/experience-program.js:339-356, onlyKeys at :122-128), so a colorTheme key fails PROGRAM_UNKNOWN_FIELD; a field cue's config is opaque metadata (:380-384), which is how Composer carries a theme's effect, baked as explicit engine rows (rise-current.js:217, :237), never the theme. The legacy visualProgram keeps colorTheme on procedural cues only (visual-program.js:112), which Jev's dual and triple arcs use (src/core/jev-sequence.js:117-121); the Chamber applies it at each phase (Chamber.js:2262, :910-925).
After B2a: nothing new is stored. No field joins a cue, a program, a Current or a decision. The theme stays on session.presentation; the lowering is a pure function of (cue, sessionColorThemeId(session), the map), so replay is deterministic and a saved program replays exactly. Five consumers:
-
Chamber.mountVisualFieldCuereadssessionColorThemeId(this.session)at mount for the attractor row and the Genesis preset (§3). Both entrances end there:initializeGenesisandinitializeAttractorcall the Director directly (Chamber.js:2628-2646), scheduled cues arrive through it too (:2276). The Director keys on the authored cue (visual-field-director.js:35); the theme is constant for the reading, so no key change. -
The session boundary.
chamber-session-factory.js:310-348addscolorTheme: sessionColorThemeId(session)besideflameColors(:320), plusostensoriaPalette: 'auto'andapparitioPalette: 'auto'. Both identity functions clear them (visual-cortex.js:688-705and:715-734, whereflameColors: nullalready sits at:703, :731), because the cortex is a singleton andharmonographClimateshows what an unreset key does to the next reading (the Navigator preview draws with the previous reading's climate,:1731-1733, per the plates verification). The wiring guardsrc/content/imagery/imagery.test.js:246-249gains the three names. -
The cortex's points of use (§3):
_themed(engine)=themeEngine(this.config.colorTheme, engine);flameColorsfor the derived engines. -
Follow text's director (
reading-state.js:105), the Workshop export (Chamber.js:2133) and the Page fallback (Chamber.js:3985) build flame recipes throughthemedFlameLookup. -
Home's backdrop (
reading-backdrop.js:30-82) readsdecision.config.colorThemefor the attractor row and the plate look, so the preview matches the reading it opens. The module is a dynamic import from Home (src/components/Home.js:326, preloaded bysrc/app/home-preload.js:22), so the map andrise-current.jsjoin a chunk the budget does not count (measure-first-load.mjs:47-58countsindex.html's own tags and theHome-*chunks).
Jev arcs and R6. A dual or triple arc changes colorTheme per phase (jev-sequence.js:103-121), and the Chamber repaints chrome and the fractal (Chamber.js:910-925). B2a keeps that path (the decision record gives R6 to C4, decisions.md:12) and makes the engines follow with the chrome: applyScheduledColorTheme replaces its direct fractal call (:923) with visualCortex.updateConfig({ colorTheme, flameColors: sessionColorTheme(this.session) }, { preservePresentation: true }), which already forwards flameColors to the fractal (visual-cortex.js:2327-2328) and now re-resolves Klee Lines, the plates (setLook aborts the bake under way, so plates are not one plate late) and the Harmonograph's next figure. So a Jev arc still changes colour mid-reading until C4, and chrome, flame, plates and lattice change together. Ending per-phase colour now is owner question M3, with the test inversions listed in §6 ready. Composer cues carry no colorTheme (field cues drop it, visual-program.js:120-129).
Readings with no theme (Library, Keystone, Chapel, Journeys, Workshop, Reader setup with nothing carried in): sessionColorThemeId is null, every resolver returns the engine's own value, and every engine behaves exactly as today: white filament, random Genesis, cycling Klee presets, random Turrell and Neural, the reliquary/ice coin toss, random climates, mood flames. B2a gives them no default theme; what their chrome shows once the accent is gone is B2b's decision, and the default look's colour (Gallery, classic) arrives with C1.
Reader setup colour control: not in B2a. Reader setup has no theme control today (theme-flow map §3, confirmed). The proposal places Colour inside C1's Customize look sheet (CONSOLIDATED-READER.md:426, :447-456), and C2 cuts the first screen to eight controls. B2a makes that control a one-line write: set presentation = { colorTheme, colors: jevColors(colorTheme) } into ChamberOrbital's config, which is already saved and sent at Begin, and every engine follows through the carriage above. Until C1, a theme reaches setup only by Adjust from Home. The cost: B2a's acceptance is exercised through Jev, roll, Today, Composer and Adjust readings, and the tests use compiled sessions directly.
Quality changes (R3). Nothing here touches a quality tier: the attractor's steps never change system, palette or form (CONSOLIDATED-READER.md:530), the flame's tier change keeps the recipe and its LUT, and the plates bake a finished still.
Kept, with the reason:
-
ember: the theme (
jev-palette.js:6); Iris and Spectral ramps (ostensoria.js:46;apparitio.js:32); the label of climateemberDawn(visual-style-definitions.js:35); a Turrell palette (turrell.js:68); the compositionember-cathedral(flame-presets.js:20-21); the roll temper (roll.js:97-102). Engine ids are the engines' own vocabulary and saved Workshop cues carry them (normalizeProceduralStylewould turn a renamed id intoauto,visual-style-definitions.js:150). Where a theme id meets an engine id in the map they mean the same thing (ember → ember, ember, ember). The temper becomes the Iris look in C1 (Q2). -
nocturne: a temper (
roll.js:79-84) and a soundscape (jev-config.js:5); names no palette, climate or composition; C1 and C5. -
prism: the theme and a Spectral ramp (
apparitio.js:30); mapped to each other deliberately. -
jade, rose, citrine, silver: themes and attractor palettes of the same name (
visual-style-definitions.js:16-19); mapped to each other byRISE_CURRENT_THEMES.roseis also a focal glyph (:72), prism's ink name (jev-palette.js:31) and, after B2a, an Iris ramp: a colour word used by an engine for the same colour, theemberpattern.jadeis also the gecko accent's label (chamber-accent.js:24per the maps): goes with the accent in B2b. - lilac, teal, ivory: Iris ramps and ink names, or accent labels; the accent labels go in B2b.
-
themeas a parameter:KleeEngine.generateRandom(theme = 'harmonic')means a preset (klee-enhanced.js:1299per the maps). Left; mentioned so the reader does not take it for a colour theme. The new module isTHEME_ENGINE_MAP, not another…THEMES, and its keys are registry engine ids plusattractor,genesisandlivingFlame, never display names.
Retired: nothing in B2a. The second colour vocabularies a model still sees (Jev's visualPalette and kleePreset menus, src/core/decision/recommend.js:80-94; the flame treatment criteria, treatments.js:21-32) are owner question M1.
Fixed contracts untouched. rise.current.v1, rise.experience-program.v1, the compiler input, RISE_CURRENT_THEMES and RISE_CURRENT_VISUALS, ATTRACTOR_VISUAL_MANIFEST, validateJevRecommendation and the admission surface: no file under src/live/**, worker/**, src/core/rise-current.js, src/core/jev-palette.js, src/core/jev-color-themes.js, src/core/visual-control-contract.js, src/core/decision/**, src/app/jev-reading.js, src/core/roll.js (S2, answered 2026-10-05: its signal and garden temper lists are edited after all; see §8) or src/core/session-compiler.js is edited.
Guards that stay green, by path:
-
src/core/rise-current.test.js:49-62, :263-290, :336-365, :367-433(the FND-006 table, brightness ceiling, frozenness): unchanged; the map holds the rows by identity. -
src/core/rise-current.test.js:234-253(the Composer cue reachesapplyScheduledVisualCueunchanged): unchanged; B2a lowers insidemountVisualFieldCue, never by rewriting the cue. -
src/visuals/attractor-manifest.test.js,src/core/jev-palette.test.js:27-31: unchanged. -
src/visuals/attractor.test.js:88-95(ten palette ids),src/visuals/turrell.test.js:168-179,src/visuals/ostensoria.test.js:37-62,src/visuals/harmonograph.test.js:58-65, :67-89,src/visuals/klee-field.test.js:32-39,src/visuals/living-flame/flame-math.test.js:14-33,src/core/decision/recommend.test.js(menus),src/core/visual-catalog.test.js,src/core/engine-lists.test.js:38-62: unchanged and green. -
src/components/read/Chamber.jev-colors.test.js:9-21, :23-38, :40-50, :52-67andsrc/components/read/Chamber.journey.test.js:264-284: assertions unchanged (:52-67passes becauseupdateConfigforwardsflameColorsto the fractal).src/core/jev-sequence.test.js:52-53,e2e/jev-sequence.spec.js:52-56,e2e/jev-steering.spec.js:128-130,e2e/night-drive.spec.js:22-35: unchanged. -
src/components/reading-backdrop.test.js:56-69, :71-81, :102-121: unchanged.
Tests that change or are added, by path and how:
- NEW
src/core/theme-engine-map.test.js: §2. -
src/core/session-presentation.test.js:21-38: addsessionColorThemeId: the id when the colours match, null when they do not or there is no presentation. -
src/core/conductor.test.js:419-433: add the literal before/after check ofbuildAccentFlamePalette(JEV_PALETTES[id])[64]and[255]for the nine themes, andaccentFlameAnchorsreturns five triples. - NEW
src/components/read/Chamber.theme-engines.test.js: the attractor and Genesis cases of §3 including the Composer-exactness loop (compileRiseCurrentfor each of the nine themes; the attractor and genesis cues mounted with and without the theme give the sameAttractorFieldoptions andKleeFieldpreset; cobalt's literals thomas/blue/mirror and architectural). -
src/visuals/visual-cortex.test.js:232-249: add the plate keys,_plateLookprecedence,_kleePresetresolution, Rock Garden literals, Harmonographanchors, and thatbeginSessionVisualIdentity({})andresetSessionVisualIdentity()setcolorThemenull and the plate keysauto.:2181-2190: a second run withcolorTheme: 'rose',flameColors: JEV_PALETTES.rose, asserting what each engine spy receives (turrell'ganzfeld', neural'organic', harmonographanchors, rockgarden the rose ground and ink, apparitio{ palette: 'marian' }, ostensoriasetLook{ palette: 'rose' }, kleeFlashesconfigurepresetharmonic). -
src/content/imagery/imagery.test.js:246-249: the forwarded-field list gainscolorTheme,ostensoriaPalette,apparitioPalette. It fails until the factory adds them: the guard doing its job. -
src/components/read/Chamber.jev-colors.test.js::9-21gains the nine-theme Focal loop;:52-67gainsvisualCortex.config.colorTheme === 'prism'after the phase. -
src/components/reading-backdrop.test.js: add white + cobalt → blue row; an ember plate decision passeslook: { ostensoria: { palette: 'ember' } }. -
src/visuals/turrell.test.js,src/visuals/apparitio.test.js,src/visuals/ostensoria.test.js,src/visuals/plate-field.test.js,src/visuals/harmonograph.test.js,src/visuals/harmonograph-field.test.js, a small Neural case,src/core/passage-visuals/reading-state.test.js,src/core/passage-visuals/workshop-export.test.js: the additions in §3. -
src/core/visual-style-definitions.test.js: if it pins the Iris list,rosejoins it (B3'svisual-catalog.test.js:142pins attractor and Klee options only, per the maps). -
docs/specs/ARCHITECTURE.md§3: the generated diagram prints module counts (scripts/build-architecture-diagram.mjs:142-148), so PR1 runsnpm run docs:diagramand commits the result (CI requires no diff,.github/workflows/ci.yml:41-42).src/core/system-design.test.js:123-140forbids core and visuals importing components; the map obeys. - For the coordinating agent, not this PR (AGENTS.md keeps
.github/workflows/*with it): addsrc/core/theme-engine-map.test.jsandsrc/components/read/Chamber.jev-colors.test.jsto the pull-request vitest list (ci.yml:44-77), ande2e/theme-engines.spec.jsto the sharded suite.
The Workshop plate palette bug is fixed here. The Workshop offers and saves an Iris or Spectral palette per passage (src/components/make/Workshop.js:1555-1564, :4001-4006), the program keeps it (visual-program.js:113-114), and the live cortex drops it: applyCue copies only the Klee preset and the climate (visual-cortex.js:622-628), PlateField bakes with no options (plate-field.js:324, :327, :361), and only the dev-only MP4 stage applies it. The theme needs exactly that wire, and the rule "an explicit author value wins" requires the live path to see the author's value at all. Cost beyond the theme: two lines in applyCue, the two keys in styleChanged (:629-632), and setLook's abort.
Seven pull requests, each small, tests first, each green on CI before the next. Locally run only the named files (npx vitest run <file>); never the suite (owner's rule). Lane: Seth builds (S), Mateo reviews PR1 and PR2 (M owns the ids and Composer parity).
PR1: the map and the accessor (core only). Create src/core/theme-engine-map.js, src/core/theme-engine-map.test.js; edit src/core/session-presentation.js (+ test), src/core/conductor.js (+ test), docs/specs/ARCHITECTURE.md (regenerated).
- Write
conductor.test.js's before/after literal test against the unrefactored code: printbuildAccentFlamePalette(JEV_PALETTES[id])[64]and[255]for the nine ids once, paste as literals, runnpx vitest run src/core/conductor.test.js, green, commit. - Write
theme-engine-map.test.js(§2) and thesessionColorThemeIdtest; run; red (module not found). - Write the module and the accessor; factor
accentFlameAnchorsout ofbuildAccentFlamePalette; run the three files; green. -
npm run docs:diagram; commit the regenerated file. Verification on CI: the pull-request job plus the full suite on main.
PR2: the field renderers at mount (Chamber). Create src/components/read/Chamber.theme-engines.test.js; edit src/components/read/Chamber.js:2397-2427 and src/components/reading-backdrop.js:34-38 (+ test). Interfaces consumed: themeEngine, sessionColorThemeId.
- Write the mount tests (attractor row on white, explicit purple stays, Genesis random → theme preset, Composer-exactness loop over nine compiled Currents, no-theme cases, night-drive keeps speed); run; red.
- Implement the two resolutions at
:2404and:2421, and the backdrop's attractor; green. Verification:rise-current.test.jsstays green (run it).
PR3: the cortex carriage, Klee Lines, Turrell, Neural, Rock Garden. Edit src/app/chamber-session-factory.js:310-348, src/visuals/visual-cortex.js (config defaults :262-270, resets :688-734, _kleePreset at :1008 and :2316, forward condition :2314, _themed, stills :1693, :1726, :1759-1763), src/visuals/turrell.js:291-293, src/visuals/neural.js:89-91, src/components/read/Chamber.js:923; tests imagery.test.js:246-249, visual-cortex.test.js, turrell.test.js, a Neural case, Chamber.jev-colors.test.js. Interfaces produced: config.colorTheme, _themed(engine), _kleePreset().
- Add the three names to
imagery.test.js; run; red. Write the cortex tests (resets,_kleePreset, spies undercolorTheme: 'rose', Rock Garden literals) and the Turrell/Neuralgenerate(key)tests; run; red. - Implement; green. Verification:
visual-cortex.test.js:2181-2190green with and without a theme.
PR4: the plates, the rose ramp and the Workshop fix. Edit src/visuals/plate-field.js (look option, setLook, :324, :327, :361), src/visuals/ostensoria.js (rose ramp and preset at :43-65; setLook and :283), src/core/visual-style-definitions.js:43-54, src/visuals/visual-cortex.js (applyCue :622-632, _plateLook, _syncPlateField :2058, :1745, updateConfig → ostensoria.setLook), src/components/reading-backdrop.js:43; tests plate-field.test.js, ostensoria.test.js, apparitio.test.js, visual-cortex.test.js, reading-backdrop.test.js.
- Tests first (§3); run; red. 2. Implement; green. 3. Render the
roseramp once (the stills script's harness, step PR7) and tune the anchors by eye before merge.
PR5: the Harmonograph's colour. Edit src/core/conductor.js:753-767, src/visuals/harmonograph-field.js:46-48, :211-213, src/visuals/visual-cortex.js:2008-2015, :1731-1733; tests harmonograph.test.js, harmonograph-field.test.js, visual-cortex.test.js. Tests first; the "same figure, new anchors" and "no mutation of FLAME_PALETTES" cases are the point.
PR6: Living Flame. Edit src/core/passage-visuals/reading-state.js:105, src/components/read/Chamber.js:2133, :3985; tests reading-state.test.js, workshop-export.test.js; the hue test already lives in PR1's map test. Tests first.
PR7: the browser check and the render pass. Create e2e/theme-engines.spec.js (Playwright, the sharded suite on main; the gate list is the coordinating agent's). It uses the harness scripts/build-engine-stills.mjs:146-159 already uses: window.__RISE_TEST__.ensureVisualCortex() (src/app/test-bridge.js:21), cortex.init(), then for each of the nine themes cortex.beginSessionVisualIdentity({ colorTheme, flameColors: JEV_PALETTES[theme], activeTypes: [...] }) and cortex._renderContinuousProceduralWork(engine) for klee, turrell, neural, rockgarden, harmonograph, ostensoria and apparitio. It asserts each still is painted (non-ground pixels exist) and, per engine, that the nine stills differ (mean colour of non-ground pixels distinct across themes) and, for the derived engines (rockgarden, harmonograph, fractal), that the dominant hue lies within 25° of the accent. The attractor and Genesis are covered per theme by PR2's mount tests plus the existing themed e2e (night-drive.spec.js, Composer's live-mcp.spec.js); Living Flame by the LUT hue and probeRecipe raster in PR1 (a real CPU draw, the flame-math.test.js:22 pattern). Also: extend scripts/build-engine-stills.mjs with a --themes flag that writes nine stills per engine to a scratch folder (an artifact for eyes, never committed) so the owners run the render-and-tune pass on the B2a branch before PR4 to PR6 merge; everything they tune is one id or one dial in the map.
What the Visual Lab and the benches keep. The native lists stay for authors: the Visual Lab's full Living Flame bench with its hue control (VisualLab.js per the maps), the Workshop's Filament, Climate, Klee climate, Iris palette and Spectral palette selects (Workshop.js:1529-1564), the Navigator's Preset and Climate benches (visual-taxonomy.js:123-126), Settings › Affect (Emotions.js:54-71) and the Visual Catalog manifests. After PR4 a Workshop Iris or Spectral palette reaches the live reading for the first time. The theme map is what readers and models see.
Marked M (Mateo) or S (Seth). Only what changes what a reader gets or is hard to reverse.
-
S1. Under a theme, B2a treats the attractor's
whiteas "no choice" and mounts the theme's full row (colour and shape, the brightness-safe pair). A signal roll draws white one time in three (roll.js:93) and a Jevwhiteanswer is possible: cobalt readings then get thomas/blue/mirror instead of aizawa/white/kaleido, prism readings halvorsen/neon/mirror without streaks (Composer's prism). Alternative: palette only, keeping the config's form, which puts neon on kaleido outside the tested ceiling. Confirm the row, or keep white explicit until C1 rewrites rolls as looks.Answer (Mateo, 2026-10-05): under a theme, 'white' is no choice and the attractor takes the theme's full RISE_CURRENT_THEMES row (system, palette and form).
-
M1. Jev and Kev still answer
visualPalette(ten attractor colours) andkleePreset(recommend.js:80-94) besidecolorTheme, and admission requires both (jev-reading.js:42-43). B2a honours an explicit model answer (a purple filament under cobalt stays purple), so Q6 is met for those readings only when the model answers white or random. Decide whether a later M-lane change derives both fromcolorThemeinsidechoiceConfig(fields kept, so admission passes) and stops asking the model; it changes the evaluation cases andrecommend.test.js's menu pin.Answer: pending.
-
M2 (with S). Jev dual and triple arcs change colour per phase (
jev-sequence.js:103-121;Chamber.js:910-925). B2a keeps it and makes the engines follow the chrome, per the decision record (R6 → C4). Ending it now is one deletion (Chamber.js:2262and:910-925) with these inversions ready:Chamber.jev-colors.test.js:23-67,Chamber.journey.test.js:264-284,e2e/jev-sequence.spec.js:52-56(sessionThemeandcolorbecome the opening theme's),e2e/jev-steering.spec.js:128(#E84BFF→#C8AE83), and the scene demo (classic then prism). It also leaves the model'smiddleThemeandfinaleThemeanswers inert. Now, or C4?Answer: pending.
-
S2. Rolls keep their temper lists until C1, so a signal roll's blue or purple filament and a garden roll's named preset stay under the theme. A one-line alternative in Seth's lane: delete
palettesfrom the signal temper (roll.js:93) and set the garden temper'skleeto['random'](:105), so rolls take the theme now, at the cost of one shift in Today's seeded sequence on the deploy day. Now, or C1?Answer (Mateo, 2026-10-05): delete the signal temper's
paletteslist and set the garden temper's klee list to ['random'] in src/core/roll.js now, so rolls take the theme (one shift in Today's seeded sequence is accepted). -
S3. Every nearest-native pick, the
roseIris ramp and the flame offsets are unrendered. Who runs the render-and-tune pass (PR7's--themesstills) before PR4 to PR6 merge, and is a shared pick (classic/citrine on dawn, amethyst/jade on holo, cobalt/rose on marian, four themes on organic) acceptable for engines no look depends on?Answer: pending.
- Nothing was rendered. Every pick is a formula or a nearest-hue judgement; the render pass (PR7) is the honest check, and every pick is one id or dial in the map.
- Reader-visible changes in themed Reader readings, which are the feature: the ember temper's plates go from prism and reliquary/ice to ember ramps; the nocturne and salon tempers' Harmonograph takes theme anchors over random chords; a white draw or white answer takes the theme's attractor row (S1); Genesis under a theme with
randomlocks one preset (episode variety becomes seed variety); Turrell and Neural lose per-still variety under a theme (R6); Jev arcs still change colour mid-reading until C4 (M2). - Explicit model and temper values stay (M1, S2): Q6 is not fully met for a Jev reading whose model chose a palette, until Mateo decides.
- Genesis colour is preset-level: classic/rose, amethyst/prism, ember/citrine and cobalt/silver each share a scheme, exactly as Composer draws them. C4 must not vary the Genesis preset within a reading, or the colour changes.
- The cortex is a singleton:
colorThemeand the two plate keys must be cleared in both identity functions; the reset test guards it. - Page stills for Iris come from a queue baked with no options (
ostensoria.js:282-283);setLookmust flush it on change or a themed Stream reading opened in Page shows a reliquary or ice plate first (an R6 seam). - First load: the map imports
rise-current.js(13.7 KB raw) into the Chamber and backdrop chunks, not the shell. Ifmeasure:first-loadmoves, the fallback is to hold the two Composer columns by value with atoEqualguard againstRISE_CURRENT_THEMESin the map test. - The generated architecture diagram changes (module count); forgetting
docs:diagramfails CI (ci.yml:41-42). - Engine grounds stay fixed (
#0A0A0Cbaked into plates, Klee and the Harmonograph;[10,10,12]for the flame); the theme ground shows only behind the attractor and the Living Flame, and in the Turrell and Neural palettes' own grounds. Matching grounds is D1's or C4's, because doing it for Genesis changes Composer's pixels. - MP4 export is dev-only and unthemed; the CPU flame still takes the hue (it samples the same LUT), so no renderer disagreement is introduced.
- The guards for pane colour (
Chamber.jev-colors,Chamber.journey) andtheme-engine-map.test.jsrun only on main until the coordinating agent adds them to the pull-request list.
Published from docs/product/discussions/2026-10-05-one-colour-vocabulary-design.md. Edits made here are overwritten on the next push to main.