diff --git a/docs/superpowers/plans/2026-06-08-01-foundation.md b/docs/superpowers/plans/2026-06-08-01-foundation.md
new file mode 100644
index 0000000..6c8d9dd
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-08-01-foundation.md
@@ -0,0 +1,2047 @@
+# Calendula - Plan 01: Foundation & CI 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:** Liefere ein vollständig konfiguriertes Android-Projektgerüst für Calendula: build-/install-/testbar, mit Material 3 Expressive Theme, korrektem Adaptive-Icon, DE/EN-i18n-Grundgerüst, Hilt, DataStore, und grünen Gitea-CI-Workflows. Am Ende läuft die App und zeigt einen schlichten "Calendula"-Screen mit korrekt themed Surface, das Icon erscheint im Launcher als "1" auf Slate-Squircle, und `./gradlew test lint assembleDebug` ist grün.
+
+**Architecture:** Single Gradle module `:app`, Kotlin 2.1 + Jetpack Compose mit Material 3 Expressive (1.5+), Hilt für DI, DataStore-Preferences für App-Settings. Build via Gradle Kotlin DSL und Version Catalog (`libs.versions.toml`). CI über Gitea Workflows (adaptiert von HouseHoldKeaper, Flutter-Steps durch Gradle-Steps ersetzt). Alle Resourcen + Strings i18n-fähig ab Tag 1 (DE + EN).
+
+**Tech Stack:**
+- Kotlin 2.1.0, Gradle 8.11.1, AGP 8.7.2, JVM Target 17
+- minSdk 29, targetSdk 36, compileSdk 36
+- Jetpack Compose BOM 2025.05.00, Material3 1.5.0 (Expressive APIs)
+- Hilt 2.53 + KSP 2.1.0-1.0.29
+- AndroidX DataStore Preferences 1.1.1
+- Tests: JUnit5 5.11.x + Truth 1.4.4 + Compose UI Test (BOM)
+- CI: Gitea Actions auf Docker-Runner (Java 17 + Android SDK 36)
+
+---
+
+## File Structure
+
+Files this plan creates (all relative to project root `/home/jlmak/Projects/jlmak/cal/`):
+
+**Repo-Meta:**
+- `LICENSE` — MIT
+- `README.md` — Short app description + build instructions
+- `CHANGELOG.md` — Started with `## [Unreleased]` section
+- `.gitignore` — Android Studio + JetBrains + Kotlin/Gradle
+- `.gitattributes` — Line ending normalization
+- `.editorconfig` — Code style
+
+**.planning/ (project tracking docs, HouseHoldKeaper convention):**
+- `.planning/PROJECT.md` — high-level summary
+- `.planning/REQUIREMENTS.md` — feature requirements (validated/active/out-of-scope)
+- `.planning/ROADMAP.md` — V1/V2/V3 milestones
+- `.planning/STATE.md` — current implementation status
+
+**F-Droid metadata:**
+- `fdroid-metadata/de.jeanlucmakiola.calendula.yml`
+- `fdroid-metadata/de.jeanlucmakiola.calendula/en-US/short_description.txt`
+- `fdroid-metadata/de.jeanlucmakiola.calendula/en-US/full_description.txt`
+- `fdroid-metadata/de.jeanlucmakiola.calendula/de/short_description.txt`
+- `fdroid-metadata/de.jeanlucmakiola.calendula/de/full_description.txt`
+
+**Gradle root:**
+- `settings.gradle.kts`
+- `build.gradle.kts`
+- `gradle.properties`
+- `gradle/libs.versions.toml`
+- `gradle/wrapper/gradle-wrapper.properties` (jar generated by `gradle wrapper`)
+
+**App module:**
+- `app/.gitignore`
+- `app/build.gradle.kts`
+- `app/proguard-rules.pro`
+- `app/src/main/AndroidManifest.xml`
+- `app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt`
+- `app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt`
+- `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Color.kt`
+- `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt`
+- `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Type.kt`
+
+**Resources:**
+- `app/src/main/res/values/strings.xml` (English master)
+- `app/src/main/res/values-de/strings.xml` (German)
+- `app/src/main/res/values/colors.xml`
+- `app/src/main/res/values/themes.xml`
+- `app/src/main/res/drawable/ic_launcher_background.xml`
+- `app/src/main/res/drawable/ic_launcher_foreground.xml`
+- `app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml`
+- `app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml`
+
+**Tests:**
+- `app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/ColorSchemeTest.kt`
+- `app/src/androidTest/java/de/jeanlucmakiola/calendula/MainActivitySmokeTest.kt`
+
+**CI:**
+- `.gitea/workflows/ci.yaml`
+- `.gitea/workflows/release.yaml`
+
+---
+
+## Task 1: Repo Meta Files (LICENSE, README, CHANGELOG, gitignore)
+
+**Files:**
+- Create: `LICENSE`
+- Create: `README.md`
+- Create: `CHANGELOG.md`
+- Create: `.gitignore`
+- Create: `.gitattributes`
+- Create: `.editorconfig`
+
+- [ ] **Step 1: Create `LICENSE` (MIT)**
+
+```
+MIT License
+
+Copyright (c) 2026 Jean-Luc Makiola
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+- [ ] **Step 2: Create `README.md`**
+
+```markdown
+# Calendula
+
+A modern Material 3 Expressive calendar app for Android.
+
+Calendula is named after the flower of the same name, whose name comes from
+the Latin *kalendae* — the first day of the month — the same root as the
+word "calendar". Calendula reads from Android's built-in `CalendarContract`,
+so any calendar source synced to your device (CalDAV via DAVx5, Google,
+local, WebCal subscriptions, ...) is shown.
+
+## Features (V1)
+
+- Month, Week, and Day views
+- Read-only event details (write support comes in V2)
+- Multi-calendar visibility toggle
+- Material You Dynamic Color (Android 12+)
+- Light/Dark theme follows system
+- German + English UI
+
+## Building
+
+Requires Android SDK 36 and JDK 17.
+
+```bash
+# Bootstrap the gradle wrapper (one-time, requires a host gradle install)
+gradle wrapper --gradle-version 8.11.1 --distribution-type bin
+
+# Build debug APK
+./gradlew assembleDebug
+
+# Run unit tests
+./gradlew test
+
+# Run lint
+./gradlew lint
+```
+
+## License
+
+[MIT](LICENSE) — Jean-Luc Makiola, 2026
+```
+
+- [ ] **Step 3: Create `CHANGELOG.md`**
+
+```markdown
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+- Initial project scaffold with Material 3 Expressive theme
+- Adaptive launcher icon (numeral "1" on slate squircle, referencing *kalendae*)
+- German + English localization infrastructure
+- Hilt + DataStore dependency injection and preferences setup
+- Gitea CI/CD workflows for build, test, lint, and release
+```
+
+- [ ] **Step 4: Create `.gitignore`**
+
+```
+# Built application files
+*.apk
+*.aar
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+release/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log files
+*.log
+
+# Android Studio / IntelliJ
+*.iml
+.idea/
+.navigation/
+captures/
+.externalNativeBuild/
+.cxx/
+
+# Keystore files
+*.jks
+*.keystore
+/key.properties
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# F-Droid local artifacts (the pipeline generates them in CI)
+fdroid/repo/
+fdroid/keystore.p12
+
+# KSP
+.ksp/
+```
+
+- [ ] **Step 5: Create `.gitattributes`**
+
+```
+* text=auto eol=lf
+*.bat text eol=crlf
+*.jar binary
+*.png binary
+*.jpg binary
+*.gif binary
+*.webp binary
+```
+
+- [ ] **Step 6: Create `.editorconfig`**
+
+```ini
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{yml,yaml,toml,json,md}]
+indent_size = 2
+
+[*.{kt,kts}]
+ij_kotlin_packages_to_use_import_on_demand = unset
+
+[Makefile]
+indent_style = tab
+```
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add LICENSE README.md CHANGELOG.md .gitignore .gitattributes .editorconfig
+git commit -m "chore: add repo meta files (LICENSE, README, CHANGELOG, gitignore)"
+```
+
+---
+
+## Task 2: Project Planning Docs (`.planning/`)
+
+Adopts the HouseHoldKeaper convention. These docs live alongside the spec and track higher-level project state.
+
+**Files:**
+- Create: `.planning/PROJECT.md`
+- Create: `.planning/REQUIREMENTS.md`
+- Create: `.planning/ROADMAP.md`
+- Create: `.planning/STATE.md`
+
+- [ ] **Step 1: Create `.planning/PROJECT.md`**
+
+```markdown
+# Calendula
+
+## What This Is
+
+A modern Material 3 Expressive Android calendar app, read-only V1. Lives
+entirely on top of Android's `CalendarContract` — any calendar synced to the
+device (CalDAV via DAVx5, Google, local, WebCal, …) shows up automatically.
+The differentiator is visual: real Material 3 Expressive design that no
+existing FOSS calendar app delivers.
+
+## Core Value
+
+A calendar app that is genuinely pleasant to look at and use, without
+re-inventing the calendar sync stack — leave that to DAVx5 and the system.
+
+## Current Milestone
+
+**v0.1 — Foundation & CI:** Buildable Android project scaffold with theme,
+icon, i18n, Hilt, DataStore, green CI.
+
+## Stack
+
+Kotlin 2.1, Jetpack Compose + Material 3 Expressive, Hilt, DataStore.
+Gradle Kotlin DSL with Version Catalog.
+
+Read-only V1, write support V2.
+
+Android-only (minSdk 29, targetSdk 36). No iOS.
+
+## Naming
+
+"Calendula" — Latin *kalendae* ("first day of the month", root of "calendar")
+is also the etymological root of the marigold flower Calendula. The icon
+shows a stylized "1" on a slate squircle.
+
+## Source
+
+Hosted on self-hosted Gitea, released through self-hosted F-Droid repo on
+Hetzner. Same infrastructure as `HouseHoldKeaper`.
+```
+
+- [ ] **Step 2: Create `.planning/REQUIREMENTS.md`**
+
+```markdown
+# Calendula — Requirements
+
+See full design spec: `docs/superpowers/specs/2026-06-08-calendar-app-design.md`
+
+## V1 Scope (Variant "B")
+
+### Validated (shipped)
+- (none yet — first milestone in progress)
+
+### Active (V1)
+
+- [ ] Foundation & CI infrastructure
+- [ ] Data Layer over `CalendarContract`
+- [ ] Permission flow (`READ_CALENDAR`)
+- [ ] Month view (S1)
+- [ ] Week view (S2)
+- [ ] Day view (S3)
+- [ ] Event Detail Sheet (S4)
+- [ ] Multi-Calendar Filter (M3)
+- [ ] Today button + Jump-to-Date (M2)
+- [ ] View-Switcher (M1)
+- [ ] Settings screen (M4)
+- [ ] Empty / no-permission / no-calendars states
+- [ ] German + English localization
+- [ ] Loading/Failure/Success states per screen (architectural pattern)
+
+### Out of Scope (V2+)
+
+- Event create / edit / delete (V2)
+- Home-screen widget
+- Full-text search
+- Quick-add
+- Custom notifications/reminders (system already handles these)
+- Tablet/foldable-specific layouts
+- iOS support (Android-only by design)
+
+## Constraints
+
+- **Tech stack:** Kotlin + Jetpack Compose + Material 3 Expressive, Hilt, DataStore
+- **Platform:** Android 10+ (API 29 minimum), Android 16 (API 36) target
+- **Offline-first:** all data lives in `CalendarContract`; no app-side network
+- **Privacy:** zero telemetry, no analytics
+- **i18n:** German + English from day one
+- **Tests + CI from day one**
+- **License:** MIT
+```
+
+- [ ] **Step 3: Create `.planning/ROADMAP.md`**
+
+```markdown
+# Calendula — Roadmap
+
+## v0.x — Pre-Release
+
+| Version | Milestone | Status |
+|---|---|---|
+| v0.1 | Foundation & CI | in progress |
+| v0.2 | Data Layer & Permission Flow | pending |
+| v0.3 | Month view | pending |
+| v0.4 | Week view | pending |
+| v0.5 | Day view | pending |
+| v0.6 | Event Detail Sheet | pending |
+| v0.7 | Filter & Settings | pending |
+
+## v1.0 — First Public Release
+
+All V1 features shipped, polished, on F-Droid. Read-only calendar.
+
+## v2.0 — Write Support
+
+- Event create / edit / delete via `CalendarContract` writes
+- Quick-add sheet
+- Conflict UX (event modified externally during edit)
+
+## v3.0 — Power-User Features
+
+- Home-screen widget
+- Full-text search
+- Tablet / foldable layouts
+- Optional: ICS file import (drag-and-drop)
+
+Order is indicative — community feedback after V1 may re-prioritize.
+```
+
+- [ ] **Step 4: Create `.planning/STATE.md`**
+
+```markdown
+# Calendula — Current State
+
+*Last updated: 2026-06-08*
+
+## Status
+
+**Milestone:** v0.1 — Foundation & CI
+**Phase:** Implementation starting
+
+## Progress
+
+- [x] Design spec written and committed (`docs/superpowers/specs/2026-06-08-calendar-app-design.md`)
+- [x] V1 design decisions resolved (App name "Calendula", icon, seed color)
+- [x] Plan 01 written (`docs/superpowers/plans/2026-06-08-01-foundation.md`)
+- [ ] Plan 01 executed
+
+## Next
+
+1. Execute Plan 01 to land foundation
+2. Write and execute Plan 02 (Data Layer & Permission Flow)
+3. Iterate on UI design (mockups) before screens are built
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add .planning/
+git commit -m "docs: add .planning/ project-tracking documents"
+```
+
+---
+
+## Task 3: F-Droid Metadata Stub
+
+**Files:**
+- Create: `fdroid-metadata/de.jeanlucmakiola.calendula.yml`
+- Create: `fdroid-metadata/de.jeanlucmakiola.calendula/en-US/short_description.txt`
+- Create: `fdroid-metadata/de.jeanlucmakiola.calendula/en-US/full_description.txt`
+- Create: `fdroid-metadata/de.jeanlucmakiola.calendula/de/short_description.txt`
+- Create: `fdroid-metadata/de.jeanlucmakiola.calendula/de/full_description.txt`
+
+- [ ] **Step 1: Create `fdroid-metadata/de.jeanlucmakiola.calendula.yml`**
+
+```yaml
+AuthorName: Jean-Luc Makiola
+License: MIT
+Name: Calendula
+
+Categories:
+ - Time
+
+SourceCode: https://gitea.jeanlucmakiola.de/makiolaj/calendula
+IssueTracker: https://gitea.jeanlucmakiola.de/makiolaj/calendula/issues
+```
+
+- [ ] **Step 2: Create `fdroid-metadata/de.jeanlucmakiola.calendula/en-US/short_description.txt`**
+
+```
+A modern Material 3 Expressive calendar for Android.
+```
+
+- [ ] **Step 3: Create `fdroid-metadata/de.jeanlucmakiola.calendula/en-US/full_description.txt`**
+
+```
+Calendula is a modern, open-source calendar app for Android. It reads from
+the system calendar provider, so any source synced to your device — Nextcloud
+via DAVx5, Google, local, WebCal subscriptions — shows up automatically.
+
+The differentiator is the design: real Material 3 Expressive throughout, with
+dynamic color, expressive motion, and expressive shapes.
+
+V1 is read-only. Event creation, editing, and deletion are planned for V2.
+
+Privacy: zero telemetry, no analytics, no network access — your data never
+leaves the device.
+```
+
+- [ ] **Step 4: Create `fdroid-metadata/de.jeanlucmakiola.calendula/de/short_description.txt`**
+
+```
+Ein moderner Material-3-Expressive-Kalender für Android.
+```
+
+- [ ] **Step 5: Create `fdroid-metadata/de.jeanlucmakiola.calendula/de/full_description.txt`**
+
+```
+Calendula ist eine moderne, quelloffene Kalender-App für Android. Sie liest
+direkt aus dem System-Kalender-Provider — jede Quelle, die mit deinem Gerät
+synchronisiert ist (Nextcloud über DAVx5, Google, lokal, WebCal-Subscriptions)
+erscheint automatisch.
+
+Der Unterschied liegt im Design: echtes Material 3 Expressive durchgehend,
+mit Dynamic Color, expressiven Animationen und neuen Shape-Sprachen.
+
+V1 ist read-only. Erstellen, Bearbeiten und Löschen von Events kommt mit V2.
+
+Datenschutz: keinerlei Telemetrie, kein Tracking, kein Netzwerkzugriff — deine
+Daten bleiben auf dem Gerät.
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add fdroid-metadata/
+git commit -m "chore: add F-Droid metadata for de.jeanlucmakiola.calendula"
+```
+
+---
+
+## Task 4: Gradle Root Setup
+
+**Files:**
+- Create: `settings.gradle.kts`
+- Create: `build.gradle.kts`
+- Create: `gradle.properties`
+- Create: `gradle/libs.versions.toml`
+
+- [ ] **Step 1: Create `settings.gradle.kts`**
+
+```kotlin
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "Calendula"
+include(":app")
+```
+
+- [ ] **Step 2: Create `gradle/libs.versions.toml`**
+
+```toml
+[versions]
+agp = "8.7.2"
+kotlin = "2.1.0"
+ksp = "2.1.0-1.0.29"
+hilt = "2.53"
+coreKtx = "1.15.0"
+lifecycleRuntime = "2.8.7"
+activityCompose = "1.9.3"
+composeBom = "2025.05.00"
+material3 = "1.5.0"
+datastore = "1.1.1"
+junit = "5.11.4"
+junitJupiterPlatform = "1.11.4"
+truth = "1.4.4"
+androidxJunit = "1.2.1"
+espressoCore = "3.6.1"
+
+[libraries]
+# AndroidX core
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntime" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+
+# Compose BOM + libs
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+
+# Material 3 (Expressive lives in this artifact for 1.5+)
+androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
+
+# Hilt
+hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
+hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
+
+# DataStore
+androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
+
+# Unit tests
+junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
+junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" }
+junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junitJupiterPlatform" }
+truth = { group = "com.google.truth", name = "truth", version.ref = "truth" }
+
+# Android tests
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
+```
+
+- [ ] **Step 3: Create `build.gradle.kts` (root)**
+
+```kotlin
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+ alias(libs.plugins.ksp) apply false
+ alias(libs.plugins.hilt) apply false
+}
+```
+
+- [ ] **Step 4: Create `gradle.properties`**
+
+```properties
+# Project-wide Gradle settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+
+# Required for AndroidX.
+android.useAndroidX=true
+
+# Kotlin code style for this project: "official" or "obsolete".
+kotlin.code.style=official
+
+# Enables namespacing of each library's R class, less RAM, faster builds.
+android.nonTransitiveRClass=true
+
+# Use new K2 compiler defaults (Kotlin 2.x).
+kotlin.incremental=true
+
+# Reproducible builds for F-Droid.
+android.uniquePackageNames=true
+```
+
+- [ ] **Step 5: Bootstrap the Gradle wrapper**
+
+This requires a host `gradle` installation. Run once:
+
+```bash
+gradle wrapper --gradle-version 8.11.1 --distribution-type bin
+```
+
+Expected: creates `gradle/wrapper/gradle-wrapper.jar`, `gradle/wrapper/gradle-wrapper.properties`, `gradlew`, `gradlew.bat`.
+
+Then make `gradlew` executable:
+
+```bash
+chmod +x gradlew
+```
+
+- [ ] **Step 6: Verify gradle wrapper works**
+
+```bash
+./gradlew --version
+```
+
+Expected output includes:
+```
+Gradle 8.11.1
+Kotlin: 2.0.x
+```
+
+(Don't worry about the specific Kotlin version in the gradle output — that's gradle's bundled Kotlin, separate from our project Kotlin 2.1.0.)
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add settings.gradle.kts build.gradle.kts gradle.properties gradle/
+chmod +x gradlew && git add gradlew gradlew.bat
+git commit -m "chore: add gradle wrapper + version catalog + root build"
+```
+
+---
+
+## Task 5: App Module Gradle Setup
+
+**Files:**
+- Create: `app/.gitignore`
+- Create: `app/build.gradle.kts`
+- Create: `app/proguard-rules.pro`
+
+- [ ] **Step 1: Create `app/.gitignore`**
+
+```
+/build
+```
+
+- [ ] **Step 2: Create `app/proguard-rules.pro`**
+
+```
+# Keep Hilt-generated classes
+-keep class dagger.hilt.** { *; }
+-keep class * extends dagger.hilt.android.HiltAndroidApp
+
+# Compose Compiler may keep its own; defaults are fine
+-dontwarn org.jetbrains.annotations.**
+```
+
+- [ ] **Step 3: Create `app/build.gradle.kts`**
+
+```kotlin
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.hilt)
+}
+
+android {
+ namespace = "de.jeanlucmakiola.calendula"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "de.jeanlucmakiola.calendula"
+ minSdk = 29
+ targetSdk = 36
+ versionCode = 1
+ versionName = "0.1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables { useSupportLibrary = true }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ debug {
+ applicationIdSuffix = ".debug"
+ isMinifyEnabled = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+
+ testOptions {
+ unitTests.all { test ->
+ test.useJUnitPlatform()
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.compiler)
+
+ implementation(libs.androidx.datastore.preferences)
+
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ testImplementation(libs.junit.jupiter.api)
+ testRuntimeOnly(libs.junit.jupiter.engine)
+ testRuntimeOnly(libs.junit.platform.launcher)
+ testImplementation(libs.truth)
+
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+}
+```
+
+- [ ] **Step 4: Sync gradle**
+
+```bash
+./gradlew help
+```
+
+Expected: Build succeeds. No project setup errors. Some dependency download output is normal on first run.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add app/.gitignore app/build.gradle.kts app/proguard-rules.pro
+git commit -m "chore: configure :app module gradle build"
+```
+
+---
+
+## Task 6: Android Manifest + Base Resources
+
+**Files:**
+- Create: `app/src/main/AndroidManifest.xml`
+- Create: `app/src/main/res/values/strings.xml`
+- Create: `app/src/main/res/values-de/strings.xml`
+- Create: `app/src/main/res/values/colors.xml`
+- Create: `app/src/main/res/values/themes.xml`
+
+- [ ] **Step 1: Create `app/src/main/AndroidManifest.xml`**
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 2: Create backup-rules placeholder XML files**
+
+Create `app/src/main/res/xml/backup_rules.xml`:
+
+```xml
+
+
+
+
+```
+
+Create `app/src/main/res/xml/data_extraction_rules.xml`:
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 3: Create `app/src/main/res/values/strings.xml` (English master)**
+
+```xml
+
+ Calendula
+ A modern calendar.
+
+
+ Loading…
+ Retry
+ Something went wrong.
+ Calendar access is required.
+ Grant access
+ No calendars configured.
+ Open system calendar settings
+ Could not read the calendar.
+
+```
+
+- [ ] **Step 4: Create `app/src/main/res/values-de/strings.xml`**
+
+```xml
+
+ Calendula
+ Ein moderner Kalender.
+
+ Lädt…
+ Erneut versuchen
+ Etwas ist schiefgelaufen.
+ Zugriff auf den Kalender wird benötigt.
+ Zugriff erlauben
+ Keine Kalender eingerichtet.
+ System-Kalender-Einstellungen öffnen
+ Kalender konnte nicht gelesen werden.
+
+```
+
+- [ ] **Step 5: Create `app/src/main/res/values/colors.xml`**
+
+```xml
+
+
+ #FF5C6B7A
+
+ #FF5C6B7A
+
+```
+
+- [ ] **Step 6: Create `app/src/main/res/values/themes.xml`** (minimal stub — Compose handles real theming)
+
+```xml
+
+
+
+```
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add app/src/main/AndroidManifest.xml app/src/main/res/
+git commit -m "feat: add android manifest, strings (DE+EN), colors, base theme"
+```
+
+---
+
+## Task 7: Adaptive Launcher Icon — Static "1" on Slate Squircle
+
+**Files:**
+- Create: `app/src/main/res/drawable/ic_launcher_background.xml`
+- Create: `app/src/main/res/drawable/ic_launcher_foreground.xml`
+- Create: `app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml`
+- Create: `app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml`
+
+The adaptive icon is 108dp × 108dp, with the inner 72dp safe zone visible (mask applied by launcher). Foreground is centered on a 432dp viewport when designing in vector terms (factor 4× over 108dp).
+
+- [ ] **Step 1: Create `app/src/main/res/drawable/ic_launcher_background.xml`**
+
+A solid slate color background:
+
+```xml
+
+
+
+
+```
+
+- [ ] **Step 2: Create `app/src/main/res/drawable/ic_launcher_foreground.xml`**
+
+A bold "1" centered in the 108dp × 108dp viewport, sized to stay inside the 66dp inner safe zone (launcher masks may crop further). The "1" is drawn as a single-color path with a slight serif foot.
+
+```xml
+
+
+
+
+
+```
+
+- [ ] **Step 3: Create `app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml`**
+
+```xml
+
+
+
+
+
+
+```
+
+- [ ] **Step 4: Create `app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml`**
+
+```xml
+
+
+
+
+
+
+```
+
+- [ ] **Step 5: Verify build still works**
+
+```bash
+./gradlew assembleDebug
+```
+
+Expected: BUILD SUCCESSFUL. The APK is in `app/build/outputs/apk/debug/`.
+
+> Note: The "1" path above is a simple approximation. The icon will be visually refined during the later UI-design iteration; this is the "ships" version that establishes shape, color, and meaning correctly.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add app/src/main/res/drawable/ic_launcher_background.xml \
+ app/src/main/res/drawable/ic_launcher_foreground.xml \
+ app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml \
+ app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+git commit -m "feat: adaptive launcher icon — '1' on slate squircle (kalendae)"
+```
+
+---
+
+## Task 8: Compose Theme (Color, Theme, Typography) + Unit Test
+
+**Files:**
+- Create: `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Color.kt`
+- Create: `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt`
+- Create: `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Type.kt`
+- Create: `app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/ColorSchemeTest.kt`
+
+- [ ] **Step 1: Write the failing color scheme test**
+
+Create `app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/ColorSchemeTest.kt`:
+
+```kotlin
+package de.jeanlucmakiola.calendula.ui.theme
+
+import androidx.compose.ui.graphics.Color
+import com.google.common.truth.Truth.assertThat
+import org.junit.jupiter.api.Test
+
+class ColorSchemeTest {
+
+ @Test
+ fun `seed color matches design spec slate`() {
+ // The seed color must remain stable - the design is anchored to it.
+ // Change this only if the spec is updated.
+ assertThat(CalendulaSeed).isEqualTo(Color(0xFF5C6B7A))
+ }
+
+ @Test
+ fun `light fallback scheme uses seed as primary derivation source`() {
+ val scheme = CalendulaLightFallback
+ // Primary should be a recognizable derivative of the seed (not neutral gray)
+ assertThat(scheme.primary).isNotEqualTo(Color.Black)
+ assertThat(scheme.primary).isNotEqualTo(Color.White)
+ }
+
+ @Test
+ fun `dark fallback scheme differs from light`() {
+ assertThat(CalendulaDarkFallback.background).isNotEqualTo(CalendulaLightFallback.background)
+ }
+}
+```
+
+- [ ] **Step 2: Run the failing test**
+
+```bash
+./gradlew :app:testDebugUnitTest --tests "de.jeanlucmakiola.calendula.ui.theme.ColorSchemeTest"
+```
+
+Expected: FAIL with `Unresolved reference: CalendulaSeed` (and similar). Good — the test drives the API.
+
+- [ ] **Step 3: Create `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Color.kt`**
+
+```kotlin
+package de.jeanlucmakiola.calendula.ui.theme
+
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.ui.graphics.Color
+
+/**
+ * Seed color anchoring the entire palette. See spec section 12.
+ * Desaturated slate blue-gray; distinct from HouseHoldKeaper's sage.
+ */
+val CalendulaSeed: Color = Color(0xFF5C6B7A)
+
+/**
+ * Fallback light scheme used on devices that don't support dynamic color
+ * (API < 31) or when the user disables it. The real palette is derived by
+ * Material's tonal-palette generator from CalendulaSeed at runtime; these
+ * constants are a hand-picked subset that approximates that result.
+ */
+val CalendulaLightFallback = lightColorScheme(
+ primary = Color(0xFF3B5364),
+ onPrimary = Color(0xFFFFFFFF),
+ primaryContainer = Color(0xFFCBE6FA),
+ onPrimaryContainer = Color(0xFF001E2E),
+ secondary = Color(0xFF526070),
+ onSecondary = Color(0xFFFFFFFF),
+ background = Color(0xFFFBFCFE),
+ onBackground = Color(0xFF191C1F),
+ surface = Color(0xFFFBFCFE),
+ onSurface = Color(0xFF191C1F),
+)
+
+val CalendulaDarkFallback = darkColorScheme(
+ primary = Color(0xFFA3CBE2),
+ onPrimary = Color(0xFF003348),
+ primaryContainer = Color(0xFF21495F),
+ onPrimaryContainer = Color(0xFFCBE6FA),
+ secondary = Color(0xFFB9C8DA),
+ onSecondary = Color(0xFF243240),
+ background = Color(0xFF101316),
+ onBackground = Color(0xFFE1E3E6),
+ surface = Color(0xFF101316),
+ onSurface = Color(0xFFE1E3E6),
+)
+```
+
+- [ ] **Step 4: Run the test to verify it passes**
+
+```bash
+./gradlew :app:testDebugUnitTest --tests "de.jeanlucmakiola.calendula.ui.theme.ColorSchemeTest"
+```
+
+Expected: 3 tests PASS.
+
+- [ ] **Step 5: Create `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Type.kt`**
+
+For V1 we use Compose Material 3 defaults. Refinement (custom font, expressive type scale) happens later in the UI-design iteration.
+
+```kotlin
+package de.jeanlucmakiola.calendula.ui.theme
+
+import androidx.compose.material3.Typography
+
+/**
+ * Default Material 3 Expressive typography. Custom font + tuned scale will
+ * land in a later UI-design iteration; the defaults are intentional for V1
+ * scaffolding to keep the foundation lean.
+ */
+val CalendulaTypography = Typography()
+```
+
+- [ ] **Step 6: Create `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt`**
+
+```kotlin
+package de.jeanlucmakiola.calendula.ui.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+/**
+ * App theme. Honors:
+ * - System light/dark.
+ * - Dynamic Color on API 31+, else falls back to the hand-tuned scheme
+ * derived from [CalendulaSeed].
+ *
+ * The Settings screen (later) can override useDynamicColor and themePreference,
+ * but the V1 foundation just follows the system.
+ */
+@Composable
+fun CalendulaTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit,
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val ctx = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(ctx) else dynamicLightColorScheme(ctx)
+ }
+ darkTheme -> CalendulaDarkFallback
+ else -> CalendulaLightFallback
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = CalendulaTypography,
+ content = content,
+ )
+}
+```
+
+- [ ] **Step 7: Verify everything builds**
+
+```bash
+./gradlew assembleDebug
+```
+
+Expected: BUILD SUCCESSFUL.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/ \
+ app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/
+git commit -m "feat: M3 Expressive theme with dynamic color + fallback scheme from slate seed"
+```
+
+---
+
+## Task 9: Application Class (Hilt entry point) + MainActivity
+
+**Files:**
+- Create: `app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt`
+- Create: `app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt`
+
+- [ ] **Step 1: Create `app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt`**
+
+```kotlin
+package de.jeanlucmakiola.calendula
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+/**
+ * Application entry point. Registered as android:name=".CalendulaApp"
+ * in AndroidManifest.xml. Hilt initializes its component graph here.
+ */
+@HiltAndroidApp
+class CalendulaApp : Application()
+```
+
+- [ ] **Step 2: Create `app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt`**
+
+```kotlin
+package de.jeanlucmakiola.calendula
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import dagger.hilt.android.AndroidEntryPoint
+import de.jeanlucmakiola.calendula.ui.theme.CalendulaTheme
+
+@AndroidEntryPoint
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ CalendulaTheme {
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ PlaceholderScreen(modifier = Modifier.padding(innerPadding))
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun PlaceholderScreen(modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = stringResource(R.string.app_name),
+ style = MaterialTheme.typography.displayMedium,
+ )
+ Text(
+ text = stringResource(R.string.app_tagline),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PlaceholderPreview() {
+ CalendulaTheme { PlaceholderScreen() }
+}
+```
+
+- [ ] **Step 3: Build & install on a device or emulator (manual sanity check)**
+
+```bash
+./gradlew installDebug
+```
+
+Expected: APK installs as "Calendula" with the slate "1" icon. Launching it shows "Calendula" + "A modern calendar." centered on themed background.
+
+> If you don't have a device handy, just `./gradlew assembleDebug` and move on; the UI test in Task 10 covers behavior.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt \
+ app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt
+git commit -m "feat: scaffold CalendulaApp + MainActivity with themed placeholder"
+```
+
+---
+
+## Task 10: Smoke UI Test
+
+**Files:**
+- Create: `app/src/androidTest/java/de/jeanlucmakiola/calendula/MainActivitySmokeTest.kt`
+
+- [ ] **Step 1: Write the failing UI test**
+
+```kotlin
+package de.jeanlucmakiola.calendula
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class MainActivitySmokeTest {
+
+ @get:Rule
+ val composeTestRule = createAndroidComposeRule()
+
+ @Test
+ fun appName_isDisplayed_onLaunch() {
+ composeTestRule.onNodeWithText("Calendula").assertIsDisplayed()
+ }
+
+ @Test
+ fun tagline_isDisplayed_onLaunch() {
+ composeTestRule.onNodeWithText("A modern calendar.").assertIsDisplayed()
+ }
+}
+```
+
+- [ ] **Step 2: Verify the test runs (requires emulator/device)**
+
+```bash
+./gradlew :app:connectedDebugAndroidTest
+```
+
+Expected: 2 tests PASS. If no emulator is running, this step is informational only — the CI emulator runs it instead.
+
+> If running locally without an emulator: skip `connectedDebugAndroidTest` for now; CI will catch failures.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add app/src/androidTest/java/de/jeanlucmakiola/calendula/MainActivitySmokeTest.kt
+git commit -m "test: add UI smoke test for MainActivity placeholder"
+```
+
+---
+
+## Task 11: CI Workflow (`.gitea/workflows/ci.yaml`)
+
+**Files:**
+- Create: `.gitea/workflows/ci.yaml`
+
+- [ ] **Step 1: Create `.gitea/workflows/ci.yaml`**
+
+```yaml
+name: CI
+
+on:
+ push:
+ branches:
+ - '**'
+ tags-ignore:
+ - '**'
+ pull_request:
+
+jobs:
+ ci:
+ runs-on: docker
+ env:
+ ANDROID_HOME: /opt/android-sdk
+ ANDROID_SDK_ROOT: /opt/android-sdk
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'zulu'
+ java-version: '17'
+
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v3
+
+ - name: Install Android SDK packages
+ run: |
+ sdkmanager --licenses >/dev/null <<'EOF'
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ EOF
+ sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0"
+
+ - name: Install jq
+ run: |
+ set -e
+ SUDO=""
+ if command -v sudo >/dev/null 2>&1; then
+ SUDO="sudo"
+ fi
+ if command -v apt-get >/dev/null 2>&1; then
+ $SUDO apt-get update
+ $SUDO apt-get install -y jq
+ elif command -v apk >/dev/null 2>&1; then
+ $SUDO apk add --no-cache jq
+ fi
+
+ - name: Setup Gradle cache
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle/libs.versions.toml') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x ./gradlew
+
+ - name: Lint
+ run: ./gradlew lint --no-daemon
+
+ - name: Unit tests
+ run: ./gradlew test --no-daemon
+
+ - name: Assemble debug APK
+ run: ./gradlew assembleDebug --no-daemon
+
+ - name: Trivy filesystem scan
+ run: |
+ set -e
+ SUDO=""
+ if command -v sudo >/dev/null 2>&1; then
+ SUDO="sudo"
+ fi
+ if command -v apt-get >/dev/null 2>&1; then
+ $SUDO apt-get update
+ $SUDO apt-get install -y wget apt-transport-https gnupg lsb-release
+ wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | $SUDO tee /usr/share/keyrings/trivy.gpg > /dev/null
+ echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | $SUDO tee /etc/apt/sources.list.d/trivy.list
+ $SUDO apt-get update
+ $SUDO apt-get install -y trivy
+ fi
+ trivy filesystem --severity HIGH,CRITICAL --exit-code 0 .
+ continue-on-error: true
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add .gitea/workflows/ci.yaml
+git commit -m "ci: add gitea CI workflow (lint, test, assemble, trivy)"
+```
+
+---
+
+## Task 12: Release Workflow (`.gitea/workflows/release.yaml`)
+
+**Files:**
+- Create: `.gitea/workflows/release.yaml`
+
+This adapts the HouseHoldKeaper release workflow for Gradle/Android. Keystore secrets (`KEYSTORE_BASE64`, `KEY_PASSWORD`, `KEY_ALIAS`, `HETZNER_HOST`, `HETZNER_USER`, `HETZNER_PASS`) must be configured in Gitea repo settings before the first tag is pushed.
+
+- [ ] **Step 1: Create `.gitea/workflows/release.yaml`**
+
+```yaml
+name: Build and Release to F-Droid
+
+on:
+ push:
+ tags:
+ - '*'
+ workflow_dispatch:
+
+jobs:
+ ci:
+ runs-on: docker
+ env:
+ ANDROID_HOME: /opt/android-sdk
+ ANDROID_SDK_ROOT: /opt/android-sdk
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'zulu'
+ java-version: '17'
+
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v3
+
+ - name: Install Android SDK packages
+ run: |
+ sdkmanager --licenses >/dev/null <<'EOF'
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ EOF
+ sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0"
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x ./gradlew
+
+ - name: Lint + tests + debug build (sanity)
+ run: ./gradlew lint test assembleDebug --no-daemon
+
+ build-and-deploy:
+ needs: ci
+ runs-on: docker
+ env:
+ ANDROID_HOME: /opt/android-sdk
+ ANDROID_SDK_ROOT: /opt/android-sdk
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'zulu'
+ java-version: '17'
+
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v3
+
+ - name: Install Android SDK packages
+ run: |
+ sdkmanager --licenses >/dev/null <<'EOF'
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ EOF
+ sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0"
+
+ - name: Install jq
+ run: |
+ set -e
+ SUDO=""
+ if command -v sudo >/dev/null 2>&1; then SUDO="sudo"; fi
+ if command -v apt-get >/dev/null 2>&1; then
+ $SUDO apt-get update
+ $SUDO apt-get install -y jq
+ elif command -v apk >/dev/null 2>&1; then
+ $SUDO apk add --no-cache jq
+ fi
+
+ - name: Set version from git tag
+ run: |
+ set -e
+ RAW_TAG="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}"
+ VERSION="${RAW_TAG#v}"
+ MAJOR=$(echo "$VERSION" | cut -d. -f1)
+ MINOR=$(echo "$VERSION" | cut -d. -f2)
+ PATCH=$(echo "$VERSION" | cut -d. -f3)
+ MAJOR=${MAJOR:-0}; MINOR=${MINOR:-0}; PATCH=${PATCH:-0}
+ VERSION_CODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
+ echo "Version: $VERSION, VersionCode: $VERSION_CODE"
+ sed -i "s/versionName = \".*\"/versionName = \"$VERSION\"/" app/build.gradle.kts
+ sed -i "s/versionCode = .*/versionCode = $VERSION_CODE/" app/build.gradle.kts
+ grep -E 'versionName|versionCode' app/build.gradle.kts
+
+ - name: Setup Android Keystore
+ env:
+ KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
+ KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
+ KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
+ run: |
+ mkdir -p app
+ echo "$KEYSTORE_BASE64" | base64 --decode > app/upload-keystore.jks
+ cat > key.properties </dev/null 2>&1; then SUDO="sudo"; fi
+ $SUDO apt-get update
+ $SUDO apt-get install -y sshpass python3-pip
+ pip3 install --break-system-packages --upgrade fdroidserver
+
+ - name: Initialize or fetch F-Droid Repository
+ env:
+ HOST: ${{ secrets.HETZNER_HOST }}
+ USER: ${{ secrets.HETZNER_USER }}
+ PASS: ${{ secrets.HETZNER_PASS }}
+ run: |
+ mkdir -p fdroid
+ sshpass -p "$PASS" sftp -o StrictHostKeyChecking=no "$USER@$HOST" <<'SFTP'
+ -mkdir dev
+ -mkdir dev/fdroid
+ -mkdir dev/fdroid/repo
+ SFTP
+ sshpass -p "$PASS" scp -o StrictHostKeyChecking=no -r "$USER@$HOST:dev/fdroid/." fdroid/ || (cd fdroid && fdroid init)
+
+ - name: Ensure F-Droid repo signing key and icon
+ run: |
+ cd fdroid
+ mkdir -p repo/icons
+ if [ ! -f repo/icons/icon.png ]; then
+ # Fallback: copy a placeholder; until a PNG icon ships,
+ # F-Droid uses a generic icon. Real icon comes when we have a PNG export.
+ true
+ fi
+ if [ ! -f keystore.p12 ]; then
+ fdroid update --create-key
+ fi
+
+ - name: Copy new APK to repo
+ run: |
+ set -e
+ mkdir -p fdroid/repo
+ REF_NAME="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}"
+ SAFE_REF_NAME="$(echo "$REF_NAME" | tr '/ ' '__' | tr -cd '[:alnum:]_.-')"
+ if [ -z "$SAFE_REF_NAME" ]; then
+ SAFE_REF_NAME="${GITHUB_SHA:-manual}"
+ fi
+ cp app/build/outputs/apk/release/app-release.apk "fdroid/repo/calendula_${SAFE_REF_NAME}.apk"
+
+ - name: Copy metadata to F-Droid repo
+ run: |
+ mkdir -p fdroid/metadata
+ cp -r fdroid-metadata/* fdroid/metadata/
+
+ - name: Generate F-Droid Index
+ run: |
+ cd fdroid
+ fdroid update -c
+
+ - name: Upload Repo to Hetzner
+ env:
+ HOST: ${{ secrets.HETZNER_HOST }}
+ USER: ${{ secrets.HETZNER_USER }}
+ PASS: ${{ secrets.HETZNER_PASS }}
+ run: |
+ set -euo pipefail
+ SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=20"
+ sshpass -p "$PASS" sftp $SSH_OPTS "$USER@$HOST" <<'SFTP'
+ -mkdir dev
+ -mkdir dev/fdroid
+ -mkdir dev/fdroid/repo
+ SFTP
+ sshpass -p "$PASS" scp $SSH_OPTS -r fdroid/. "$USER@$HOST:dev/fdroid/"
+```
+
+> Note: The release workflow assumes `app/build.gradle.kts` will pick up signing config from a `key.properties` file at the project root. We add that wiring in Task 13.
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add .gitea/workflows/release.yaml
+git commit -m "ci: add gitea release workflow with F-Droid pipeline"
+```
+
+---
+
+## Task 13: Wire Signing Config into App Gradle
+
+**Files:**
+- Modify: `app/build.gradle.kts`
+
+The release workflow drops a `key.properties` at project root and a `upload-keystore.jks` in `app/`. Make Gradle read these.
+
+- [ ] **Step 1: Replace the contents of `app/build.gradle.kts`**
+
+```kotlin
+import java.util.Properties
+import java.io.FileInputStream
+
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.hilt)
+}
+
+val keystorePropertiesFile = rootProject.file("key.properties")
+val keystoreProperties = Properties().apply {
+ if (keystorePropertiesFile.exists()) {
+ load(FileInputStream(keystorePropertiesFile))
+ }
+}
+
+android {
+ namespace = "de.jeanlucmakiola.calendula"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "de.jeanlucmakiola.calendula"
+ minSdk = 29
+ targetSdk = 36
+ versionCode = 1
+ versionName = "0.1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables { useSupportLibrary = true }
+ }
+
+ signingConfigs {
+ if (keystorePropertiesFile.exists()) {
+ create("release") {
+ keyAlias = keystoreProperties["keyAlias"] as String
+ keyPassword = keystoreProperties["keyPassword"] as String
+ storeFile = file(keystoreProperties["storeFile"] as String)
+ storePassword = keystoreProperties["storePassword"] as String
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ if (keystorePropertiesFile.exists()) {
+ signingConfig = signingConfigs.getByName("release")
+ }
+ }
+ debug {
+ applicationIdSuffix = ".debug"
+ isMinifyEnabled = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+
+ testOptions {
+ unitTests.all { test ->
+ test.useJUnitPlatform()
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.compiler)
+
+ implementation(libs.androidx.datastore.preferences)
+
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ testImplementation(libs.junit.jupiter.api)
+ testRuntimeOnly(libs.junit.jupiter.engine)
+ testRuntimeOnly(libs.junit.platform.launcher)
+ testImplementation(libs.truth)
+
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+}
+```
+
+- [ ] **Step 2: Verify debug build still works (without keystore)**
+
+```bash
+./gradlew assembleDebug
+```
+
+Expected: BUILD SUCCESSFUL. The release block silently skips signing config when no `key.properties` is present.
+
+- [ ] **Step 3: Verify release build skips signing locally**
+
+```bash
+./gradlew assembleRelease
+```
+
+Expected: BUILD SUCCESSFUL with a warning that the release APK is unsigned (or signed with debug key) — this is fine; only CI signs releases.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add app/build.gradle.kts
+git commit -m "build: wire signing config from key.properties when present"
+```
+
+---
+
+## Task 14: Final Verification & CHANGELOG Update
+
+- [ ] **Step 1: Run the full local verification**
+
+```bash
+./gradlew lint test assembleDebug
+```
+
+Expected: All three steps complete with BUILD SUCCESSFUL.
+
+- [ ] **Step 2: Update `CHANGELOG.md`**
+
+Replace the existing `[Unreleased]` block with explicit shipped items for v0.1.0 and a new (empty) `[Unreleased]` placeholder. Final file content:
+
+```markdown
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [0.1.0] — 2026-06-08
+
+### Added
+- Initial project scaffold (Gradle Kotlin DSL, Version Catalog, Hilt, DataStore)
+- Material 3 Expressive theme with Dynamic Color (API 31+) and slate-derived fallback
+- Adaptive launcher icon — stylized "1" on slate squircle (references *kalendae*)
+- German + English localization infrastructure
+- Permission declaration for `READ_CALENDAR` (no UI flow yet — that's Plan 02)
+- Gitea CI workflow: lint, unit tests, debug build, Trivy scan
+- Gitea release workflow: signed release APK + F-Droid metadata sync to Hetzner
+- F-Droid metadata stubs (DE + EN short/full descriptions)
+- `.planning/` project-tracking documents
+```
+
+- [ ] **Step 3: Update `.planning/STATE.md`**
+
+```markdown
+# Calendula — Current State
+
+*Last updated: 2026-06-08*
+
+## Status
+
+**Milestone:** v0.1 — Foundation & CI
+**Phase:** Plan 01 complete; ready to start Plan 02
+
+## Progress
+
+- [x] Design spec written and committed (`docs/superpowers/specs/2026-06-08-calendar-app-design.md`)
+- [x] V1 design decisions resolved (App name "Calendula", icon, seed color)
+- [x] Plan 01 written and executed (`docs/superpowers/plans/2026-06-08-01-foundation.md`)
+- [x] Foundation lands: theme, icon, i18n, Hilt, DataStore, CI green
+- [ ] Plan 02 written (Data Layer & Permission Flow)
+
+## Next
+
+1. Write Plan 02: Data Layer & Permission Flow
+2. Execute Plan 02
+3. Iterate on UI design (mockups) before screens are built
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add CHANGELOG.md .planning/STATE.md
+git commit -m "docs: record v0.1.0 foundation in CHANGELOG, update STATE"
+```
+
+- [ ] **Step 5: Tag v0.1.0 — Optional, only after CI passes on Gitea**
+
+This step happens only once the repo is pushed to Gitea, CI is configured, and CI runs green on the foundation commit. Skip locally.
+
+```bash
+# After CI is green on Gitea:
+git tag -a v0.1.0 -m "v0.1.0 — foundation"
+git push origin v0.1.0
+```
+
+The release workflow will then build, sign, and publish to F-Droid.
+
+---
+
+## Verification Checklist (post-execution)
+
+After all 14 tasks are completed, verify:
+
+- [ ] `./gradlew lint` exits 0
+- [ ] `./gradlew test` exits 0; all unit tests pass (ColorSchemeTest x3)
+- [ ] `./gradlew assembleDebug` produces a working APK at `app/build/outputs/apk/debug/app-debug.apk`
+- [ ] APK installs and shows "Calendula" + "A modern calendar." in the system language (DE if locale=de_DE, else EN)
+- [ ] Launcher icon shows the "1" on slate squircle
+- [ ] Theme respects system light/dark
+- [ ] On API 31+ with a colored wallpaper, theme picks up wallpaper colors (Dynamic Color)
+- [ ] CI workflow file is at `.gitea/workflows/ci.yaml`
+- [ ] Release workflow file is at `.gitea/workflows/release.yaml`
+- [ ] F-Droid metadata is in `fdroid-metadata/de.jeanlucmakiola.calendula/`
+- [ ] `.planning/STATE.md` reflects "Plan 01 complete"
+
+When pushing to Gitea for the first time:
+
+- [ ] Configure repo secrets: `KEYSTORE_BASE64`, `KEY_PASSWORD`, `KEY_ALIAS`, `HETZNER_HOST`, `HETZNER_USER`, `HETZNER_PASS`
+- [ ] First CI run is green on `main`
+- [ ] Tag `v0.1.0` triggers the release workflow successfully
+- [ ] F-Droid repo at `https:///dev/fdroid/` shows Calendula
+
+---
+
+## What Plan 01 Does NOT Do (deferred to subsequent plans)
+
+- No `CalendarRepository`, no `ContentResolver` queries → Plan 02
+- No permission UI flow → Plan 02
+- No calendar event reading → Plan 02
+- No Month / Week / Day views → Plans 03 / 04 / 05
+- No Event-Detail-Sheet → Plan 06
+- No Filter / Settings → Plan 07
+- The "1" icon path is geometrically simple; visual refinement happens during UI-design iteration before V1 release
+- Custom typography / fonts → UI-design iteration
+
+The foundation deliberately ships small. Every subsequent plan layers one focused feature on top of a known-good base.