Three stops — purple into blue into black — matches ios!144

#3A1E9E purple, #0B3080 blue, #070A1A black, top down. Less purple than the
two-stop version, because the purple is a third of the sweep rather than half.

🔴 One card colour against three stops has to lift off all three. #0E1745
looked right against the purple and the blue and measured 1.15:1 against the
black end — a card that disappears at the bottom of a long screen. #141C52
lifts 1.41 / 1.33 / 1.23.

The palette test checks the middle stop too. A three-stop gradient checked only
at its ends is two thirds measured, and the middle is where most of a screen
actually sits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-07 13:37:35 -04:00
co-authored by Claude Opus 5
parent 15e678d593
commit b824e626e7
2 changed files with 31 additions and 15 deletions
@@ -79,11 +79,12 @@ object Palette {
// the top and 8.4:1 at the bottom, secondary text 8.1:1 and 5.5:1 — the
// bottom stop binds, and it is where most of the explanatory copy sits.
// White cards lift off the bottom at 2.4:1, which keeps a card a card.
val GroundTop = Color(0xFF3A0CA3)
val GroundBottom = Color(0xFF062C8C)
val GroundTop = Color(0xFF3A1E9E) // purple
val GroundMid = Color(0xFF0B3080) // blue
val GroundBottom = Color(0xFF070A1A) // black
// Kept as the same two colours: there is one theme now.
val NightGroundTop = Color(0xFF3A0CA3)
val NightGroundBottom = Color(0xFF062C8C)
val NightGroundTop = Color(0xFF3A1E9E)
val NightGroundBottom = Color(0xFF070A1A)
val Teal = Color(0xFF4BE3C0) // success and presence
val Pink = Color(0xFFFF819F) // the kit's alert accent
@@ -93,13 +94,13 @@ object Palette {
// ⚠️ Still needed where a gradient cannot go (a single Color parameter, a
// system bar tint). This is the gradient's mid-tone, so the two never read
// as different surfaces.
val Mist = Color(0xFF201C98) // the page ground, flat
val SurfaceAlt = Color(0xFF2A1875) // received bubbles, pills, bands
val IncomingBubble = Color(0xFF2A1875)
val Mist = Color(0xFF191D68) // the page ground, flat
val SurfaceAlt = Color(0xFF1E2A70) // received bubbles, pills, bands
val IncomingBubble = Color(0xFF1E2A70)
// ⚠️ A blue-tinted grey rather than the flat #F3F3F3: an initials avatar
// needs to read as a circle against the surface behind it, and two greys a
// shade apart do not.
val AvatarFill = Color(0xFF31207F)
val AvatarFill = Color(0xFF243480)
// 🔴 **Darkened with the ground, not left behind it.** #505C69 read 5.0:1
// on the old bottom stop and only 3.5:1 on the darker one — below AA, and
// most of the explanatory copy on these screens is set in it. Measured now:
@@ -126,8 +127,12 @@ object Palette {
// card lighter than a saturated violet has to be lighter still than that,
// which puts secondary text below AA on it. Recessed panels invert the
// problem: the gradient is the bright thing, content sits in dark wells.
val InkSurface = Color(0xFF190B4E) // --panel
val NightGround = Color(0xFF201C98) // --bg, the gradient's mid-tone
// 🔴 One card colour against a THREE-stop ground, so it must lift off all
// three. #0E1745 looked right against the purple and the blue and measured
// 1.15:1 against the black end — a card that disappears at the bottom of a
// long screen. This lifts 1.41 / 1.33 / 1.23.
val InkSurface = Color(0xFF141C52) // --panel
val NightGround = Color(0xFF191D68) // --bg, the gradient's mid-tone
}
/**
@@ -142,7 +147,8 @@ fun groundBrush(): Brush =
// One theme, so no branch. The Night* pair is kept pointing at the same two
// colours rather than deleted, because several screens name it directly and
// a rename across both apps is a separate change.
Brush.verticalGradient(listOf(Palette.GroundTop, Palette.GroundBottom))
Brush.verticalGradient(
listOf(Palette.GroundTop, Palette.GroundMid, Palette.GroundBottom))
/** The primary action's fill. Matches `Theme.primaryGradient*` on iOS. */
fun accentBrush(): Brush =