# Design fixes — home screen and detail pages Audit of 2026-08-01. Findings only; no product code was changed. The three items under "Confirmed layout bugs" were reproduced by rendering the real composables at TV 1080p (`w960dp-h540dp-television-xhdpi`) through the existing Roborazzi harness — everything else is read from the source. **All of it is implemented.** The findings below are left as written — they are the diagnosis, and each one says why the fix is shaped the way it is. What was done: | # | Fixed in | |---|----------| | 1 | `HomeMovieHero.kt` — column padding 28→22dp, and a wrapped title stands the synopsis down (`onTextLayout` line count) so Play is never what gets cut. Captured as `df_home-movie-hero-long-title.png`. | | 2 | `DetailPageComponents.detailPaneHeight()` — the slot is derived from the viewport (250–420dp) instead of a hard 250dp. Every technical spec now renders; `df_detail-pane-cast-details.png`. Studio, which both columns claimed, is dropped from the technical column. | | 3 | `DetailFoldPeek` (34dp) holds the tab strip off the bottom edge. | | 4 | The hero's private `heroFacts`/runtime formatter is gone; it calls `detail/DetailFacts.kt`. `HomeComponents.formatTvRuntime` too — one formatter left in the app. | | 5 | `MembyScore` token, and every rating goes through `ratingLabel` (`Locale.US`). The home metadata panel renders the score as its own run of text so it can carry the same gold. | | 6 | "No favourite shows yet" / "Mark a series as a favourite…". | | 7 | `FactSeparator` between facts, `ValueSeparator` inside a fact that holds a list. | | 8 | `ui/theme/DesignTokens.kt`; `HomeComponents` and `DetailPageComponents` colours are aliases of it, and `Theme.kt` uses the same near-blacks. | | 9 | Same — the detail page picked up the raised TV neutrals. | | 10 | Three radii: `MembyChipCorner` 8, `MembyCardCorner` 10, `MembyPanelCorner` 14. | | 11 | `ui/MembyButtons.kt` — `MembyPlayButton`, `MembyPlayChip`, `MembyChoiceChip`. The hero chip, the detail Play button, the metadata panel's "▶ Resume" and the For You time budget all use them. | | 12 | `DetailHero` honours `Settings.showTitleLogo` (new `EmbyRepository.showTitleLogo`) and shares `useTextTitleForLogo` with the screensaver (`ui/TitleLogo.kt`). | | 13 | One `UHD_MIN_WIDTH` (3800) for the badge and the `(4K)` suffix. Unit-tested. | | 14 | `mediaBadges` reads `dynamicRangeLabel`, so HDR10+ stays HDR10+. Unit-tested. | | 15 | One `FocusRequester` per pane in both overlays; none is attached to two live nodes. | | 16 | A series passes `mediaBadges(item)`, and `DetailFactRow` takes 4 badges so the airing badge is not squeezed out. | | 17 | `HomeRowHeaderIcon` / `HomeRowHeaderIconGap` / `HomeRowHeaderSpacing`, used by `MediaRow`, `MyShowsStrip` and `RecentSearchesRow` — which also gained the vertical padding its focus-scaled chips needed. | | 18 | `HomeHeroPick` carries the row a title was drawn from; the caption is no longer the card's slot. Unit-tested. | | 19 | Deleted (≈260 lines: `HomeHero`, `HomeRow`, `ContentCard`, `HomeRowData`, `HomeRowSkeleton` and the two runtime formatters only they used). | | 20 | The peek under the strip plus a chevron at its end. | | Docs | `CLAUDE.md`'s detail-page section rewritten to describe this code, with the token, button and header conventions above it. | Screenshots of the result are `app/build/screenshots/df_*.png` (`.\gradlew.bat :app:testDebugUnitTest --tests "*ScreenshotTest"`). --- ## Confirmed layout bugs These clip real content on a real TV. Fix these first. ### 1. The featured home hero drops its Play button when the title wraps to two lines `ui/HomeMovieHero.kt:171-236`, `ui/MainActivity.kt:137` (`homeHeaderHeight`) The card's content column measures ~229dp with a one-line title and ~261dp with two. `homeHeaderHeight(540dp, showHero = true)` yields 248dp, minus the hero row's 16dp top and 10dp bottom padding, so the card gets **222dp** — and `FocusScaleContainer` clips it to a 14dp rounded rect. Rendered with a two-line title, the kicker, title, fact line and synopsis draw and the green Play chip is **gone entirely**. A one-line title is already 7dp over budget; it only survives because the part cut off is the chip's shadow. Fix direction: the column is `align(Alignment.CenterStart)` inside a fixed-height box, so overflow is split top and bottom and the button is always the first thing lost. Either give the hero a height derived from its content, cap the title at one line, or drop the synopsis when the title wraps. ### 2. The Cast & Details tab silently discards every technical spec `ui/DetailPageComponents.kt:258-278` (the 250dp pane), `:563-580` (`DetailFocusablePane`), `:613-643` (`DetailCastAndDetailsPane`) The tab content slot is a hard `.height(250.dp)` and `DetailFocusablePane` applies `.clip(RoundedCornerShape(10.dp))`. `releaseAndTechnical` builds Released / Certificate / Runtime and then `addAll(specs)` — Video, Codec, Audio, Subtitles, Studio. Rendered at the real slot geometry, only the first three rows survive; the entire output of `technicalSpecs()` is clipped below the fold of a pane that cannot scroll. That is the whole reason the tab exists. `DetailOverviewPane` shares the ceiling: a five-line synopsis plus the series "Up next" supporting line pushes its credit rows past the same boundary. Fix direction: the pane needs a height budget that accounts for its worst case, or the two-column meta block needs to page/scroll. Note the pane deliberately does not scroll (one screen per tab), so the honest fix is probably fewer rows per column, not a scroller. ### 3. The detail tab strip sits flush against the bottom screen edge `ui/DetailPageComponents.kt:209` (`heroHeight = maxHeight - DetailTabHeight`), `:494-550` The selection underline is cut in half at y=1080 in both `detail-movie-more-like-this.png` and `detail-series-cast-details.png`. On a TV with overscan the underline and part of the labels are off-screen. This is the only element in the app with zero safe-area inset — gutters are 36-58dp and the home clock keeps 18dp. --- ## Copy and formatting ### 4. The home hero formats runtime differently from everywhere else `ui/HomeMovieHero.kt:351` is a private `heroFacts` shadowing `ui/detail/DetailFacts.kt:62`. Different field order (year · certificate · runtime vs year · runtime · certificate) and `"${it}m"` instead of `formatRuntime`. The checked-in `home-movie-hero.png` shows **"2026 • M • 124m"** in the hero and **"2026 • 2h 4m"** on the card directly beneath it. There are three runtime formatters in the app: `DetailFacts.formatRuntime`, `HomeComponents.formatTvRuntime` (private, identical) and this one. ### 5. The community score changes colour and locale by screen Gold `0xFFF5C518` on detail (`DetailPageComponents.kt:484`), grey `MutedText` on home (`HomeComponents.kt:1025`). `HomeMovieHero.kt:355` uses `"★ %.1f".format(it)` with the default locale while every other rating goes through `Locale.US` — a comma decimal in a non-US locale. ### 6. American spelling in two user-facing strings `ui/HomeComponents.kt:1549` "No **favorite** shows yet" and `:1555` "Mark a series as a **favorite** and it'll be waiting here." The rail says Favourites, the quick menu says "Add to favourites", the detail hero action says "Add to Favourites". ### 7. Four separator styles for the same kind of fact list `" • "` (home metadata), `" • "` (detail fact row, schedule metadata, home hero), `" · "` (home genres), `", "` (credit rows). --- ## Design-token drift ### 8. Four near-blacks Theme `background 0xFF0B0E11` and `surface 0xFF101418` (`ui/theme/Theme.kt`, effectively unused), home `0xFF090B0D`, detail `0xFF080A0C`. The accent green is duplicated four ways: `EmbyGreen`, `DetailAccent`, and the hero's `0xFF69C762` / `0xFF7BD574`. ### 9. Secondary-text contrast diverged between the two screens `ui/HomeComponents.kt:129-132` carries a comment about raising the neutrals for TV distance (`MutedText 0xFFD0D6DB`, `QuietText 0xFFAEB7BF`). The detail page still uses the pre-fix values (`DetailMutedText 0xFFB6BDC3`, `DetailQuietText 0xFF8C959D`). The two sit side by side the moment a detail page is opened from a row. ### 10. Corner radii are ad hoc 9dp home cards, 8dp related posters, 8/10dp cast cards, 14dp featured hero, 11dp mini hero, 12dp overlays, 7dp chips, 999dp search chips. ### 11. Three button languages The hand-rolled `DetailPlayButton` (23/12dp padding, 16sp), the hero's hand-rolled play chip (12/7dp, 13sp — same look, different metrics), and raw `androidx.tv.material3.Button` with glyph text in `MediaMetadataPanel` ("▶ Resume") and `ForYouTimeBudget` ("✓ 30 min"), which picks up theme colours nothing else in the app uses. --- ## Logic and behaviour ### 12. `showTitleLogo` is ignored by the detail pages The Settings copy promises "shows each title's logo artwork from Emby instead of plain text", but only `ui/screensaver/ScreensaverContent.kt:773` honours it; `ui/DetailPageComponents.kt:322` always fetches the logo. The screensaver also has `useTextTitleForLogo`, a fallback for logos too dark to read — the detail hero has no equivalent, so a dark logo is invisible on the near-black scrim. ### 13. Two different 4K thresholds The badge fires at video width ≥ 3800 (`HomeComponents.kt:1184`); the `(4K)` suffix at ≥ 3400 (`DetailFacts.kt:190`). A 3600-wide file is 4K in the spec row and not on the badge. ### 14. HDR10+ is named in `dynamicRangeLabel` but collapses to plain "HDR" in `mediaBadges` ### 15. One `FocusRequester` attached to two live nodes `informationPane` is attached by the Overview pane, the Cast & Details pane and the Episodes empty states (`ui/MediaDetailsOverlay.kt:202-216`, `ui/SeriesDetailsOverlay.kt:291-329`). `AnimatedContent`'s 80ms fade-out keeps the outgoing pane composed, so a Down press landing in that window can request focus on a pane that is disappearing. ### 16. A series can never show format badges `ui/SeriesDetailsOverlay.kt:248` passes `badges = emptyList()` where movies pass real ones. Related: `DetailFactRow` does `badges.take(3)`, so on a 4K/HDR/HEVC movie the airing badge appended in `ui/MediaDetailsOverlay.kt:147` is silently dropped. ### 17. Home row headers do not align `MediaRow` and `MyShowsStrip` lead with a 28dp icon chip plus 10dp; `RecentSearchesRow` (`ui/MainActivity.kt:2084`) has no chip, so its title starts 38dp further left. It also uses 9dp header spacing against everyone else's 6dp and gives its `LazyRow` no vertical padding, so focus-scaled chips have no room to grow. ### 18. The mini hero labels are positional fiction `ui/HomeMovieHero.kt:124`: `listOf("POPULAR", "NEW RELEASE", "TRENDING")[index]`. But `selectHomeHeroMovies` interleaves new releases and popular picks and then falls back to every movie in the response. In the current screenshot a 2025 title is labelled NEW RELEASE and a 2026 one POPULAR. ### 19. A dead second home implementation `ui/MainActivity.kt:2752-2900`: `HomeHero`, `HomeRow`, `ContentCard`, `HomeRowData` and `HomeRowSkeleton` are unreferenced (only the gateway *model* named `HomeRow` is in use). They carry a competing 48dp gutter and SemiBold header style — a live-looking template for the wrong conventions. ### 20. Tab content sits entirely below the fold with no affordance By design per the code comments, but nothing on screen tells the viewer that Down reveals anything. --- ## Documentation `CLAUDE.md`'s detail-page section no longer describes this code. It documents a poster-left layout with Play hanging off the poster's bottom-right corner, "nothing scrolls vertically", a tab list of "Overview, Episodes, Cast, Details", and a `DetailReasonStrip` of several short phrases. The code is a full-bleed scrolling hero with Overview / Episodes / More Like This / Cast & Details and a single reason line (`ui/DetailPageComponents.kt:375-385`). Worth correcting before it misleads the next change.