brand: loading state, themes, manifest, art preview, pairing screen updates

This commit is contained in:
null 2026-06-25 15:24:46 -05:00
parent fed91dbe46
commit 95cad84cb5
13 changed files with 212 additions and 27 deletions

2
.gitignore vendored
View File

@ -75,3 +75,5 @@ ClaudeReport.md
docs/brand/visual-identity.md docs/brand/visual-identity.md
docs/brand/asset-system.md docs/brand/asset-system.md
docs/brand/visual-identity.md docs/brand/visual-identity.md
docs/brand/asset-system.md
ClaudeBrandingReview.md

View File

@ -99,8 +99,9 @@ Legend: ✅ on-brand / no art needed · add/þwire art (prompt below) ·
Priority order = the moments a user feels most. Each is self-contained after you paste the House Style block first. Priority order = the moments a user feels most. Each is self-contained after you paste the House Style block first.
**A1 · Pairing success celebration** — *1:1, transparent bg.* **A1 · Pairing success celebration** — *1:1, transparent bg.*
> Scene: two equal heart-halves (soft pink + soft lavender) sliding together into one whole heart at the center, with > Scene: the Closer mark resolving into place — a soft-pink upper C-arc and a lavender lower sweep curving together to
> a gentle burst of small floating hearts and petals around it. Convey "you're connected now." Calm, joyful, no text. > enclose a heart-shaped space with a small keyhole at its center — with a gentle burst of small floating hearts and
> petals around it. Convey "you're connected now." Calm, joyful, no text. (White keyhole if on a dark background.)
**A2 · Answer history empty state** — *1:1.* **A2 · Answer history empty state** — *1:1.*
> Scene: a small soft journal/photo-album, slightly open, with two paired cards tucked inside and a few faint floating > Scene: a small soft journal/photo-album, slightly open, with two paired cards tucked inside and a few faint floating
@ -151,7 +152,7 @@ Priority order = the moments a user feels most. Each is self-contained after you
> alarm imagery. > alarm imagery.
**G-set · Notification + relationship glyphs** — *single-color vector, square, legible at 24 dp.* (One prompt each:) **G-set · Notification + relationship glyphs** — *single-color vector, square, legible at 24 dp.* (One prompt each:)
> Simple single-color flat glyph in the Closer style, no text, no background: **heart-of-two-halves**, **paired sealed > Simple single-color flat glyph in the Closer style, no text, no background: **the Closer C-heart-keyhole mark**, **paired sealed
> cards**, **daily card**, **sealed answer (card with small lock)**, **memory capsule**, **date-card with heart**, > cards**, **daily card**, **sealed answer (card with small lock)**, **memory capsule**, **date-card with heart**,
> **quiet-hours moon**, **couple-premium (heart + small crown/spark, tasteful)**, **export-data**, **delete-account**. > **quiet-hours moon**, **couple-premium (heart + small crown/spark, tasteful)**, **export-data**, **delete-account**.
> Export in lavender for in-app and as high-contrast monochrome for the platform notification glyph. > Export in lavender for in-app and as high-contrast monochrome for the platform notification glyph.

View File

@ -131,6 +131,7 @@ dependencies {
implementation(composeBom) implementation(composeBom)
implementation("androidx.core:core-ktx:1.15.0") implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("androidx.activity:activity-compose:1.9.3") implementation("androidx.activity:activity-compose:1.9.3")

View File

@ -25,7 +25,7 @@
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Closer"> android:theme="@style/Theme.Closer.Splash">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

View File

@ -22,6 +22,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -62,7 +63,21 @@ class MainActivity : AppCompatActivity() {
private val pendingDeepLink = mutableStateOf<String?>(null) private val pendingDeepLink = mutableStateOf<String?>(null)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Gentle scale-up + fade exit, handing off to the animated in-app logo loader.
splashScreen.setOnExitAnimationListener { provider ->
provider.iconView.animate()
.scaleX(1.15f)
.scaleY(1.15f)
.setDuration(300L)
.start()
provider.view.animate()
.alpha(0f)
.setDuration(300L)
.withEndAction { provider.remove() }
.start()
}
maybeRequestNotificationPermission() maybeRequestNotificationPermission()
registerFcmToken() registerFcmToken()
pendingDeepLink.value = deepLinkRouteFromIntent(intent) pendingDeepLink.value = deepLinkRouteFromIntent(intent)

View File

@ -4,29 +4,43 @@ import app.closer.R
import app.closer.ui.theme.closerCardColor import app.closer.ui.theme.closerCardColor
import android.provider.Settings import android.provider.Settings
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.imageResource
import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
@ -68,6 +82,15 @@ fun CloserHeartLoader(
CloserMarkLoader(modifier = modifier, size = size) CloserMarkLoader(modifier = modifier, size = size)
} }
/** Below this size we use a lightweight inline spinner instead of the full app-icon chip. */
private val ChipLoaderMinSize = 40.dp
/**
* The brand loader. At prominent sizes it renders the **white-keyhole logo on an aubergine app-icon
* chip** with a soft fill that rises through the mark (the "charging the connection" motion). At small
* inline sizes (e.g. inside buttons) it falls back to a lightweight indeterminate arc tinted to the
* current content color, so it never shows a dark square on a colored button. Honors "remove animations".
*/
@Composable @Composable
fun CloserMarkLoader( fun CloserMarkLoader(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -81,21 +104,48 @@ fun CloserMarkLoader(
1f 1f
) == 0f ) == 0f
} }
if (size >= ChipLoaderMinSize) {
CloserMarkChipLoader(modifier = modifier, size = size, reducedMotion = reducedMotion)
} else {
CloserInlineSpinner(
modifier = modifier,
size = size,
color = LocalContentColor.current,
reducedMotion = reducedMotion
)
}
}
@Composable
private fun CloserMarkChipLoader(
modifier: Modifier,
size: Dp,
reducedMotion: Boolean
) {
val transition = rememberInfiniteTransition(label = "closerMarkLoader") val transition = rememberInfiniteTransition(label = "closerMarkLoader")
val animatedPulse = transition.animateFloat( val fill = if (reducedMotion) 1f else transition.animateFloat(
initialValue = 0.96f, initialValue = 0f,
targetValue = 1.04f, targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1500, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Restart
),
label = "closerMarkFill"
).value
val pulse = if (reducedMotion) 1f else transition.animateFloat(
initialValue = 0.97f,
targetValue = 1.03f,
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 900, easing = FastOutSlowInEasing), animation = tween(durationMillis = 900, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse repeatMode = RepeatMode.Reverse
), ),
label = "closerMarkPulse" label = "closerMarkPulse"
) ).value
val pulse = if (reducedMotion) 1f else animatedPulse.value
Image( // White-keyhole mark (already safe-zone padded) drawn over the aubergine app-icon chip.
painter = painterResource(R.drawable.closer_mark_loader), val mark = ImageBitmap.imageResource(R.drawable.closer_launcher_foreground)
contentDescription = null,
Canvas(
modifier = modifier modifier = modifier
.size(size) .size(size)
.graphicsLayer { .graphicsLayer {
@ -103,5 +153,88 @@ fun CloserMarkLoader(
scaleY = pulse scaleY = pulse
} }
.clearAndSetSemantics {} .clearAndSetSemantics {}
) ) {
val w = this.size.width
val h = this.size.height
val corner = CornerRadius(w * 0.22f, h * 0.22f)
// Aubergine gradient chip (matches ic_launcher_background palette).
drawRoundRect(
brush = Brush.linearGradient(
colors = listOf(Color(0xFF5A2F74), Color(0xFF3A1D4D), Color(0xFF24122F)),
start = Offset(0f, 0f),
end = Offset(w, h)
),
cornerRadius = corner
)
val src = IntSize(mark.width, mark.height)
val dst = IntSize(w.toInt(), h.toInt())
// Ghost of the full mark…
drawImage(
image = mark,
srcOffset = IntOffset.Zero,
srcSize = src,
dstOffset = IntOffset.Zero,
dstSize = dst,
alpha = 0.16f
)
// …then the fill rises bottom→top.
clipRect(top = h * (1f - fill)) {
drawImage(
image = mark,
srcOffset = IntOffset.Zero,
srcSize = src,
dstOffset = IntOffset.Zero,
dstSize = dst,
alpha = 1f
)
}
// Faint ring so the dark chip separates on dark backgrounds.
drawRoundRect(
color = Color.White.copy(alpha = 0.10f),
cornerRadius = corner,
style = Stroke(width = 1.dp.toPx())
)
}
}
@Composable
private fun CloserInlineSpinner(
modifier: Modifier,
size: Dp,
color: Color,
reducedMotion: Boolean
) {
val angle = if (reducedMotion) 0f else rememberInfiniteTransition(label = "closerInlineSpinner")
.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 900, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "closerInlineAngle"
).value
Canvas(
modifier = modifier
.size(size)
.clearAndSetSemantics {}
) {
val stroke = this.size.minDimension * 0.12f
val inset = stroke / 2f
val d = this.size.minDimension - stroke
rotate(degrees = angle) {
drawArc(
brush = Brush.sweepGradient(
colors = listOf(color.copy(alpha = 0.12f), color)
),
startAngle = 0f,
sweepAngle = 270f,
useCenter = false,
topLeft = Offset(inset, inset),
size = Size(d, d),
style = Stroke(width = stroke, cap = StrokeCap.Round)
)
}
}
} }

View File

@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp
import app.closer.R import app.closer.R
import app.closer.ui.components.CelebrationOverlay import app.closer.ui.components.CelebrationOverlay
import app.closer.ui.components.CloserActionButton import app.closer.ui.components.CloserActionButton
import app.closer.ui.components.CloserMarkLoader
import app.closer.ui.components.CloserButtonStyle import app.closer.ui.components.CloserButtonStyle
import app.closer.ui.components.CloserCard import app.closer.ui.components.CloserCard
import app.closer.ui.settings.SettingsMuted import app.closer.ui.settings.SettingsMuted
@ -84,6 +85,16 @@ fun ArtPreviewScreen(onNavigate: (String) -> Unit = {}) {
Image(painterResource(R.drawable.particle_petal), null, Modifier.size(56.dp)) Image(painterResource(R.drawable.particle_petal), null, Modifier.size(56.dp))
} }
} }
ArtCard("Loading mark (animated)") {
androidx.compose.foundation.layout.Row(
horizontalArrangement = Arrangement.spacedBy(24.dp),
verticalAlignment = Alignment.CenterVertically
) {
CloserMarkLoader(size = 76.dp) // chip + rising fill
CloserMarkLoader(size = 40.dp) // chip threshold
CloserMarkLoader(size = 24.dp) // inline arc
}
}
CloserActionButton( CloserActionButton(
label = "Play the celebration", label = "Play the celebration",

View File

@ -203,19 +203,25 @@ fun PairingSuccessScreen(
.size(60.dp) .size(60.dp)
.zIndex(2f) .zIndex(2f)
.align(Alignment.Center) .align(Alignment.Center)
.scale(pulse)
.clip(CircleShape) .clip(CircleShape)
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)
.padding(5.dp) .padding(4.dp)
.clip(CircleShape) .clip(CircleShape),
.background(Color(0xFFFFF8FC)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
// White-keyhole app-icon chip: aubergine gradient + the brand mark.
Image( Image(
painter = painterResource(R.drawable.closer_mark_loader), painter = painterResource(R.drawable.ic_launcher_background),
contentDescription = null, contentDescription = null,
modifier = Modifier contentScale = ContentScale.Crop,
.size(38.dp) modifier = Modifier.matchParentSize()
.scale(pulse) )
Image(
painter = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.matchParentSize()
) )
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Cold-launch splash background: brand aubergine, behind the white-keyhole icon. -->
<color name="splash_background">#FF24122F</color>
</resources>

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.Closer" parent="Theme.AppCompat.Light.NoActionBar" /> <style name="Theme.Closer" parent="Theme.AppCompat.Light.NoActionBar" />
<!-- Cold-launch splash: aubergine background + the white-keyhole app icon, then hands off to
Theme.Closer (and the animated in-app logo loader). Splash attrs are from core-splashscreen
(no android: prefix) so they back-compat down to the minSdk. -->
<style name="Theme.Closer.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splash_background</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="postSplashScreenTheme">@style/Theme.Closer</item>
</style>
</resources> </resources>

View File

@ -97,18 +97,17 @@ masters (do not ship them directly); ship the exports and the wired Android rast
| `docs/brand/sources/closer-approved-icon-square.png` | Square 1024 master | iOS `AppIcon` 1024, store 1024 | | `docs/brand/sources/closer-approved-icon-square.png` | Square 1024 master | iOS `AppIcon` 1024, store 1024 |
| `docs/brand/sources/closer-mark.svg` | Vector master of the mark | Source for all PNG exports | | `docs/brand/sources/closer-mark.svg` | Vector master of the mark | Source for all PNG exports |
| `docs/brand/sources/closer-mark-transparent-keyhole-white.png` | Transparent mark, white keyhole (for dark aubergine/purple) | **Source of** `drawable-nodpi/closer_launcher_foreground.png` (adaptive fg over the aubergine bg) + the Auth/Onboarding logo tile (dark surface) | | `docs/brand/sources/closer-mark-transparent-keyhole-white.png` | Transparent mark, white keyhole (for dark aubergine/purple) | **Source of** `drawable-nodpi/closer_launcher_foreground.png` (adaptive fg over the aubergine bg) + the Auth/Onboarding logo tile (dark surface) |
| `docs/brand/sources/closer-mark-transparent-keyhole-aubergine.png` | Transparent mark, dark keyhole (for light/blush surfaces) | **Source of** `drawable-nodpi/closer_mark_loader.png` (all loaders + the PairingSuccess hero badge on blush) | | `docs/brand/sources/closer-mark-transparent-keyhole-aubergine.png` | Transparent mark, dark keyhole (for light/blush surfaces) | Logo on light/blush placements + web/docs exports (no in-app wiring today — in-app loaders now use the white-keyhole chip) |
| `docs/brand/sources/closer-mark-transparent-keyhole-black.png` | Mono/max-contrast mark | Print/legal, max-contrast light | | `docs/brand/sources/closer-mark-transparent-keyhole-black.png` | Mono/max-contrast mark | Print/legal, max-contrast light |
| `docs/brand/exports/logo/transparent-keyhole-aubergine/closer-mark-{24..1024}.png` | Sized PNGs, dark keyhole | Web/email/social on light, docs, favicons | | `docs/brand/exports/logo/transparent-keyhole-aubergine/closer-mark-{24..1024}.png` | Sized PNGs, dark keyhole | Web/email/social on light, docs, favicons |
| `docs/brand/exports/logo/transparent-keyhole-white/closer-mark-{24..1024}.png` | Sized PNGs, white keyhole | Same, on dark surfaces | | `docs/brand/exports/logo/transparent-keyhole-white/closer-mark-{24..1024}.png` | Sized PNGs, white keyhole | Same, on dark surfaces |
| `docs/brand/exports/logo/transparent-keyhole-black/closer-mark-{24..1024}.png` | Sized PNGs, mono | One-color/print/footer | | `docs/brand/exports/logo/transparent-keyhole-black/closer-mark-{24..1024}.png` | Sized PNGs, mono | One-color/print/footer |
| `docs/brand/exports/logo/social/closer-mark-social-{1024,2048}.png` | Square social avatars / OG | Social profile + open-graph square | | `docs/brand/exports/logo/social/closer-mark-social-{1024,2048}.png` | Square social avatars / OG | Social profile + open-graph square |
| `docs/brand/exports/logo/app-icon/closer-app-icon-512.png` | Play listing icon | `docs/store/app-icon-512.png` (Play Console) | | `docs/brand/exports/logo/app-icon/closer-app-icon-512.png` | Play listing icon | `docs/store/app-icon-512.png` (Play Console) |
| `app/src/main/res/drawable-nodpi/closer_launcher_foreground.png` | Adaptive **foreground** (white-keyhole mark, safe-zone) | `drawable/ic_launcher_foreground.xml``mipmap-anydpi-*/ic_launcher*.xml`; also the Auth/Onboarding logo tile | | `app/src/main/res/drawable-nodpi/closer_launcher_foreground.png` | Adaptive **foreground** (white-keyhole mark, safe-zone) | `drawable/ic_launcher_foreground.xml``mipmap-anydpi-*/ic_launcher*.xml`; Auth/Onboarding logo tile; **`CloserMarkLoader` chip + `PairingSuccess` hero** (white-keyhole logo on the aubergine app-icon chip) |
| `app/src/main/res/drawable/ic_launcher_background.xml` | Adaptive **background** (aubergine gradient) | `mipmap-anydpi-*/ic_launcher*.xml` | | `app/src/main/res/drawable/ic_launcher_background.xml` | Adaptive **background** (aubergine gradient) | `mipmap-anydpi-*/ic_launcher*.xml` |
| `app/src/main/res/drawable-nodpi/closer_launcher_monochrome.png` | Themed-icon **monochrome** (grayscale+alpha) | `drawable/ic_launcher_monochrome.xml``mipmap-anydpi-v33/*` | | `app/src/main/res/drawable-nodpi/closer_launcher_monochrome.png` | Themed-icon **monochrome** (grayscale+alpha) | `drawable/ic_launcher_monochrome.xml``mipmap-anydpi-v33/*` |
| `app/src/main/res/drawable-nodpi/ic_notification_closer.png` | Notification small icon (white+alpha silhouette) | `NotificationHelper`, `PartnerNotificationManager` `setSmallIcon` | | `app/src/main/res/drawable-nodpi/ic_notification_closer.png` | Notification small icon (white+alpha silhouette) | `NotificationHelper`, `PartnerNotificationManager` `setSmallIcon` |
| `app/src/main/res/drawable-nodpi/closer_mark_loader.png` | Full-color transparent mark | `CloserMarkLoader` (all loaders), `PairingSuccessScreen` hero, `AuthLogoMark`/Onboarding tile |
Note: the section-1 rows `closer-mark-on-dark.svg` / `closer-mark-on-light.svg` and the lockup/favicon Note: the section-1 rows `closer-mark-on-dark.svg` / `closer-mark-on-light.svg` and the lockup/favicon
rows are still **to-build** — the on-disk equivalents today are the `transparent-keyhole-white` (dark) rows are still **to-build** — the on-disk equivalents today are the `transparent-keyhole-white` (dark)

View File

@ -42,8 +42,11 @@ Use the keyhole color to support contrast while keeping the official icon visual
the black-keyhole variant only when maximum contrast is needed. the black-keyhole variant only when maximum contrast is needed.
- **Notification small icon:** use the single-color platform glyph; do not use the full-color - **Notification small icon:** use the single-color platform glyph; do not use the full-color
launcher art. launcher art.
- **Loading mark:** use the aubergine-keyhole variant on light/card surfaces so it stays calmer than - **Loading mark:** the in-app loader is the **white-keyhole logo on an aubergine app-icon chip** with a
the launcher icon. soft fill that rises through the mark (a "charging" loop). Below ~40dp (e.g. inside buttons) it falls
back to a lightweight indeterminate arc tinted to the surrounding content color.
- **Cold-launch splash:** aubergine background + the white-keyhole app icon (static, system-masked), with
a scale/fade exit that hands off to the in-app loading mark above.
## Core colors ## Core colors