Field report: "with the bright background colors on Apple TV the text still stays white in many places".
The trap
A view's @Environment resolves against its parent. A screen that applies gamepadPaletteInk() to its own body is therefore above its own copy — the modifier covers the body's descendants, but not the body's own ink.… references. So every one of these screens read whatever was published above it.
On iOS that happened to be right: they are the shell's in-place layers, nested inside GamepadHomeView below its ink. On tvOS and macOS they are covers and sheets, attached outside the launcher's modifier — nothing is above them, so they got the bare GamepadInk.dark default.
The symptom is a screen split down the middle: the title, the tab pills, every row label and value white, and the focus wash still brand violet — while the child views on the same screen (hint bar, host tiles, ConsoleGlass frost) resolved the real palette and went correctly dark-on-pale.
This is the same trap the gamepadMetrics comment already documents, one environment key over.
Before / after — console Settings on Mint:
before
after
white title, white tab labels, white row labels and values over pale glass; violet focus wash and selected pill
palette ink throughout; the pill and focus wash are the palette's own accent
The fix
The six screens that publish the ink — and GamepadScreenBackground, mounted as their .background { } — resolve it from the stored ui_palette instead of the environment:
Deliberately not the fix gamepadMetrics uses (publish at the ContentView root): gamepadPaletteInk also publishes \.colorScheme, and forcing .light over a live session would flip StreamHUDView's .secondary/.tertiary to dark text over video. Resolving from storage also works no matter who mounts the screen, including the DEBUG shot harness, which mounts them with no ContentView above.
Three more of the same family (tvOS)
A tvOS fullScreenCover draws no background of its own. The pairing cover rendered the system's dark chrome straight over the launcher showing through it — the PIN prompt was white on the bright aurora. It gets the console field and the palette now, in the launcher's branch only; HomeView's route to the same PairSheet is the touch UI and still belongs to the system background.
A NavigationStack draws the navigation title, and it wraps LibraryView from outside that view's own ink — so the shelf's name stayed white over content that had already gone dark. Inked on the stack, on tvOS and on the macOS sheet (gated there: that sheet is both modes' library).
LibraryView's loading / error / empty states mounted no backdrop at all — only the coverflow did — so the spinner sat on the launcher's own aurora with the host tiles still visible behind it. Reported separately by the user while this was in flight.
Also: fg vs onAccent
A saved host's badge glyph took ink.fg, which is chosen against the field, while the badge it sits on is the accent. The two disagree at both ends of the set — a pale palette put near-black on a deep accent, and Graphite (accent luma ≈ 0.80) put white on light grey. It takes onAccent now, exactly like the selected settings tab in 08e462fe.
Verification
Built and run on the tvOS 26.5 simulator (Apple TV 4K), screenshotting each screen under Mint, Sunset (pale) and Violet, Graphite (dark): launcher, settings, add-host, pairing, and the library's loading state. Dark palettes are unchanged.
swift test — 288 passed, 6 skipped, 0 failures
swift build (macOS), Punktfunk-iOS and Punktfunk-tvOS all build — the #if os(iOS)-only (GamepadLibraryScreen) and os(iOS) || os(macOS)-only (GamepadPairView) branches are compiled out of the tvOS build, so both were typechecked separately.
⚠ Simulator only — not yet on real Apple TV hardware.
Field report: *"with the bright background colors on Apple TV the text still stays white in many places"*.
## The trap
A view's `@Environment` resolves against its **parent**. A screen that applies `gamepadPaletteInk()` to its own body is therefore *above* its own copy — the modifier covers the body's descendants, but not the body's own `ink.…` references. So every one of these screens read whatever was published above it.
On iOS that happened to be right: they are the shell's in-place layers, nested inside `GamepadHomeView` below its ink. On **tvOS and macOS they are covers and sheets**, attached outside the launcher's modifier — nothing is above them, so they got the bare `GamepadInk.dark` default.
The symptom is a screen split down the middle: the title, the tab pills, every row label and value white, and the focus wash still brand violet — while the **child** views on the same screen (hint bar, host tiles, `ConsoleGlass` frost) resolved the real palette and went correctly dark-on-pale.
This is the same trap the `gamepadMetrics` comment already documents, one environment key over.
**Before / after — console Settings on Mint:**
| before | after |
| --- | --- |
| white title, white tab labels, white row labels and values over pale glass; violet focus wash and selected pill | palette ink throughout; the pill and focus wash are the palette's own accent |
## The fix
The six screens that publish the ink — and `GamepadScreenBackground`, mounted as their `.background { }` — resolve it from the stored `ui_palette` instead of the environment:
```swift
@AppStorage(DefaultsKey.uiPalette) private var paletteID = "violet"
private var ink: GamepadInk { .stored(paletteID) }
```
Deliberately **not** the fix `gamepadMetrics` uses (publish at the ContentView root): `gamepadPaletteInk` also publishes `\.colorScheme`, and forcing `.light` over a live session would flip `StreamHUDView`'s `.secondary`/`.tertiary` to dark text over video. Resolving from storage also works no matter who mounts the screen, including the DEBUG shot harness, which mounts them with no ContentView above.
## Three more of the same family (tvOS)
- **A tvOS `fullScreenCover` draws no background of its own.** The pairing cover rendered the system's dark chrome straight over the launcher showing through it — the PIN prompt was white on the bright aurora. It gets the console field and the palette now, in the launcher's branch only; `HomeView`'s route to the same `PairSheet` is the touch UI and still belongs to the system background.
- **A `NavigationStack` draws the navigation title**, and it wraps `LibraryView` from outside that view's own ink — so the shelf's name stayed white over content that had already gone dark. Inked on the stack, on tvOS and on the macOS sheet (gated there: that sheet is both modes' library).
- **`LibraryView`'s loading / error / empty states mounted no backdrop at all** — only the coverflow did — so the spinner sat on the launcher's own aurora with the host tiles still visible behind it. Reported separately by the user while this was in flight.
## Also: `fg` vs `onAccent`
A saved host's badge glyph took `ink.fg`, which is chosen against the **field**, while the badge it sits on **is** the accent. The two disagree at both ends of the set — a pale palette put near-black on a deep accent, and Graphite (accent luma ≈ 0.80) put white on light grey. It takes `onAccent` now, exactly like the selected settings tab in 08e462fe.
## Verification
Built and run on the **tvOS 26.5 simulator** (Apple TV 4K), screenshotting each screen under **Mint**, **Sunset** (pale) and **Violet**, **Graphite** (dark): launcher, settings, add-host, pairing, and the library's loading state. Dark palettes are unchanged.
- `swift test` — 288 passed, 6 skipped, 0 failures
- `swift build` (macOS), `Punktfunk-iOS` and `Punktfunk-tvOS` all build — the `#if os(iOS)`-only (`GamepadLibraryScreen`) and `os(iOS) || os(macOS)`-only (`GamepadPairView`) branches are compiled out of the tvOS build, so both were typechecked separately.
⚠ Simulator only — not yet on real Apple TV hardware.
A screen that applies `gamepadPaletteInk()` to its own body sits ABOVE its own copy of the
environment: the modifier covers its descendants, never the body's own `ink.…` references. So
each of these screens read whatever was published above it — and on tvOS, where they are
presented as covers rather than nested in the iOS shell, that is nothing at all. They got the
bare dark default while their CHILD views (the hint bar, the host tiles, the glass) resolved the
real palette, which is why a pale field came out with a white title, white row labels and white
values under correctly-pale glass, with the focus wash still brand violet instead of the
palette's accent. The same trap the `gamepadMetrics` comment already documents, one environment
key over.
Resolve the ink from the stored `ui_palette` instead of the environment in the six screens that
publish it, and in `GamepadScreenBackground` — mounted as their `.background { }`, so it was
reading the parent's ink too and bleaching a pale field's scrim toward white.
Three more of the same family, all tvOS-only:
- the pairing cover drew the system's dark chrome straight over the launcher showing through
it (a tvOS cover has no background of its own): the PIN prompt was white on the bright
aurora. It gets the console field and the palette now, in the launcher's branch only — the
touch route to the same sheet still belongs to the system background.
- the library cover's navigation title is drawn by the NavigationStack, which wraps LibraryView
from outside its own ink, so the shelf's name stayed white over content that had already gone
dark. Fixed on tvOS and on the macOS sheet (gated there — that sheet is both modes').
- the library's loading / error / empty states mounted no backdrop at all; only the coverflow
did. They now take the same field, so the spinner no longer sits on the launcher's own
aurora with the host tiles still visible behind it.
And a contrast bug the same screens made visible: a saved host's badge glyph took `fg`, which is
chosen against the FIELD, while the badge it sits on IS the accent. The two disagree at both ends
of the set — a pale palette put near-black on a deep accent, Graphite (accent luma 0.80) put
white on light grey. It takes `onAccent` now, like the selected settings tab.
Verified on the tvOS 26.5 simulator across Mint, Sunset, Violet and Graphite: launcher, settings,
add-host, pairing and the library's loading state. `swift test` 288 passed / 6 skipped; iOS and
tvOS both build.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Field report: "with the bright background colors on Apple TV the text still stays white in many places".
The trap
A view's
@Environmentresolves against its parent. A screen that appliesgamepadPaletteInk()to its own body is therefore above its own copy — the modifier covers the body's descendants, but not the body's ownink.…references. So every one of these screens read whatever was published above it.On iOS that happened to be right: they are the shell's in-place layers, nested inside
GamepadHomeViewbelow its ink. On tvOS and macOS they are covers and sheets, attached outside the launcher's modifier — nothing is above them, so they got the bareGamepadInk.darkdefault.The symptom is a screen split down the middle: the title, the tab pills, every row label and value white, and the focus wash still brand violet — while the child views on the same screen (hint bar, host tiles,
ConsoleGlassfrost) resolved the real palette and went correctly dark-on-pale.This is the same trap the
gamepadMetricscomment already documents, one environment key over.Before / after — console Settings on Mint:
The fix
The six screens that publish the ink — and
GamepadScreenBackground, mounted as their.background { }— resolve it from the storedui_paletteinstead of the environment:Deliberately not the fix
gamepadMetricsuses (publish at the ContentView root):gamepadPaletteInkalso publishes\.colorScheme, and forcing.lightover a live session would flipStreamHUDView's.secondary/.tertiaryto dark text over video. Resolving from storage also works no matter who mounts the screen, including the DEBUG shot harness, which mounts them with no ContentView above.Three more of the same family (tvOS)
fullScreenCoverdraws no background of its own. The pairing cover rendered the system's dark chrome straight over the launcher showing through it — the PIN prompt was white on the bright aurora. It gets the console field and the palette now, in the launcher's branch only;HomeView's route to the samePairSheetis the touch UI and still belongs to the system background.NavigationStackdraws the navigation title, and it wrapsLibraryViewfrom outside that view's own ink — so the shelf's name stayed white over content that had already gone dark. Inked on the stack, on tvOS and on the macOS sheet (gated there: that sheet is both modes' library).LibraryView's loading / error / empty states mounted no backdrop at all — only the coverflow did — so the spinner sat on the launcher's own aurora with the host tiles still visible behind it. Reported separately by the user while this was in flight.Also:
fgvsonAccentA saved host's badge glyph took
ink.fg, which is chosen against the field, while the badge it sits on is the accent. The two disagree at both ends of the set — a pale palette put near-black on a deep accent, and Graphite (accent luma ≈ 0.80) put white on light grey. It takesonAccentnow, exactly like the selected settings tab in08e462fe.Verification
Built and run on the tvOS 26.5 simulator (Apple TV 4K), screenshotting each screen under Mint, Sunset (pale) and Violet, Graphite (dark): launcher, settings, add-host, pairing, and the library's loading state. Dark palettes are unchanged.
swift test— 288 passed, 6 skipped, 0 failuresswift build(macOS),Punktfunk-iOSandPunktfunk-tvOSall build — the#if os(iOS)-only (GamepadLibraryScreen) andos(iOS) || os(macOS)-only (GamepadPairView) branches are compiled out of the tvOS build, so both were typechecked separately.⚠ Simulator only — not yet on real Apple TV hardware.
A screen that applies `gamepadPaletteInk()` to its own body sits ABOVE its own copy of the environment: the modifier covers its descendants, never the body's own `ink.…` references. So each of these screens read whatever was published above it — and on tvOS, where they are presented as covers rather than nested in the iOS shell, that is nothing at all. They got the bare dark default while their CHILD views (the hint bar, the host tiles, the glass) resolved the real palette, which is why a pale field came out with a white title, white row labels and white values under correctly-pale glass, with the focus wash still brand violet instead of the palette's accent. The same trap the `gamepadMetrics` comment already documents, one environment key over. Resolve the ink from the stored `ui_palette` instead of the environment in the six screens that publish it, and in `GamepadScreenBackground` — mounted as their `.background { }`, so it was reading the parent's ink too and bleaching a pale field's scrim toward white. Three more of the same family, all tvOS-only: - the pairing cover drew the system's dark chrome straight over the launcher showing through it (a tvOS cover has no background of its own): the PIN prompt was white on the bright aurora. It gets the console field and the palette now, in the launcher's branch only — the touch route to the same sheet still belongs to the system background. - the library cover's navigation title is drawn by the NavigationStack, which wraps LibraryView from outside its own ink, so the shelf's name stayed white over content that had already gone dark. Fixed on tvOS and on the macOS sheet (gated there — that sheet is both modes'). - the library's loading / error / empty states mounted no backdrop at all; only the coverflow did. They now take the same field, so the spinner no longer sits on the launcher's own aurora with the host tiles still visible behind it. And a contrast bug the same screens made visible: a saved host's badge glyph took `fg`, which is chosen against the FIELD, while the badge it sits on IS the accent. The two disagree at both ends of the set — a pale palette put near-black on a deep accent, Graphite (accent luma 0.80) put white on light grey. It takes `onAccent` now, like the selected settings tab. Verified on the tvOS 26.5 simulator across Mint, Sunset, Violet and Graphite: launcher, settings, add-host, pairing and the library's loading state. `swift test` 288 passed / 6 skipped; iOS and tvOS both build.