diff --git a/docs/superpowers/plans/2026-08-06-ui-redesign.md b/docs/superpowers/plans/2026-08-06-ui-redesign.md
new file mode 100644
index 0000000..14fa58e
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-06-ui-redesign.md
@@ -0,0 +1,1905 @@
+# MyIce UI Redesign Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Complete visual overhaul of MyIce KMP app — teal arctique theme, Material icons, redesigned screens and components, sticky date headers, French accent corrections.
+
+**Architecture:** Theme foundation first (colors, typography, shapes, spacing tokens), then shared components, then screen redesigns building on those components. One new testable utility (date grouping) follows TDD; UI composables are compile-verified (no Compose UI testing infrastructure in project).
+
+**Tech Stack:** Kotlin 2.3.20, Compose Multiplatform 1.11.1, Material 3, kotlinx-datetime 0.7.1
+
+## Global Constraints
+
+- Package: `ch.parano.myicek`
+- All source files include SPDX GPL-3.0-or-later headers (copy from existing files)
+- No comments in code unless explicitly requested
+- French UI strings with correct accents (é, è, ê, à, î, û)
+- Compile check: `./gradlew composeApp:compileKotlinDesktop`
+- Test run: `./gradlew composeApp:desktopTest`
+- Compose Multiplatform 1.11.1 — `stickyHeader` requires `@OptIn(ExperimentalFoundationApi::class)`
+- Material icons: add `compose.materialIconsExtended` dependency for `SportsHockey`, `CloudOff`, `CalendarClear`, `CalendarMonth`
+
+---
+
+### Task 1: Theme Foundation & Design Tokens
+
+**Files:**
+- Modify: `composeApp/build.gradle.kts:37-50` (add materialIconsExtended)
+- Modify: `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/theme/Theme.kt` (full rewrite)
+- Create: `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/theme/Dimens.kt`
+
+**Interfaces:**
+- Produces: `MyIceTheme(darkTheme, content)` composable with full Material 3 color scheme, typography, shapes
+- Produces: `Dimens` object with spacing tokens (`xs`, `sm`, `md`, `lg`, `xl`, `xxl`)
+
+- [ ] **Step 1: Add material-icons-extended dependency**
+
+In `composeApp/build.gradle.kts`, add to the `commonMain` dependencies block (after line 39 `implementation(compose.material3)`):
+
+```kotlin
+ implementation(compose.materialIconsExtended)
+```
+
+- [ ] **Step 2: Create Dimens.kt**
+
+Create `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/theme/Dimens.kt`:
+
+```kotlin
+// SPDX-License-Identifier: GPL-3.0-or-later
+// MyIce Kotlin Multiplatform — schedule/convocation viewer
+// Copyright (C) 2026 parano.ch
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package ch.parano.myicek.ui.theme
+
+import androidx.compose.ui.unit.dp
+
+object Dimens {
+ val xs = 4.dp
+ val sm = 8.dp
+ val md = 12.dp
+ val lg = 16.dp
+ val xl = 24.dp
+ val xxl = 32.dp
+}
+```
+
+- [ ] **Step 3: Rewrite Theme.kt**
+
+Replace the entire content of `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/theme/Theme.kt` with:
+
+```kotlin
+// SPDX-License-Identifier: GPL-3.0-or-later
+// MyIce Kotlin Multiplatform — schedule/convocation viewer
+// Copyright (C) 2026 parano.ch
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package ch.parano.myicek.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Shapes
+import androidx.compose.material3.Typography
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+private val LightColors = lightColorScheme(
+ primary = Color(0xFF0D9488),
+ onPrimary = Color(0xFFFFFFFF),
+ primaryContainer = Color(0xFF99F6E4),
+ onPrimaryContainer = Color(0xFF134E4A),
+ secondary = Color(0xFF475569),
+ onSecondary = Color(0xFFFFFFFF),
+ secondaryContainer = Color(0xFFE2E8F0),
+ onSecondaryContainer = Color(0xFF1E293B),
+ tertiary = Color(0xFF0EA5E9),
+ onTertiary = Color(0xFFFFFFFF),
+ tertiaryContainer = Color(0xFFE0F2FE),
+ onTertiaryContainer = Color(0xFF075985),
+ error = Color(0xFFDC2626),
+ onError = Color(0xFFFFFFFF),
+ errorContainer = Color(0xFFFEE2E2),
+ onErrorContainer = Color(0xFF7F1D1D),
+ background = Color(0xFFFAFAFA),
+ onBackground = Color(0xFF1C1917),
+ surface = Color(0xFFFFFFFF),
+ onSurface = Color(0xFF1C1917),
+ surfaceVariant = Color(0xFFF1F5F9),
+ onSurfaceVariant = Color(0xFF475569),
+ outline = Color(0xFFCBD5E1),
+)
+
+private val DarkColors = darkColorScheme(
+ primary = Color(0xFF2DD4BF),
+ onPrimary = Color(0xFF003833),
+ primaryContainer = Color(0xFF0D9488),
+ onPrimaryContainer = Color(0xFF99F6E4),
+ secondary = Color(0xFF94A3B8),
+ onSecondary = Color(0xFF1E293B),
+ secondaryContainer = Color(0xFF334155),
+ onSecondaryContainer = Color(0xFFE2E8F0),
+ tertiary = Color(0xFF0EA5E9),
+ onTertiary = Color(0xFF003547),
+ background = Color(0xFF0F172A),
+ onBackground = Color(0xFFF1F5F9),
+ surface = Color(0xFF1E293B),
+ onSurface = Color(0xFFF1F5F9),
+ surfaceVariant = Color(0xFF334155),
+ onSurfaceVariant = Color(0xFF94A3B8),
+ outline = Color(0xFF475569),
+ error = Color(0xFFF87171),
+ onError = Color(0xFF7F1D1D),
+ errorContainer = Color(0xFF7F1D1D),
+ onErrorContainer = Color(0xFFFECACA),
+)
+
+private val AppTypography = Typography(
+ headlineLarge = TextStyle(fontSize = 28.sp, fontWeight = FontWeight.Bold),
+ titleLarge = TextStyle(fontSize = 22.sp, fontWeight = FontWeight.SemiBold),
+ titleMedium = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.SemiBold),
+ bodyLarge = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Normal),
+ bodyMedium = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal),
+ bodySmall = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal),
+ labelMedium = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Medium),
+)
+
+private val AppShapes = Shapes(
+ small = RoundedCornerShape(8.dp),
+ medium = RoundedCornerShape(12.dp),
+ large = RoundedCornerShape(16.dp),
+)
+
+@Composable
+fun MyIceTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit,
+) {
+ MaterialTheme(
+ colorScheme = if (darkTheme) DarkColors else LightColors,
+ typography = AppTypography,
+ shapes = AppShapes,
+ content = content,
+ )
+}
+```
+
+- [ ] **Step 4: Compile to verify**
+
+Run: `./gradlew composeApp:compileKotlinDesktop`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add composeApp/build.gradle.kts composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/theme/Theme.kt composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/theme/Dimens.kt
+git commit -m "feat: add teal arctique theme with full M3 color scheme, typography, shapes, spacing tokens
+
+- Complete light/dark color schemes (all M3 slots)
+- Typography scale (headlineLarge through labelMedium)
+- Shape tokens (small 8dp, medium 12dp, large 16dp)
+- Dimens spacing object (xs through xxl)
+- Add material-icons-extended dependency"
+```
+
+---
+
+### Task 2: Date Grouping Helper (TDD)
+
+**Files:**
+- Modify: `composeApp/src/commonMain/kotlin/ch/parano/myicek/filter/EventFilter.kt` (add `groupEventsByDate`)
+- Test: `composeApp/src/commonTest/kotlin/ch/parano/myicek/filter/EventFilterTest.kt`
+
+**Interfaces:**
+- Produces: `fun groupEventsByDate(events: List): List>>` — groups events by date key (ISO `YYYY-MM-DD`), sorted chronologically. Each pair is `(dateKey, eventsForThatDay)`.
+
+- [ ] **Step 1: Write the failing tests**
+
+Add these tests to the end of `EventFilterTest.kt` (before the closing `}`):
+
+```kotlin
+ @Test
+ fun testGroupEventsByDateSingleDay() {
+ val events = listOf(
+ event(id = "1", start = "2024-11-10T14:00:00"),
+ event(id = "2", start = "2024-11-10T16:00:00"),
+ )
+ val grouped = groupEventsByDate(events)
+ assertEquals(1, grouped.size)
+ assertEquals("2024-11-10", grouped[0].first)
+ assertEquals(2, grouped[0].second.size)
+ }
+
+ @Test
+ fun testGroupEventsByDateMultipleDays() {
+ val events = listOf(
+ event(id = "1", start = "2024-11-11T10:00:00"),
+ event(id = "2", start = "2024-11-10T14:00:00"),
+ event(id = "3", start = "2024-11-10T16:00:00"),
+ )
+ val grouped = groupEventsByDate(events)
+ assertEquals(2, grouped.size)
+ assertEquals("2024-11-10", grouped[0].first)
+ assertEquals(2, grouped[0].second.size)
+ assertEquals("2024-11-11", grouped[1].first)
+ assertEquals(1, grouped[1].second.size)
+ }
+
+ @Test
+ fun testGroupEventsByDateWithSpaceSeparator() {
+ val events = listOf(
+ event(id = "1", start = "2024-11-10 14:00:00"),
+ )
+ val grouped = groupEventsByDate(events)
+ assertEquals(1, grouped.size)
+ assertEquals("2024-11-10", grouped[0].first)
+ }
+
+ @Test
+ fun testGroupEventsByDateEmpty() {
+ val grouped = groupEventsByDate(emptyList())
+ assertTrue(grouped.isEmpty())
+ }
+```
+
+Also add `import kotlin.test.assertTrue` to the imports at the top of the test file if not already present.
+
+- [ ] **Step 2: Run tests to verify they fail**
+
+Run: `./gradlew composeApp:desktopTest --tests "ch.parano.myicek.filter.EventFilterTest.testGroupEventsByDate*"`
+Expected: FAIL with "Unresolved reference: groupEventsByDate"
+
+- [ ] **Step 3: Implement groupEventsByDate**
+
+Add to the end of `composeApp/src/commonMain/kotlin/ch/parano/myicek/filter/EventFilter.kt` (before the final closing `}`... actually after `distinctSubgroups`):
+
+```kotlin
+fun groupEventsByDate(events: List): List>> =
+ events
+ .sortedBy { it.start }
+ .groupBy { it.start.take(10) }
+ .toList()
+```
+
+- [ ] **Step 4: Run tests to verify they pass**
+
+Run: `./gradlew composeApp:desktopTest --tests "ch.parano.myicek.filter.EventFilterTest.testGroupEventsByDate*"`
+Expected: PASS (4 tests)
+
+- [ ] **Step 5: Run full test suite to check for regressions**
+
+Run: `./gradlew composeApp:desktopTest`
+Expected: All tests PASS
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add composeApp/src/commonMain/kotlin/ch/parano/myicek/filter/EventFilter.kt composeApp/src/commonTest/kotlin/ch/parano/myicek/filter/EventFilterTest.kt
+git commit -m "feat: add groupEventsByDate helper for schedule date headers"
+```
+
+---
+
+### Task 3: Shared State Components (EmptyState, ErrorState, SkeletonCard)
+
+**Files:**
+- Create: `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/EmptyState.kt`
+- Create: `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/ErrorState.kt`
+- Create: `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/SkeletonCard.kt`
+
+**Interfaces:**
+- Produces: `EmptyState(icon: ImageVector, title: String, message: String, modifier: Modifier)` composable
+- Produces: `ErrorState(icon: ImageVector, title: String, message: String, onRetry: () -> Unit, modifier: Modifier)` composable
+- Produces: `SkeletonCard(modifier: Modifier)` composable
+
+- [ ] **Step 1: Create EmptyState.kt**
+
+```kotlin
+// SPDX-License-Identifier: GPL-3.0-or-later
+// MyIce Kotlin Multiplatform — schedule/convocation viewer
+// Copyright (C) 2026 parano.ch
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package ch.parano.myicek.ui.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun EmptyState(
+ icon: ImageVector,
+ title: String,
+ message: String,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ modifier = Modifier.size(48.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Spacer(Modifier.height(Dimens.md))
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ Spacer(Modifier.height(Dimens.sm))
+ Text(
+ text = message,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+}
+```
+
+- [ ] **Step 2: Create ErrorState.kt**
+
+```kotlin
+// SPDX-License-Identifier: GPL-3.0-or-later
+// MyIce Kotlin Multiplatform — schedule/convocation viewer
+// Copyright (C) 2026 parano.ch
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package ch.parano.myicek.ui.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ErrorState(
+ icon: ImageVector,
+ title: String,
+ message: String,
+ onRetry: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ modifier = Modifier.size(48.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Spacer(Modifier.height(Dimens.md))
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ Spacer(Modifier.height(Dimens.sm))
+ Text(
+ text = message,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Spacer(Modifier.height(Dimens.lg))
+ Button(onClick = onRetry) {
+ Text("Réessayer")
+ }
+ }
+}
+```
+
+- [ ] **Step 3: Create SkeletonCard.kt**
+
+```kotlin
+// SPDX-License-Identifier: GPL-3.0-or-later
+// MyIce Kotlin Multiplatform — schedule/convocation viewer
+// Copyright (C) 2026 parano.ch
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package ch.parano.myicek.ui.components
+
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import ch.parano.myicek.ui.theme.Dimens
+
+@Composable
+fun SkeletonCard(modifier: Modifier = Modifier) {
+ val transition = rememberInfiniteTransition(label = "shimmer")
+ val alpha by transition.animateFloat(
+ initialValue = 0.3f,
+ targetValue = 0.6f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(800),
+ repeatMode = RepeatMode.Reverse,
+ ),
+ label = "shimmerAlpha",
+ )
+
+ val skeletonColor = MaterialTheme.colorScheme.surfaceVariant
+ val skeletonModifier = Modifier
+ .alpha(alpha)
+ .background(skeletonColor, RoundedCornerShape(4.dp))
+
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = Dimens.lg, vertical = Dimens.sm)
+ .clip(MaterialTheme.shapes.medium)
+ .background(MaterialTheme.colorScheme.surface)
+ .padding(Dimens.lg),
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(Dimens.sm)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Box(modifier = Modifier
+ .height(20.dp)
+ .width(160.dp)
+ .then(skeletonModifier))
+ Box(modifier = Modifier
+ .size(28.dp)
+ .clip(CircleShape)
+ .then(skeletonModifier))
+ }
+ Box(modifier = Modifier
+ .height(16.dp)
+ .width(200.dp)
+ .then(skeletonModifier))
+ Box(modifier = Modifier
+ .height(16.dp)
+ .width(180.dp)
+ .then(skeletonModifier))
+ }
+ }
+}
+```
+
+- [ ] **Step 4: Compile to verify**
+
+Run: `./gradlew composeApp:compileKotlinDesktop`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/EmptyState.kt composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/ErrorState.kt composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/SkeletonCard.kt
+git commit -m "feat: add EmptyState, ErrorState, and SkeletonCard shared components"
+```
+
+---
+
+### Task 4: EventCard Redesign
+
+**Files:**
+- Modify: `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/EventCard.kt` (full rewrite)
+
+**Interfaces:**
+- Consumes: `Event` model, `formatEventTimeRange(start, end)` from `ch.parano.myicek.util`
+- Produces: `EventCard(event: Event, onClick: () -> Unit, modifier: Modifier)` composable — same signature, new layout
+
+- [ ] **Step 1: Rewrite EventCard.kt**
+
+Replace the entire content of `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/EventCard.kt` with:
+
+```kotlin
+// SPDX-License-Identifier: GPL-3.0-or-later
+// MyIce Kotlin Multiplatform — schedule/convocation viewer
+// Copyright (C) 2026 parano.ch
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package ch.parano.myicek.ui.components
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Place
+import androidx.compose.material.icons.filled.Schedule
+import androidx.compose.material3.AssistChip
+import androidx.compose.material3.AssistChipDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import ch.parano.myicek.models.Event
+import ch.parano.myicek.ui.theme.Dimens
+import ch.parano.myicek.util.formatEventTimeRange
+
+@Composable
+fun EventCard(
+ event: Event,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val isGame = event.eventType == "Jeu"
+ val stripeColor = event.color?.let { parseHexColor(it) }
+ ?: MaterialTheme.colorScheme.primary
+
+ Card(
+ onClick = onClick,
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = Dimens.lg, vertical = Dimens.sm),
+ elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .border(width = 4.dp, color = stripeColor),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(Dimens.lg),
+ verticalArrangement = Arrangement.spacedBy(Dimens.xs),
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = "${event.agegroup} - ${event.name}",
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.weight(1f),
+ )
+ AssistChip(
+ onClick = {},
+ label = {
+ Text(
+ if (isGame) "Match" else "Entraînement",
+ style = MaterialTheme.typography.labelMedium,
+ )
+ },
+ colors = if (isGame) {
+ AssistChipDefaults.assistChipColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ labelColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ )
+ } else {
+ AssistChipDefaults.assistChipColors(
+ containerColor = MaterialTheme.colorScheme.secondaryContainer,
+ labelColor = MaterialTheme.colorScheme.onSecondaryContainer,
+ )
+ },
+ )
+ }
+ Text(
+ text = if (isGame) "Match vs ${event.opponent}" else "Entraînement",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Spacer(Modifier.height(Dimens.xs))
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(Dimens.xs),
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Place,
+ contentDescription = null,
+ modifier = Modifier.width(18.dp).height(18.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Text(
+ text = event.place,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(Dimens.xs),
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Schedule,
+ contentDescription = null,
+ modifier = Modifier.width(18.dp).height(18.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Text(
+ text = formatEventTimeRange(event.start, event.end),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+ }
+ }
+}
+
+private fun parseHexColor(hex: String): Color? {
+ val value = hex.removePrefix("#").toLongOrNull(16) ?: return null
+ return Color(0xFF000000 or value)
+}
+```
+
+- [ ] **Step 2: Compile to verify**
+
+Run: `./gradlew composeApp:compileKotlinDesktop`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/EventCard.kt
+git commit -m "feat: redesign EventCard with color border, type badge, leading icons
+
+- 4dp colored border from event.color (fallback primary)
+- AssistChip badge: Match (teal) vs Entraînement (slate)
+- Opponent line only for games, 'Entraînement' for practices
+- Place and Schedule icons leading info lines
+- Condensed time range via formatEventTimeRange"
+```
+
+---
+
+### Task 5: FilterChips Component
+
+**Files:**
+- Create: `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/FilterChips.kt`
+- Delete: `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/FilterDropdown.kt`
+
+**Interfaces:**
+- Produces: `TypeFilterSegmented(selected: TypeFilter, onSelect: (TypeFilter) -> Unit, modifier: Modifier)` — 3-segment `SegmentedButton` for Tous/Matchs/Entraînements
+- Produces: `FilterAssistChip(label: String, selectedValue: String?, options: List, onSelect: (String?) -> Unit, modifier: Modifier)` — `AssistChip` with dropdown menu
+
+- [ ] **Step 1: Create FilterChips.kt**
+
+```kotlin
+// SPDX-License-Identifier: GPL-3.0-or-later
+// MyIce Kotlin Multiplatform — schedule/convocation viewer
+// Copyright (C) 2026 parano.ch
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package ch.parano.myicek.ui.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material3.AssistChip
+import androidx.compose.material3.AssistChipDefaults
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import ch.parano.myicek.filter.TypeFilter
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TypeFilterSegmented(
+ selected: TypeFilter,
+ onSelect: (TypeFilter) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) {
+ val options = listOf(
+ TypeFilter.ALL to "Tous",
+ TypeFilter.GAMES to "Matchs",
+ TypeFilter.PRACTICES to "Entraînements",
+ )
+ options.forEachIndexed { index, (filter, label) ->
+ SegmentedButton(
+ selected = selected == filter,
+ onClick = { onSelect(filter) },
+ shape = SegmentedButtonDefaults.itemShape(index, options.size),
+ ) {
+ Text(label)
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun FilterAssistChip(
+ label: String,
+ selectedValue: String?,
+ options: List,
+ onSelect: (String?) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ var expanded by remember { mutableStateOf(false) }
+ val displayValue = selectedValue ?: "Tous"
+
+ Box(modifier = modifier) {
+ AssistChip(
+ onClick = { expanded = true },
+ label = { Text("$label: $displayValue") },
+ trailingIcon = {
+ Icon(
+ imageVector = Icons.Filled.ArrowDropDown,
+ contentDescription = null,
+ )
+ },
+ colors = if (selectedValue != null) {
+ AssistChipDefaults.assistChipColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ labelColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ )
+ } else {
+ AssistChipDefaults.assistChipColors()
+ },
+ )
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+ DropdownMenuItem(
+ text = { Text("Tous") },
+ onClick = {
+ onSelect(null)
+ expanded = false
+ },
+ )
+ options.forEach { option ->
+ DropdownMenuItem(
+ text = { Text(option) },
+ onClick = {
+ onSelect(option)
+ expanded = false
+ },
+ )
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Delete FilterDropdown.kt**
+
+Delete `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/FilterDropdown.kt`:
+
+```bash
+rm composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/FilterDropdown.kt
+```
+
+- [ ] **Step 3: Compile to verify (expect failure — ScheduleScreen still references FilterDropdown)**
+
+Run: `./gradlew composeApp:compileKotlinDesktop`
+Expected: FAIL — `Unresolved reference: FilterDropdown` in ScheduleScreen. This is expected; ScheduleScreen will be updated in Task 7.
+
+Note: We accept this intermediate broken state because ScheduleScreen is fully rewritten in Task 7 which resolves the reference. If you prefer to keep the build green between tasks, leave FilterDropdown.kt in place and delete it in Task 7 instead.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/FilterChips.kt composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/FilterDropdown.kt
+git commit -m "feat: add FilterChips (SegmentedButton + AssistChip), remove FilterDropdown
+
+- TypeFilterSegmented: 3-segment single-choice for Tous/Matchs/Entraînements
+- FilterAssistChip: chip with dropdown for account/age/subgroup
+- Selected chips highlighted with primaryContainer
+- Note: ScheduleScreen update in next task resolves FilterDropdown reference"
+```
+
+---
+
+### Task 6: LoginScreen Redesign
+
+**Files:**
+- Modify: `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/screens/LoginScreen.kt` (full rewrite)
+
+- [ ] **Step 1: Rewrite LoginScreen.kt**
+
+Replace the entire content of `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/screens/LoginScreen.kt` with:
+
+```kotlin
+// SPDX-License-Identifier: GPL-3.0-or-later
+// MyIce Kotlin Multiplatform — schedule/convocation viewer
+// Copyright (C) 2026 parano.ch
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package ch.parano.myicek.ui.screens
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Login
+import androidx.compose.material.icons.filled.SportsHockey
+import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import ch.parano.myicek.ui.theme.Dimens
+import ch.parano.myicek.viewmodel.AuthState
+import ch.parano.myicek.viewmodel.AuthViewModel
+
+@Composable
+fun LoginScreen(
+ viewModel: AuthViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val state by viewModel.state.collectAsState()
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ LaunchedEffect(state) {
+ if (state is AuthState.Unauthenticated) {
+ snackbarHostState.showSnackbar("Erreur de connexion")
+ }
+ }
+
+ Scaffold(
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ modifier = modifier,
+ ) { padding ->
+ Column(
+ modifier = Modifier.fillMaxSize().padding(padding),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Box(
+ modifier = Modifier
+ .size(80.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primaryContainer),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ imageVector = Icons.Filled.SportsHockey,
+ contentDescription = null,
+ modifier = Modifier.size(48.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer,
+ )
+ }
+ Spacer(Modifier.height(Dimens.lg))
+ Text(
+ text = "MyIce",
+ style = MaterialTheme.typography.headlineLarge,
+ fontWeight = FontWeight.Bold,
+ )
+ Text(
+ text = "Votre calendrier hockey",
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Spacer(Modifier.height(Dimens.xxl))
+ if (state is AuthState.Loading) {
+ CircularProgressIndicator()
+ } else {
+ Button(
+ onClick = { viewModel.login() },
+ modifier = Modifier.height(48.dp),
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.Login,
+ contentDescription = null,
+ )
+ Spacer(Modifier.size(Dimens.sm))
+ Text("Se connecter avec Infomaniak")
+ }
+ }
+ }
+ }
+}
+```
+
+Note: `Icons.Filled.SportsHockey` requires the `material-icons-extended` dependency added in Task 1.
+
+- [ ] **Step 2: Compile to verify**
+
+Run: `./gradlew composeApp:compileKotlinDesktop`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/screens/LoginScreen.kt
+git commit -m "feat: redesign LoginScreen with icon, branding, polished layout
+
+- Sports icon in primaryContainer circle (80dp)
+- 'MyIce' branding (dropped 'K' tech codename)
+- 'Votre calendrier hockey' subtitle
+- Login icon on connect button
+- Centered vertical layout with proper spacing"
+```
+
+---
+
+### Task 7: ScheduleScreen Redesign
+
+**Files:**
+- Modify: `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/screens/ScheduleScreen.kt` (full rewrite)
+
+**Interfaces:**
+- Consumes: `TypeFilterSegmented`, `FilterAssistChip` from Task 5, `EmptyState`, `ErrorState`, `SkeletonCard` from Task 3, `EventCard` from Task 4, `groupEventsByDate` from Task 2, `formatEventDate` from util
+
+- [ ] **Step 1: Rewrite ScheduleScreen.kt**
+
+Replace the entire content of `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/screens/ScheduleScreen.kt` with:
+
+```kotlin
+// SPDX-License-Identifier: GPL-3.0-or-later
+// MyIce Kotlin Multiplatform — schedule/convocation viewer
+// Copyright (C) 2026 parano.ch
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package ch.parano.myicek.ui.screens
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Logout
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.pulltorefresh.PullToRefreshBox
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.foundation.background
+import androidx.compose.ui.Modifier
+import androidx.compose.material.icons.filled.CalendarClear
+import androidx.compose.material.icons.filled.CloudOff
+import androidx.compose.material3.AssistChip
+import ch.parano.myicek.filter.TypeFilter
+import ch.parano.myicek.filter.groupEventsByDate
+import ch.parano.myicek.ui.components.EmptyState
+import ch.parano.myicek.ui.components.ErrorState
+import ch.parano.myicek.ui.components.EventCard
+import ch.parano.myicek.ui.components.FilterAssistChip
+import ch.parano.myicek.ui.components.SkeletonCard
+import ch.parano.myicek.ui.components.TypeFilterSegmented
+import ch.parano.myicek.ui.theme.Dimens
+import ch.parano.myicek.util.formatEventDate
+import ch.parano.myicek.viewmodel.AuthViewModel
+import ch.parano.myicek.viewmodel.ScheduleViewModel
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+@Composable
+fun ScheduleScreen(
+ scheduleViewModel: ScheduleViewModel,
+ authViewModel: AuthViewModel,
+ onEventClick: (gameId: String, account: String, eventTitle: String, eventStart: String, eventEnd: String) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val scheduleState by scheduleViewModel.state.collectAsState()
+
+ LaunchedEffect(scheduleState.selectedAccount) {
+ if (scheduleState.selectedAccount != null && scheduleState.events.isEmpty()) {
+ scheduleViewModel.loadCachedSchedule()
+ }
+ }
+
+ val hasActiveFilters = scheduleState.selectedAgegroup != null ||
+ scheduleState.selectedSubgroup != null ||
+ scheduleState.typeFilter != TypeFilter.DEFAULT
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Calendrier") },
+ actions = {
+ IconButton(onClick = { scheduleViewModel.refreshSchedule() }) {
+ Icon(
+ imageVector = Icons.Filled.Refresh,
+ contentDescription = "Rafraîchir",
+ )
+ }
+ IconButton(onClick = {
+ scheduleViewModel.clearCache()
+ authViewModel.logout()
+ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.Logout,
+ contentDescription = "Déconnexion",
+ )
+ }
+ },
+ )
+ },
+ modifier = modifier,
+ ) { padding ->
+ Column(
+ modifier = Modifier.fillMaxSize().padding(padding),
+ ) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = Dimens.lg, vertical = Dimens.sm),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant,
+ ),
+ ) {
+ Column(
+ modifier = Modifier.padding(Dimens.lg),
+ verticalArrangement = Arrangement.spacedBy(Dimens.sm),
+ ) {
+ TypeFilterSegmented(
+ selected = scheduleState.typeFilter,
+ onSelect = { scheduleViewModel.setTypeFilter(it) },
+ )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(Dimens.sm),
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ FilterAssistChip(
+ label = "Compte",
+ selectedValue = scheduleState.accounts.find { it.name == scheduleState.selectedAccount }?.label,
+ options = scheduleState.accounts.map { it.label },
+ onSelect = { label ->
+ val account = scheduleState.accounts.find { it.label == label }
+ if (account != null) scheduleViewModel.setAccount(account.name)
+ },
+ modifier = Modifier.weight(1f),
+ )
+ FilterAssistChip(
+ label = "Âge",
+ selectedValue = scheduleState.selectedAgegroup,
+ options = scheduleState.agegroups,
+ onSelect = { scheduleViewModel.setAgegroup(it) },
+ modifier = Modifier.weight(1f),
+ )
+ }
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(Dimens.sm),
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ FilterAssistChip(
+ label = "Sous-groupe",
+ selectedValue = scheduleState.selectedSubgroup,
+ options = scheduleState.subgroups,
+ onSelect = { scheduleViewModel.setSubgroup(it) },
+ modifier = Modifier.weight(1f),
+ )
+ if (hasActiveFilters) {
+ AssistChipClear(
+ onClear = {
+ scheduleViewModel.setAgegroup(null)
+ scheduleViewModel.setSubgroup(null)
+ scheduleViewModel.setTypeFilter(TypeFilter.DEFAULT)
+ },
+ modifier = Modifier.weight(1f),
+ )
+ }
+ }
+ }
+ }
+
+ PullToRefreshBox(
+ isRefreshing = scheduleState.isRefreshing,
+ onRefresh = { scheduleViewModel.refreshSchedule() },
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ when {
+ scheduleState.isLoading && scheduleState.events.isEmpty() -> {
+ LazyColumn {
+ items(4) { SkeletonCard() }
+ }
+ }
+ scheduleState.error != null && scheduleState.events.isEmpty() -> {
+ ErrorState(
+ icon = Icons.Filled.CloudOff,
+ title = "Erreur de chargement",
+ message = scheduleState.error!!,
+ onRetry = { scheduleViewModel.refreshSchedule() },
+ )
+ }
+ scheduleState.filteredEvents.isEmpty() -> {
+ EmptyState(
+ icon = Icons.Filled.CalendarClear,
+ title = "Aucun événement disponible",
+ message = "Tirez vers le bas pour rafraîchir le calendrier",
+ )
+ }
+ else -> {
+ val grouped = groupEventsByDate(scheduleState.filteredEvents)
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ grouped.forEach { (dateKey, dayEvents) ->
+ stickyHeader {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.surface)
+ .padding(horizontal = Dimens.lg, vertical = Dimens.sm),
+ ) {
+ Text(
+ text = formatEventDate(dateKey + "T00:00:00")
+ .replaceFirstChar { it.uppercase() },
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ HorizontalDivider()
+ }
+ }
+ items(dayEvents) { event ->
+ EventCard(
+ event = event,
+ onClick = {
+ onEventClick(
+ event.idEvent,
+ scheduleState.selectedAccount!!,
+ event.title,
+ event.start,
+ event.end,
+ )
+ },
+ )
+ }
+ }
+ if (scheduleState.lastUpdated != null) {
+ item {
+ Text(
+ text = "Dernière mise à jour: ${formatLastUpdated(scheduleState.lastUpdated!!)}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(Dimens.lg),
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun AssistChipClear(
+ onClear: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ AssistChip(
+ onClick = onClear,
+ label = { Text("Effacer") },
+ trailingIcon = {
+ Icon(
+ imageVector = Icons.Filled.Close,
+ contentDescription = "Effacer les filtres",
+ )
+ },
+ modifier = modifier,
+ )
+}
+
+private fun formatLastUpdated(timestamp: Long): String {
+ val instant = kotlinx.datetime.Instant.fromEpochMilliseconds(timestamp)
+ val local = instant.toLocalDateTime(kotlinx.datetime.TimeZone.currentSystemDefault())
+ return "${local.date} ${local.time}"
+}
+```
+
+Note: Some imports may need adjustment. The key changes are: TopAppBar with icon buttons, FilterChips replacing FilterDropdown, sticky date headers, shared EmptyState/ErrorState, SkeletonCard loading, `lastUpdated` display, and `account.label` in the account chip.
+
+- [ ] **Step 2: Fix any compile errors**
+
+Run: `./gradlew composeApp:compileKotlinDesktop`
+
+If there are import errors, fix them. Common issues:
+- `stickyHeader` needs `import androidx.compose.foundation.ExperimentalFoundationApi` and `import androidx.compose.foundation.lazy.stickyHeader`
+- `background` needs `import androidx.compose.foundation.background`
+- `toUpperCase` may need adjustment — use `replaceFirstChar { it.uppercase() }` instead
+
+Fix any errors until compilation succeeds.
+
+- [ ] **Step 3: Verify compilation succeeds**
+
+Run: `./gradlew composeApp:compileKotlinDesktop`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 4: Run tests to check for regressions**
+
+Run: `./gradlew composeApp:desktopTest`
+Expected: All tests PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/screens/ScheduleScreen.kt
+git commit -m "feat: redesign ScheduleScreen with icon actions, filter chips, sticky date headers
+
+- TopAppBar: Refresh/Logout icon buttons, 'Calendrier' title
+- TypeFilterSegmented + FilterAssistChips replace collapsible filter card
+- Account dropdown uses label instead of name
+- 'Effacer' chip appears when filters are active
+- Sticky date headers group events by day
+- SkeletonCard loading state (4 shimmer cards)
+- ErrorState/EmptyState shared components
+- lastUpdated timestamp displayed at bottom
+- French accents corrected throughout"
+```
+
+---
+
+### Task 8: EventDetailScreen Redesign
+
+**Files:**
+- Modify: `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/screens/EventDetailScreen.kt` (full rewrite)
+
+- [ ] **Step 1: Rewrite EventDetailScreen.kt**
+
+Replace the entire content of `composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/screens/EventDetailScreen.kt` with:
+
+```kotlin
+// SPDX-License-Identifier: GPL-3.0-or-later
+// MyIce Kotlin Multiplatform — schedule/convocation viewer
+// Copyright (C) 2026 parano.ch
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package ch.parano.myicek.ui.screens
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.CalendarMonth
+import androidx.compose.material.icons.filled.ErrorOutline
+import androidx.compose.material.icons.filled.Event
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material.icons.filled.Place
+import androidx.compose.material.icons.filled.Schedule
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material3.AssistChip
+import androidx.compose.material3.AssistChipDefaults
+import androidx.compose.material3.Badge
+import androidx.compose.material3.BadgedBox
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import ch.parano.myicek.ui.PlatformBackHandler
+import ch.parano.myicek.ui.components.ErrorState
+import ch.parano.myicek.ui.components.LoadingIndicator
+import ch.parano.myicek.ui.theme.Dimens
+import ch.parano.myicek.util.formatEventDate
+import ch.parano.myicek.util.formatEventTimeRange
+import ch.parano.myicek.viewmodel.EventDetailViewModel
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun EventDetailScreen(
+ gameId: String,
+ account: String,
+ eventTitle: String,
+ eventStart: String,
+ eventEnd: String,
+ viewModel: EventDetailViewModel,
+ onBack: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ PlatformBackHandler { onBack() }
+
+ LaunchedEffect(gameId, account) {
+ viewModel.loadEventDetail(gameId, account)
+ }
+
+ val state by viewModel.state.collectAsState()
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(eventTitle) },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Retour",
+ )
+ }
+ },
+ )
+ },
+ modifier = modifier,
+ ) { padding ->
+ when {
+ state.isLoading -> {
+ LoadingIndicator(modifier = Modifier.padding(padding))
+ }
+ state.error != null -> {
+ ErrorState(
+ icon = Icons.Filled.ErrorOutline,
+ title = "Erreur",
+ message = state.error!!,
+ onRetry = { viewModel.loadEventDetail(gameId, account) },
+ modifier = Modifier.padding(padding),
+ )
+ }
+ state.eventDetail != null -> {
+ val detail = state.eventDetail!!
+ val players = detail.convocation.available.sortedBy { it.number?.toIntOrNull() ?: Int.MAX_VALUE }
+ val staff = detail.convocation.staff
+ val hasPlayers = players.isNotEmpty()
+ val hasStaff = staff.isNotEmpty()
+
+ LazyColumn(
+ modifier = Modifier.fillMaxSize().padding(padding),
+ contentPadding = androidx.compose.foundation.layout.PaddingValues(Dimens.lg),
+ verticalArrangement = Arrangement.spacedBy(Dimens.lg),
+ ) {
+ item {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
+ ) {
+ Column(
+ modifier = Modifier.padding(Dimens.lg),
+ verticalArrangement = Arrangement.spacedBy(Dimens.md),
+ ) {
+ InfoRow(Icons.Filled.Event, "Type", detail.type)
+ InfoRow(Icons.Filled.Place, "Lieu", detail.place)
+ InfoRow(Icons.Filled.CalendarMonth, "Date", formatEventDate(eventStart))
+ InfoRow(Icons.Filled.Schedule, "Heure", formatEventTimeRange(eventStart, eventEnd))
+ }
+ }
+ }
+
+ if (!hasPlayers && !hasStaff) {
+ item {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ ),
+ ) {
+ Row(
+ modifier = Modifier.padding(Dimens.lg),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(Dimens.sm),
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Warning,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onErrorContainer,
+ )
+ Text(
+ text = "Aucun joueur ni personnel convoqué",
+ color = MaterialTheme.colorScheme.onErrorContainer,
+ )
+ }
+ }
+ }
+ }
+
+ if (hasPlayers) {
+ item {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(Dimens.sm),
+ ) {
+ Text(
+ text = "Joueurs",
+ style = MaterialTheme.typography.titleMedium,
+ )
+ BadgedBox(badge = { Badge { Text("${players.size}") } }) {}
+ }
+ }
+ item {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
+ ) {
+ Column {
+ players.forEach { player ->
+ ListItem(
+ leadingContent = {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primaryContainer),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = player.number?.let { "#$it" } ?: "",
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
+ )
+ }
+ },
+ headlineContent = {
+ Text("${player.fname} ${player.lname}")
+ },
+ supportingContent = {
+ val parts = mutableListOf()
+ if (!player.position.isNullOrBlank()) parts.add(player.position)
+ if (player.dob.isNotBlank()) parts.add("(née ${player.dob.take(4)})")
+ if (parts.isNotEmpty()) Text(parts.joinToString(" "))
+ },
+ )
+ }
+ }
+ }
+ }
+ }
+
+ if (hasStaff) {
+ item {
+ Text(
+ text = "Personnel",
+ style = MaterialTheme.typography.titleMedium,
+ )
+ }
+ item {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
+ ) {
+ Column {
+ staff.forEach { member ->
+ ListItem(
+ leadingContent = {
+ Icon(
+ imageVector = Icons.Filled.Person,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ },
+ headlineContent = {
+ Text("${member.fname} ${member.lname}")
+ },
+ trailingContent = {
+ AssistChip(
+ onClick = {},
+ label = { Text(member.role) },
+ colors = AssistChipDefaults.assistChipColors(
+ containerColor = MaterialTheme.colorScheme.secondaryContainer,
+ labelColor = MaterialTheme.colorScheme.onSecondaryContainer,
+ ),
+ )
+ },
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ else -> {
+ Column(
+ modifier = Modifier.fillMaxSize().padding(padding),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text("Aucune donnée disponible")
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun InfoRow(
+ icon: androidx.compose.ui.graphics.vector.ImageVector,
+ label: String,
+ value: String,
+) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(Dimens.sm),
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Column {
+ Text(
+ text = label.uppercase(),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Text(
+ text = value,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Compile to verify**
+
+Run: `./gradlew composeApp:compileKotlinDesktop`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/screens/EventDetailScreen.kt
+git commit -m "feat: redesign EventDetailScreen with icon back, styled info, ListItem players
+
+- ArrowBack icon replaces TextButton('Retour')
+- Info card with label/value rows and leading icons
+- Players as ListItem with number avatar in primaryContainer circle
+- Staff as ListItem with role AssistChip
+- ErrorState shared component
+- errorContainer for empty convocation warning
+- French accents corrected"
+```
+
+---
+
+### Task 9: App.kt Transitions & Desktop Branding
+
+**Files:**
+- Modify: `composeApp/src/commonMain/kotlin/ch/parano/myicek/App.kt` (add AnimatedContent)
+- Modify: `composeApp/src/desktopMain/kotlin/ch/parano/myicek/Main.kt` (title "MyIce")
+
+- [ ] **Step 1: Update App.kt with AnimatedContent**
+
+In `composeApp/src/commonMain/kotlin/ch/parano/myicek/App.kt`, replace the `MyIceTheme` block (lines 85-105) with:
+
+```kotlin
+ MyIceTheme {
+ AnimatedContent(
+ targetState = currentScreen,
+ transitionSpec = {
+ androidx.compose.animation.fadeIn(
+ androidx.compose.animation.core.tween(300),
+ ) togetherWith androidx.compose.animation.fadeOut(
+ androidx.compose.animation.core.tween(300),
+ )
+ },
+ label = "screenTransition",
+ ) { screen ->
+ when (screen) {
+ Screen.Login -> LoginScreen(viewModel = authViewModel)
+ Screen.Schedule -> ScheduleScreen(
+ scheduleViewModel = scheduleViewModel,
+ authViewModel = authViewModel,
+ onEventClick = { gameId, account, eventTitle, eventStart, eventEnd ->
+ currentScreen = Screen.EventDetail(gameId, account, eventTitle, eventStart, eventEnd)
+ },
+ )
+ is Screen.EventDetail -> EventDetailScreen(
+ gameId = screen.gameId,
+ account = screen.account,
+ eventTitle = screen.eventTitle,
+ eventStart = screen.eventStart,
+ eventEnd = screen.eventEnd,
+ viewModel = eventDetailViewModel,
+ onBack = { currentScreen = Screen.Schedule },
+ )
+ }
+ }
+ }
+```
+
+Add these imports at the top of `App.kt`:
+
+```kotlin
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.togetherWith
+```
+
+- [ ] **Step 2: Update desktop Main.kt title**
+
+In `composeApp/src/desktopMain/kotlin/ch/parano/myicek/Main.kt`, change the Window title:
+
+```kotlin
+ Window(onCloseRequest = ::exitApplication, title = "MyIce") {
+```
+
+- [ ] **Step 3: Compile to verify**
+
+Run: `./gradlew composeApp:compileKotlinDesktop`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 4: Run full test suite**
+
+Run: `./gradlew composeApp:desktopTest`
+Expected: All tests PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add composeApp/src/commonMain/kotlin/ch/parano/myicek/App.kt composeApp/src/desktopMain/kotlin/ch/parano/myicek/Main.kt
+git commit -m "feat: add screen transitions and rebrand to MyIce
+
+- AnimatedContent with fade transitions between screens (300ms)
+- Desktop window title 'MyIce' (dropped 'K' codename)"
+```
+
+---
+
+### Task 10: Final Verification
+
+- [ ] **Step 1: Full compile**
+
+Run: `./gradlew composeApp:compileKotlinDesktop`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 2: Full test suite**
+
+Run: `./gradlew composeApp:desktopTest`
+Expected: All tests PASS
+
+- [ ] **Step 3: Android compile check**
+
+Run: `./gradlew composeApp:assembleDebug`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 4: Visual verification**
+
+Run: `./gradlew composeApp:run`
+Expected: App launches with teal theme, redesigned login screen. Log in and verify:
+- Login: icon + "MyIce" branding
+- Schedule: icon actions, filter chips, sticky date headers, event cards with badges
+- Event detail: arrow back, info card with icons, player list items with number avatars
+
+- [ ] **Step 5: Commit any remaining fixes**
+
+```bash
+git add -A
+git commit -m "fix: final adjustments from visual verification"
+```