# A keyframe is the start of a ramp, not a pose that is held **Status:** โœ… `CONFIRMED` against the framebuffer capture of the running title screen โ€” the new rule aligns at **zero shift**, the old one had to be moved. ๐ŸŸก the fallback for groups that never hold is unverified. โ” interpolation between keyframes is still not implemented, only the resting pose. ## The rule that was wrong `Element::rest()` answers "where is this element when the screen is just sitting there", and every composite the port draws depends on it. It used to pick the keyframe with the **largest gap to the next keyframe's time** โ€” the frame that "dwells longest". That reads a keyframe as a value held until the next one. It is not: a keyframe is the **start of a ramp toward the next one**. So a long gap after keyframe *k* means the screen spends that whole time *arriving at* `k+1` โ€” the settled pose is at the **far** end of the gap, not the near one. The title wordmark makes it concrete. `ptlogo1.t32` zooms in from off-screen: ``` kf0 150% (-116, -7) a=0x00 t=26 off-screen, invisible kf1 150% (-116, -7) a=0x00 t=34 kf2 112% ( 109, 123) a=0x80 t=38 kf3 103% ( 165, 171) a=0xc0 t=40 kf4 101% ( 179, 186) a=0xe0 t=42 โ† old rule picked this kf5 100% ( 184, 193) a=0xff t=251 โ† the settled pose kf6 100% ( 184, 193) a=0xff t=264 โ† held here kf7 100% ( 184, 193) a=0x00 t=None fades out ``` The gap 42 โ†’ 251 is by far the largest, so the old rule picked **kf4** โ€” 1 % too large and 5 px up-left, a frame from mid-zoom. The pose the screen actually holds is kf5โ€“kf6. ## The rule that is right **The resting pose is the hold: the longest run of consecutive keyframes with an identical pose.** Ties go to the later run, matching the in โ†’ hold โ†’ out shape. Groups that ramp through every frame and never hold fall back to longest-dwell. ## The measurement `docs/re/captures/title-screen-oracle.png` is a framebuffer capture of the running title screen. It is a **1:1 crop** of the 1280ร—720 frame (1279ร—675) โ€” verified by the copyright line landing on row 669 in the capture and in both composites โ€” so frame coordinates map directly and a shift is meaningful. Edge-correlated over the wordmark box (x 150โ€“1150, y 200โ€“400) with `tools/re-capture/align_to_capture.py`. Gradient magnitude, not colour: the capture's planet is mid-explosion and orange while ours is blue, and the wordmark materials are undecoded, so a pixel diff would measure everything except the question being asked. | resting rule | best correlation | at shift | at (0,0) | |---|---|---|---| | **plateau** (landed) | **0.4597** | **(0, 0)** | 0.4597 | | longest dwell (old) | 0.1511 | (+3, +8) | 0.1268 | The old composite peaks 3ร— lower **and only after being moved** โ€” displaced by about the (โˆ’5,โˆ’7) that kf4-instead-of-kf5 predicts. The new one is already where the game puts it. * [composited with the plateau rule](../captures/ui-layout/title-composited-plateau-rest.png) * [composited with the old longest-dwell rule](../captures/ui-layout/title-composited-longest-dwell-rest.png) ## What else it fixed, on the same screen The old rule systematically picked the **invisible** end of a fade-in. On the title screen it rested these at alpha `0x00`, where the capture plainly shows them: `ptlogo_tm` (the โ„ข), `ptcopyright`, `ptlogo_back2`, `ptlogo_back2eff`, `ptloop01`/`ptloop02`. And `pteff00.prm` โ€” the full-screen fade quad that paints **last** โ€” rested at **opaque black**. That is the whole screen, and it is why `.prm` compositing was blocked ([`ui-prm-primitives.md`](ui-prm-primitives.md)). None of that was visible before, because `compose` never applied the `fade` alpha at all: `blit` modulates by `tint` only, and `tint` is `0xffffffff` on essentially every keyframe. The wrong keyframes were being chosen and then their one distinguishing field was ignored. That is worth stating as its own finding โ€” see below. ## The `fade` alpha, applied (same day) `blit` modulated by `tint` only โ€” `0xffffffff` on essentially every keyframe โ€” so the `fade` word was decoded, stored, and then thrown away. Applying it as an **ARGB** modulate on top of `tint` is what turns the resting pose from an academic result into a picture: | composite | best correlation vs the capture | at shift | |---|---|---| | plateau rest, `fade` applied | **0.9538** | (0, 0) | | plateau rest, `fade` ignored | 0.4597 | (0, 0) | | longest-dwell rest | 0.1511 | (+3, +8) | 0.95 against a framebuffer capture of the running game. [The composite](../captures/ui-layout/title-composited-fade-applied.png) now has the white wordmark with its blue outline, the โ„ข, the copyright, and the orange exploding planet โ€” the last of which appeared because a full-screen blue effect that rests at alpha 0 had been painting over it at full opacity. **ARGB is measured, not assumed.** Across a fade-in the *high* byte walks `0x00 โ†’ 0x80 โ†’ 0xc0 โ†’ 0xe0 โ†’ 0xff` while the low three stay `ffffff`, and the low 24 bits are `0xffffff` on 5 276 of the disc's 5 453 resting keyframes (with `0x000000` on 165 โ€” the `.prm` blacks โ€” and `0x5dc9ff` on 12). Risk checked, because a modulate can only ever remove pixels: it is a **no-op on 4 060 of 5 200** sprite elements, partial on 453, and hides 687 โ€” which are transient HUD indicators (`pb_emergency`, `pb_refilling`, `pbcm1_arrow1`) that should not be lit on a resting screen. **No build is left with nothing visible**, and a disc test asserts it. ### And it caught a bug in the resting rule With `fade` applied, the pause menu lost the word **PAUSE**, which the capture of the running game plainly shows ([`pause-tutorial-real-vs-rebuilt.png`](../captures/ui-layout/pause-tutorial-real-vs-rebuilt.png)). `pgptitle.rat` has **three** runs of two identical keyframes โ€” invisible, visible, invisible: ``` kf0 a=0x00 t=5 kf1 a=0x00 t=13 pre-roll kf2 a=0xff t=23 kf3 a=0xff t=28 the hold kf4 a=0x00 t=30 kf5 a=0x00 t=None the exit ``` A group carries the screen's entry animation **and its exit**. The tie-break "later run wins" grabbed the exit. Fixed: a run ending on the last keyframe is excluded unless it is the only one. The title's correlation is unchanged at 0.9538, and [PAUSE is back](../captures/ui-layout/pause-composited-fade-applied.png). This is why the two changes landed together: the trailing-run defect is invisible โ€” in the literal sense โ€” until `fade` is applied. ## What is not settled * โ” **Blend mode.** Everything above is straight alpha-over. The near-white flash quads and the coloured ones may well be additive, and nothing has been measured; the title capture cannot separate the two because its resting elements are all `0xffffff`. * ๐ŸŸก **The fallback.** Groups with no two adjacent keyframes alike still use longest-dwell. How many there are, and whether the correct answer for them is the *last* keyframe instead, is unmeasured. * โ” **Interpolation.** Only the resting pose is decoded; nothing tweens. A viewer that animates these screens needs the ramp, and whether it is linear is unknown. * ๐ŸŸก One-shot flashes (`ptlogoall_eff`, `ptlogoall_eff2`) ramp 0 โ†’ 0x80 โ†’ 0x4b โ†’ 0 and never hold at a visible value, so the plateau rule rests them at alpha 0 โ€” invisible. That is *probably* right for a settled screen, but the capture cannot confirm it while `fade` is unapplied. ## ๐Ÿ”ด `rest_plateau` is wrong for elements with no exit animation (2026-08-28) Reported by the port agent with a capture that proves it, and it affects `sylpheed-cli screen render` too โ€” this is not only a port concern. `rest_plateau` drops a trailing run of identical keyframes because that run is normally the **exit** animation. On an element that has **no exit**, the trailing run *is* the hold, and dropping it puts the element back at its **first** keyframe โ€” off-position and transparent. **The condition that identifies these exactly** (no false positives across the port's whole export): > the final untimed keyframe has the same pose as the last timed one **Six elements** on `main_menu` match it and `rest()` misses all six. The visible cost: on `main_menu` the timeline render and the `rest` render differ in exactly one region โ€” **400 ร— 470 at (440,108)**, the bounding box of `ptframe1` and `ptframe2` and nothing else. That is the **bright circuit bracket around the menu**, plainly present in [`../captures/main-menu-oracle.png`](../captures/main-menu-oracle.png) and absent from the `rest` render. Cropping the same region from the capture and from both renders puts the ring and its elbow trace pixel-aligned with the game's in the timeline render. This also explains a long-standing โ” on [`ui-paint-order-key.md`](ui-paint-order-key.md): *"`ptframe1`/`ptframe2` rest at `0x00ffffff` (alpha 0) and are therefore not drawn, but the capture shows the menu frame plainly."* Same two elements, same cause โ€” now identified. ## โœ… Fixed 2026-08-28 โ€” but the condition is the ALPHA, not the pose The report's proposed test โ€” *"the final untimed keyframe has the same pose as the last timed one"* โ€” **misfires**, and on the exact case the exclusion was written for. `pgptitle.rat`'s last two keyframes are also identical: ``` pgptitle.rat kf4: fade=0x00ffffff pos=(220,69) t=30 kf5: fade=0x00ffffff pos=(220,69) t=None <- same pose ``` Adopting it as stated would erase the word PAUSE again. What separates the two is **visibility**: | | trailing run | alpha | is it the hold? | |---|---|---|---| | `ptframe1`/`ptframe2` (main menu) | 3 ร— `0xffffffff` at (440,108) | `0xff` | **yes** โ€” no exit animation | | `pgptitle` (pause menu) | 2 ร— `0x00ffffff` | `0x00` | no โ€” it is the fade-out | An exit fades the element out, so its last keyframe is transparent; an element with no exit ends on the pose you can see. **So a trailing run is the hold exactly when it is visible**, and that is what `rest_plateau` now tests. ### Verified against a capture, not against another renderer | check | result | |---|---| | `ptframe1` rest | `(620,108) t=16` โ†’ **`(440,108) t=62`** | | bracket region draws | mean 61.74 โ†’ **62.79** | | **oracle correlation** over that region vs [`main-menu-oracle.png`](../captures/main-menu-oracle.png) | **0.9596 โ†’ 0.9748** | | pixels changed, whole frame | 10 082, bounding box **x 440โ€“839, y 108โ€“577** โ€” exactly the 400 ร— 470 at (440,108) the report predicted | | **regression control**: PAUSE wordmark, 3 pause builds | **unchanged** (2833 / 2858 / 2833 bright px) | โœ… **`cargo test -p sylpheed-formats` with `SYLPHEED_DISC` set: 131 passed, 0 failed** across 6 binaries including the disc-gated ones. (One pre-existing `ignored` โ€” the XBG7 shared-resource test โ€” unrelated.) ### โœ… The disc-wide check Both rules reimplemented over the parsed keyframes of **every** `RATC` bundle on the disc โ€” 2 859 bundles, 13 991 elements with โ‰ฅ 2 keyframes: | | | |---|---| | elements whose `rest` moves | **30** (0.21 %) | | invisible โ†’ visible | **4** | | **visible โ†’ invisible** | **0** โ€” the safety property | The change is surgical and it never hides something that was being drawn. The remaining 26 move `rest` between two *visible* poses (position, not visibility). The 4 revealed are `ptframe1.t32` and `ptframe2.t32` in `GP_TITLE` **entry 5 and entry 8** โ€” the same two elements, once per language build. ### ๐ŸŸก Reconciling with the report's "six elements" On the **English main menu exactly two** elements satisfy the report's condition (last two keyframes identical), and both are `ptframe1`/`ptframe2`. So the six must span its whole 12-screen export, not that one screen โ€” consistent with its own observation that the timeline and `rest` differ *"in exactly one region โ€ฆ the bounding box of `ptframe1`/`ptframe2` and nothing else"*. โ” **Open, and it matters:** if any of the other four have a **transparent** trailing run, this rule deliberately leaves them alone โ€” that exclusion is what protects the word PAUSE. If a capture shows one of them drawn, the alpha rule is incomplete and needs a third discriminator. **Which screens are they on?** --- ## ๐Ÿ”ด The dwell fallback is unsound whenever it actually runs (2026-08-28) `rest()` tries `rest_plateau()` first and, failing that, picks the keyframe with the longest **dwell** โ€” the largest gap `t[k+1] โˆ’ t[k]`. That rule is not sound, and the reason is structural rather than a tuning problem. A gap between `t[k]` and `t[k+1]` is time the element spends **interpolating from pose `k` to pose `k+1`**. Neither pose is *held* during it โ€” unless the two poses are equal, which is exactly a plateau, and the plateau path has already handled that case and returned. **So by the time the fallback runs, it is guaranteed that no pose is held, and the rule is choosing an endpoint of a movement.** ### The element that exposed it `GP_TITLE` build 7, `ptlogo_eff3.t32` โ€” a transient bloom: ``` 46: (98,42) 100%,100% a=0 61: (108,72) 0%,0% a=0 103: (108,72) 200%,200% a=255 r=80 -: (108,72) 0%,0% a=0 r=150 ``` No two adjacent poses are equal, so there is no plateau. The longest gap is `61 โ†’ 103` (42 units), during which the sprite grows from nothing to **200 %** at full alpha while rotating 80ยฐ, then collapses again. The rule returns whichever end of that movement the indexing lands on: | | returned "rest pose" | |---|---| | as decoded | `(108,72) 0%,0% a=0` โ€” invisible | | with `SYLPHEED_KF_TIME_SHIFT=1` | `(108,72) 200%,200% a=255` โ€” the peak | An 896ร—389 sprite at 200 % scale is 1792ร—778 โ€” larger than the screen. Painting it permanently is what made build 7's render 13.1 % different and 4.9 luminance units brighter. **The element has no resting pose.** It is a flash; after it plays there is nothing. Neither answer is *derived* โ€” one of them is merely harmless. ### ๐Ÿ”ด What this retracts Last iteration I reported the build 7 render difference as evidence **against** the keyframe-time shift, on the reasoning that language twins should match in brightness. **Withdrawn.** The difference is not about the time association at all: it is the dwell fallback guessing, and it would guess on this element under any reading of the times. The brightness comparison was measuring a heuristic, not a decode. What that leaves: the case *for* the shift (a factor of 26 on the hold:fade-out ratio, [`ui-keyframe-time-unit.md`](../ui-keyframe-time-unit.md)) is no longer opposed by render evidence โ€” 10 of 11 builds are byte-identical and the 11th differs only through an unsound heuristic. ๐ŸŸก **It is still not adopted**, for a different reason than before: adopting it would flip this element to the visibly wrong answer, so the shift and a decision about what `rest()` should do for plateau-less elements have to land together, and the second half has no capture to verify against either. ### โš ๏ธ What the port should take from this Any element whose keyframes contain **no two adjacent identical poses** has a `rest()` result that is guessed, not decoded โ€” in our renderer and in anything built from it. That is a property a consumer can test for itself in one pass over the keyframes, and it is worth flagging in an export rather than silently inheriting our guess. ## The size of the defect, and why it does not block the menu port [`tools/re-capture/plateau_census.py`](../../../tools/re-capture/plateau_census.py) walks every `GP_*.pak` placement region directly (the CLI route decodes every texture and is far too slow for a disc-wide pass). Its control reproduces `GP_TITLE` build 7's three fallback elements and names `ptlogo_eff3.t32` among them before it counts anything. Output: [`data/plateau-census.txt`](../data/plateau-census.txt). | | | |---|---| | elements with a keyframe group, disc-wide | **15 493** | | no plateau | 3 807 (24.57 %) โ€” โš ๏ธ **overstated, see the correction below** | | โ€ฆof those, **single-keyframe** (trivially at rest, not a guess) | **1 502** | | **genuinely ambiguous** (2+ keyframes, no plateau) | **2 305 (14.88 %)** | | of those, current rule returns an **invisible** pose | 1 711 (44.9 %) | | of those, current rule returns a **zero-scale** pose | **195 (5.1 %)** | | the two candidate rules **agree** | 1 911 (50.2 %) | A returned pose with `scale = 0 %` is not a pose at all, and 195 elements get one. Disc-wide the choice of rule is not cosmetic: the candidates agree only half the time. ### โœ… But on the five screens the port needs, the exposure is one element | screen | plateau-less | rules differ | |---|---|---| | main menu (entry 5) | 5 / 16 | 0 | | `EXTRAS` (entry 6) | 5 / 18 | 0 | | title (entry 4) | 2 / 24 | 0 | | developer splash (entry 11) | 2 / 7 | **1** | Thirteen of the fourteen affected elements get the same answer either way. The one disagreement is `palogo_anima_eff.t32`. ### ๐Ÿ”ด And "rest = last keyframe" is refuted on it That was the alternative I named last iteration. The developer splash carries **three sibling glows**, identical in structure and in every time: ``` palogo_gamearts_eff 15:a=0 30:a=255 45:a=255 -:a=0 โ†’ plateau โ†’ visible palogo_seta_eff 15:a=0 30:a=255 45:a=255 -:a=0 โ†’ plateau โ†’ visible palogo_anima_eff 15:a=0 30:a=255 45:a=212 -:a=0 โ†’ no plateau ``` They differ in **one byte** โ€” `212` where the others have `255`. Under "last keyframe", `anima_eff` alone goes invisible while its two siblings stay lit. A rule that makes one of three parallel elements behave differently because of a single alpha count is producing an artefact, not a decode. The capture agrees weakly. Comparing box means in [`live-splash-developer.png`](../captures/title-builds/live-splash-developer.png) against our render (the screenshot is 1279ร—675, top-aligned, so only ratios are comparable): gamearts **0.717**, seta **0.723**, anima **0.772**. If our render were adding a glow the game does not draw, anima's ratio would sit *below* its siblings'. It sits above. ### ๐ŸŸก Where this leaves it The **defect** is established and measured: `rest()` guesses for **14.88 %** of elements disc-wide and returns a degenerate zero-scale pose for 195 of them. The **fix** is not decided โ€” "last keyframe" is refuted, and the current rule survives on the only captured element that discriminates. โš ๏ธ For the menu port specifically this is **not a blocker**: one element on one screen, and our current answer for it is the defensible one. --- ## ๐Ÿ”ด The 24.57 % was overstated, and my proposed fix is refuted **2026-08-29.** Two corrections, both from one experiment. ### The prediction: draw nothing for an element with no held pose A keyframe group is **entry โ†’ hold โ†’ exit**, and the exit ends invisible โ€” on the five port screens the final keyframe is invisible for **21/24** (title), **8/16** (main menu), **12/18** (`EXTRAS`), **2/3** and **6/7** (splashes). So the screen "as seen" is the *hold*, which is why `rest_plateau` is the right primary rule and why "rest = the last keyframe" would empty every screen. That suggested a fix: an element with **no** hold has no representative pose, so draw nothing rather than guess an endpoint. Tested via `compose`'s `visible` mask (`examples/plateauless_suppression.rs`), correlated against the live captures: | screen | as-is | plateau-less suppressed | ฮ” | |---|---|---|---| | title | +0.9500 | +0.6839 | **โˆ’0.2661** | | main menu | +0.9460 | +0.9037 | โˆ’0.0423 | | `EXTRAS` | +0.9440 | +0.9094 | โˆ’0.0346 | **Refuted, decisively, on all three.** ### Why โ€” and it invalidates the headline number An element with a **single keyframe** has no *adjacent pair*, so the plateau test marks it plateau-less. But a single-keyframe element is not animated at all: its one pose *is* its rest, unambiguously. Suppressing those removes backgrounds and full-screen layers, which is where the title's โˆ’0.27 comes from. The same flaw is in the census this page published: | | | |---|---| | no plateau (as published) | 3 807 (24.57 %) | | โ€ฆof which **single-keyframe** | **1 502** โ€” trivially at rest | | **genuinely ambiguous** | **2 305 (14.88 %)** | So the guessed-rest population is **2 305, not 3 807** โ€” the published figure overstated it by **65 %**. The defect is real and smaller than reported. --- ## โœ… Where the fallback actually fires โ€” and "last keyframe" wins there **2026-08-29.** Two things fell out of scoring rules by rendering instead of arguing about them. ### The port's exposure is TWO elements, not fourteen The guessing fallback is reached only by an element that is plateau-less **and** has more than one keyframe โ€” a single-keyframe element short-circuits at `match len { 1 => first }`. Per screen: | screen | elements | plateau-less | **reach the fallback** | |---|---|---|---| | title (4) | 24 | 2 | **0** | | main menu (5) | 16 | 5 | **0** | | `EXTRAS` (6) | 18 | 5 | **0** | | publisher splash (10) | 3 | 2 | **1** | | developer splash (11) | 7 | 2 | **1** | On the three screens a port cares most about, **`rest()` never guesses at all**. That is why three different fallback rules render builds 4, 5 and 6 to *identical* correlations (+0.9500 / +0.9460 / +0.9440 for every rule) โ€” the code is unreachable there. ### On the two splashes, the last keyframe is markedly better Rendering under each rule (`SYLPHEED_REST_RULE=last|maxalpha`, default off) and correlating against the live captures: | splash | dwell (current) | **last** | maxalpha | |---|---|---|---| | publisher | +0.9600 | **+0.9982** | +0.9600 | | developer | +0.9643 | **+0.9758** | +0.9643 | `maxalpha` is indistinguishable from the current rule; **`last` gains +0.038 and +0.012**, and 0.9982 is very close to exact. ### ๐Ÿ”ด This refutes my own refutation I had killed "rest = the last keyframe" by argument: on the developer splash it makes `palogo_anima_eff` invisible while its two siblings stay lit, which looked like an artefact. **The capture says otherwise** โ€” making it invisible is what improves the match. The sibling symmetry was my expectation, not evidence. โš ๏ธ **The caveat that keeps this honest:** both captures are *single frames of a transient animation*. They fix which pose matches **these reference frames**, not which pose is canonically "at rest" โ€” a grab a second earlier would show the glows. What the measurement supports is: for reproducing the committed captures, use the last keyframe. ๐ŸŸก **Default unchanged.** The rule is better on both screens where it fires and provably identical on the other three, but it would alter the rest pose of **2 305** elements disc-wide and two of them have been measured. Reachable via `SYLPHEED_REST_RULE=last` for anyone who wants to take that further. โœ… Unaffected by the earlier correction: all **195** zero-scale rest poses are inside the genuinely-ambiguous 2 305 โ€” none is a single-keyframe element. ### The disc-wide blast radius โ€” measurable even though the rule is not scorable there Only two ambiguous elements sit on a screen with a live capture, so the rule cannot be *scored* disc-wide. What can be quantified is what changing it would do ([`tools/re-capture/rest_rule_blast_radius.py`](../../../tools/re-capture/rest_rule_blast_radius.py), [data](../data/rest-rule-blast-radius.txt)): | | dwell (current) | `last` | |---|---|---| | the two rules **differ** | **1 896 of 2 305 (82.3 %)** | | | returns an **invisible** pose | 1 711 (74.2 %) | 1 618 (70.2 %) | | returns a **zero-scale** pose | **195 (8.5 %)** | **43 (1.9 %)** | Two things worth reading off this. **It is not a marginal choice.** The rules disagree on 82 % of the affected elements, so "either is fine" is not available. **The current rule produces 4.5ร— more degenerate poses.** A zero-scale pose is *collapsed to nothing* ([rat layout](ui-rat-layout.md)) โ€” an element's **pre-roll**, before it has grown in. A pre-roll is definitionally not a rest, so 195 elements currently rest at a frame the element is only passing through, against 43 under `last`. That is an argument from the data's own structure rather than from the two captures, and it points the same way they do. ๐ŸŸก It remains **indirect**: fewer degenerate results is not the same as more correct results, and `last` still returns an invisible pose 70 % of the time โ€” which for a transient element may well be right, and for a persistent one would be wrong. The default stays put. --- ## โœ… Why `rest_plateau` is right, and why `last` is right only for a transient **2026-08-29.** The shifted keyframe-time reading ([time unit](../ui-keyframe-time-unit.md)) appears to predict something simple: the final pose is reached at a definite time and nothing follows, so "rest" should just be the last keyframe, with no heuristic at all. Tested (`SYLPHEED_REST_RULE=lastall`, bypassing the plateau rule for **every** element): | screen | current | `lastall` | ฮ” | |---|---|---|---| | title | +0.9500 | +0.6819 | **โˆ’0.2681** | | main menu | +0.9460 | +0.6416 | **โˆ’0.3044** | | `EXTRAS` | +0.9440 | +0.5745 | **โˆ’0.3695** | | publisher splash | +0.9600 | **blank** | โ€” | | developer splash | +0.9643 | **blank** | โ€” | The splashes render *entirely empty* โ€” zero variance, so the correlation is not even defined. **Refuted.** ### What the failure shows A group's keyframes describe **entry โ†’ hold โ†’ exit**, and the exit is the screen's *dismissal*. While a screen is on display it has not reached its last keyframe; it is sitting at the hold, and the exit plays only when the screen leaves. That is why: * `rest_plateau` is the correct primary rule โ€” the hold is where the screen lives, and it is what a capture of a displayed screen shows; * the last keyframe is the **post-exit** state, correct only at the moment the screen is already gone โ€” which is why applying it everywhere blanks the splashes; * and it does **not** contradict the shifted time reading. That reading says when each pose is reached; it says nothing about the group being played to completion while the screen is still up. ### And it explains why `last` wins for the plateau-less two An element with **no hold** is a transient: it flashes and is over. At any settled moment it is *gone*, and "gone" is its last keyframe. That is not a general rule about rest โ€” it is what a transient's settled state *is*. The draw capture says exactly this independently: on the developer splash the `_eff` glows are drawn on frames **94โ€“115** and the logos on **116โ€“211**. The glows are already finished when the logos are up, so a capture of the logo moment must show them invisible โ€” which is what `SYLPHEED_REST_RULE=last` produces, and why it scored +0.9982 and +0.9758. โœ… So the picture is coherent across three independent observables โ€” animation timing, static composites, and the per-frame draw log โ€” and the rule it supports is: **plateau where there is one, last keyframe where there is not.**