diff --git a/README.md b/README.md
index 585e3c6..03e7ac4 100644
--- a/README.md
+++ b/README.md
@@ -85,14 +85,19 @@ have retry states. The dashboard refreshes on window focus and every minute whil
visible, using the server’s date and saved account timezone. Select a calendar date and choose **Edit selected check-in** to correct counts,
checkboxes, or task occurrences using that date’s saved requirements. A date picker
also reaches history outside the displayed year. Future dates and days off are read-only.
-The personal progress overview below the habit list supports saved combined calendars.
+Saved combined calendars appear alongside individual habits in the same list,
+with a colored title and a **Combined chart** subtitle.
Choose **Create overview**, name the group, and select 1–100 habits, including
-archived habits for historical review. **Saved overview** switches between groups;
-**Edit overview** renames a group or changes its membership. Membership changes
+archived habits for historical review. Choose a palette or custom hex color for
+the title and graph; colors adapt to light and dark themes. All saved overviews
+are visible without a selector. The heading’s edit control changes the name,
+color, or membership. Membership changes
recalculate the entire chart using each habit’s historical requirements.
The calendar starts with three months and supports longer and custom ranges.
-**View day details** shows completed-versus-due totals, each habit’s actual
-progress, and routine tasks, with access to the existing check-in editor.
+**View day details** shows a completion ring, count and task progress bars,
+manual check-in symbols, and task completion indicators alongside exact values.
+Days off and future dates use neutral states. Each due habit opens the existing
+check-in editor.
Corrections refresh the combined calendar and dashboard. Days off are excluded
from totals; partial progress does not count as a completed habit. **Delete
overview** requires confirmation and keeps all habits and check-ins.
diff --git a/src/components/OverviewDayGraphic.tsx b/src/components/OverviewDayGraphic.tsx
new file mode 100644
index 0000000..e0e13bf
--- /dev/null
+++ b/src/components/OverviewDayGraphic.tsx
@@ -0,0 +1,47 @@
+import type { CSSProperties } from "react";
+import { Check, Circle, Clock3, Minus } from "lucide-react";
+import type { CalendarResponse } from "../shared/calendar";
+import { habitProgressColor } from "../lib/habit-palette";
+import { useTheme } from "./ThemeProvider";
+import { Button } from "./design-system/primitives";
+
+type Day = CalendarResponse["days"][number];
+
+/** The ring counts completed habits; individual bars retain partial progress. */
+export function OverviewDayGraphic({ day, color, busy, onEdit }: {
+ day: Day; color: string; busy: boolean; onEdit: (habitId: string) => void;
+}) {
+ const { theme } = useTheme();
+ const neutral = !day.due || day.future;
+ const ratio = neutral ? 0 : day.completed / day.due;
+ const summary = !day.due ? "Nothing scheduled" : day.future ? "Upcoming" : `${day.completed} of ${day.due} due habits completed`;
+ return
+
+
+
+
+ {!neutral && }
+
+ {neutral ? "—" : `${Math.round(ratio * 100)}%`}
+
+
{summary}
{!day.due ? "Days off don’t count against your progress." : day.future ? "Check back on this date to record your progress." : "Each completed habit counts equally. Partial progress is shown below."}
+
+
+ {day.habits.map(habit => {
+ const inactive = !habit.due || habit.future;
+ const StatusIcon = !habit.due ? Minus : habit.future ? Clock3 : habit.complete ? Check : Circle;
+ const status = !habit.due ? "Nothing scheduled" : habit.future ? "Upcoming" : habit.complete ? "Complete" : habit.value > 0 ? "In progress" : "Not started";
+ return
+ {habit.name ?? "Habit"} {status}
+ {inactive ? {!habit.due ? "Excluded from this day’s total." : "Progress will be available on this date."}
: habit.method === "manual" ?
+ {habit.complete ? "Checked in" : "Not checked in"}
:
+ {habit.value} of {habit.target} {habit.unit}
+
+
}
+ {!inactive && !!habit.tasks?.length && {habit.tasks.map(task => {task.done ? : } {task.name} · {task.done ? "Complete" : "Incomplete"} )} }
+ {!inactive && onEdit(habit.habitId)}>Edit check-in }
+ ;
+ })}
+
+
;
+}
diff --git a/src/components/ProgressOverview.test.tsx b/src/components/ProgressOverview.test.tsx
index 5da44b3..a5e3d6a 100644
--- a/src/components/ProgressOverview.test.tsx
+++ b/src/components/ProgressOverview.test.tsx
@@ -1,6 +1,6 @@
import { afterAll, afterEach, beforeAll, beforeEach, expect, test } from "bun:test";
import { Window } from "happy-dom";
-import { act } from "react";
+import { act, useState } from "react";
import type { Root } from "react-dom/client";
import { ProgressOverview } from "./ProgressOverview";
import { fixture } from "../habits/test-fixture";
@@ -51,11 +51,15 @@ afterAll(() => {
});
function button(text: string) {
- return [...container.querySelectorAll("button")].find((element) => element.textContent?.trim() === text)!;
+ return [...container.querySelectorAll("button")].find((element) => element.textContent?.trim() === text || element.getAttribute("aria-label") === text || element.getAttribute("aria-label")?.startsWith(`${text} `))!;
}
+function Dashboard() {
+ const [creating, setCreating] = useState(false);
+ return <> setCreating(true)}>Create overview setCreating(false)} date="2026-09-04" revision={0} onExpired={() => {}} onHistorySaved={() => {}} /> >;
+}
async function render() {
- await act(async () => root.render( {}} onHistorySaved={() => {}} />));
+ await act(async () => root.render( ));
}
function checkbox(label: string) {
return [...container.querySelectorAll('label')].find(element => element.textContent === label)!.querySelector('input')!;
@@ -151,3 +155,42 @@ test("range changes retain the selected window and future or unscheduled days ha
expect(container.querySelector('#overview-day')!.textContent).toContain('Upcoming');
expect([...container.querySelectorAll('#overview-day button')].some(b => b.textContent?.startsWith('Edit '))).toBe(false);
});
+
+test("all overviews render in the habit list and custom colors persist without resetting other settings", async () => {
+ const habit = await f.json('/habits', 'POST', { name: 'Read', method: 'manual' }, 201);
+ const first = await f.json('/charts', 'POST', { name: 'Health', habitIds: [habit.id], settings: { mainColor: '#112233', shadeCount: 7 } }, 201);
+ await f.json('/charts', 'POST', { name: 'Learning', habitIds: [habit.id] }, 201);
+ await render();
+ expect(container.querySelectorAll('.ds-habit-list > .ds-overview')).toHaveLength(2);
+ expect(container.querySelector('select')).toBeNull();
+ expect([...container.querySelectorAll('.ds-goal-meta')].map(el => el.textContent)).toEqual(['Combined chart · 1 habit', 'Combined chart · 1 habit']);
+ await click('Edit overview Health');
+ await click('Custom');
+ const hex = container.querySelector('input[placeholder="#426582"]')!;
+ await act(async () => {
+ Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, 'value')!.set!.call(hex, '#a35488');
+ hex.dispatchEvent(new Event('input', { bubbles: true }));
+ });
+ await click('Save overview');
+ expect((await f.json(`/charts/${first.id}`)).settings).toMatchObject({ mainColor: '#a35488', shadeCount: 7 });
+ expect(container.querySelectorAll('.ds-overview')).toHaveLength(2);
+ await click('Edit overview Health');
+ expect(container.querySelector('input[placeholder="#426582"]')!.value).toBe('#a35488');
+});
+
+test("day graphics show partial task and count progress without crediting incomplete habits", async () => {
+ const count = await f.json('/habits', 'POST', { name: 'Water', method: 'count', target: 8, unit: 'glasses' }, 201);
+ const routine = await f.json('/habits', 'POST', { name: 'Routine', method: 'tasks', tasks: [{ name: 'Stretch' }, { name: 'Walk' }] }, 201);
+ const manual = await f.json('/habits', 'POST', { name: 'Read', method: 'manual' }, 201);
+ await f.json(`/habits/${count.id}/days/2026-09-04/progress`, 'PUT', { count: 7 });
+ await f.json(`/habits/${manual.id}/days/2026-09-04/progress`, 'PUT', { done: true });
+ await f.json(`/habits/${routine.id}/days/2026-09-04/tasks/${routine.tasks[0].id}`, 'PUT', { done: true });
+ await f.json('/charts', 'POST', { name: 'Together', habitIds: [count.id, routine.id, manual.id] }, 201);
+ await render();
+ await click('View day details');
+ expect(container.querySelector('.ds-overview-ring')?.textContent).toBe('33%');
+ expect(container.querySelector('progress[aria-label="Water progress"]')?.getAttribute('value')).toBe('7');
+ expect(container.querySelector('progress[aria-label="Routine progress"]')?.getAttribute('value')).toBe('1');
+ expect(container.querySelectorAll('.ds-overview-tasks li')).toHaveLength(2);
+ expect(container.querySelector('.ds-overview-check')?.textContent).toBe('Checked in');
+});
diff --git a/src/components/ProgressOverview.tsx b/src/components/ProgressOverview.tsx
index a7b9ed4..731e5fa 100644
--- a/src/components/ProgressOverview.tsx
+++ b/src/components/ProgressOverview.tsx
@@ -1,11 +1,15 @@
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useRef, useState, type CSSProperties } from "react";
import type { HabitService } from "../habits/service";
import { habitRequest, type TodayHabit } from "../lib/dashboard";
import { toHeatmapDays, type CalendarResponse } from "../shared/calendar";
import { CalendarHeatmap } from "./design-system/CalendarHeatmap";
import { monthWindow, type MonthView } from "./design-system/calendar-model";
-import { Button, Checkbox, SectionHeading } from "./design-system/primitives";
-import { Card } from "./design-system/Card";
+import { Button, Checkbox } from "./design-system/primitives";
+import { HabitColorPicker, isHabitColor } from "./design-system/HabitColorPicker";
+import { ItemActions } from "./design-system/ItemActions";
+import { useTheme } from "./ThemeProvider";
+import { habitProgressColor } from "../lib/habit-palette";
+import { OverviewDayGraphic } from "./OverviewDayGraphic";
import { Field } from "./design-system/Field";
import { Modal } from "./design-system/Modal";
import { FormMessage } from "./design-system/WorkspacePanel";
@@ -16,79 +20,71 @@ type Chart = ReturnType;
type Day = CalendarResponse["days"][number];
const message = (error: unknown) => error instanceof Error ? error.message : "Could not load your overview. Try again.";
-export function ProgressOverview({ date, revision, disabled, onExpired, onHistorySaved }: {
+export function ProgressOverview({ date, revision, disabled, onExpired, onHistorySaved, creating = false, onCloseCreate = () => {} }: {
+ creating?: boolean; onCloseCreate?: () => void;
date: string; revision: number; disabled?: boolean; onExpired: () => void; onHistorySaved: () => void;
}) {
const [charts, setCharts] = useState([]);
- const [selected, setSelected] = useState("");
+ const { theme } = useTheme();
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [attempt, setAttempt] = useState(0);
- const [editing, setEditing] = useState(null);
+ const [editing, setEditing] = useState(null);
const [deleting, setDeleting] = useState(null);
const [busy, setBusy] = useState(false);
const [deleteError, setDeleteError] = useState("");
const saving = useRef(false);
const expired = useRef(onExpired); expired.current = onExpired;
- const heading = useRef(null);
+
useEffect(() => {
const controller = new AbortController();
setLoading(true); setError("");
habitRequest<{ charts: Chart[] }>("/charts", { signal: controller.signal })
.then(data => { if (!controller.signal.aborted) {
setCharts(data.charts);
- setSelected(current => data.charts.some(chart => chart.id === current) ? current : data.charts[0]?.id ?? "");
} })
.catch(error => { if (!controller.signal.aborted) { setError(message(error)); if (message(error).includes("session has expired")) expired.current(); } })
.finally(() => { if (!controller.signal.aborted) setLoading(false); });
return () => controller.abort();
}, [revision, date, attempt]);
- const chart = charts.find(item => item.id === selected);
async function remove() {
if (!deleting || saving.current) return;
saving.current = true; setBusy(true); setDeleteError("");
try {
await habitRequest(`/charts/${deleting.id}`, { method: "DELETE" });
setCharts(current => current.filter(item => item.id !== deleting.id));
- setSelected(charts.find(item => item.id !== deleting.id)?.id ?? "");
setDeleting(null); setAttempt(value => value + 1);
} catch (error) { setDeleteError(message(error)); if (message(error).includes("session has expired")) expired.current(); }
finally { saving.current = false; setBusy(false); }
}
- return
-
- Your progress, together. >} />
- setEditing("new")}>Create overview
-
- Group habits into a saved calendar. Each day counts completed habits out of those due; partial progress stays visible in the day details.
- {error && <>{error} setAttempt(value => value + 1)}>Retry overviews >}
- {loading && Loading overviews…
}
- {!loading && !error && !charts.length && Create your first overview for a morning routine, health goals, or the habits that matter most.
}
- {!!charts.length &&
- {id => setSelected(event.target.value)} disabled={busy}>
- {charts.map(item => {item.name} )}
- }
- {chart &&
- setEditing(chart)}>Edit overview
- { setDeleteError(""); setDeleting(chart); }}>Delete overview
-
}>
-
- }
- }
- {editing && setEditing(null)} onExpired={onExpired} onSaved={saved => {
- setCharts(current => [...current.filter(item => item.id !== saved.id), saved]); setSelected(saved.id); setEditing(null); setAttempt(value => value + 1);
+ return <>
+ {error && {error} setAttempt(value => value + 1)}>Retry overviews
}
+ {loading && !charts.length && Loading overviews…
}
+ {charts.map(chart => )}
+ {(creating || editing) && { setEditing(null); onCloseCreate(); }} onExpired={onExpired} onSaved={saved => {
+ setCharts(current => current.some(item => item.id === saved.id) ? current.map(item => item.id === saved.id ? saved : item) : [...current, saved]); setEditing(null); onCloseCreate(); setAttempt(value => value + 1);
}} />}
- {deleting && heading.current} onClose={() => setDeleting(null)}>
+ {deleting && document.getElementById("create-overview")} onClose={() => setDeleting(null)}>
{deleteError}
setDeleting(null)}>Keep overview void remove()}>{busy ? "Deleting…" : "Delete overview"}
}
- ;
+ >;
}
function OverviewForm({ chart, date, onClose, onSaved, onExpired }: {
chart?: Chart; date: string; onClose: () => void; onSaved: (chart: Chart) => void; onExpired: () => void;
}) {
const [name, setName] = useState(chart?.name ?? "");
+ const [color, setColor] = useState(chart?.settings.mainColor ?? "#196127");
const [ids, setIds] = useState(chart?.habitIds ?? []);
const [habits, setHabits] = useState([]);
const [loading, setLoading] = useState(true);
@@ -109,10 +105,11 @@ function OverviewForm({ chart, date, onClose, onSaved, onExpired }: {
async function save() {
if (saving.current || loading || loadError) return;
if (!name.trim() || !ids.length || ids.length > 100) { setError("Enter a name and choose between 1 and 100 habits."); return; }
+ if (!isHabitColor(color)) { setError("Choose a six-digit hex color."); return; }
saving.current = true; setBusy(true); setError("");
try {
const saved = await habitRequest(chart ? `/charts/${chart.id}` : "/charts", {
- method: chart ? "PATCH" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name.trim(), habitIds: ids }),
+ method: chart ? "PATCH" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name.trim(), habitIds: ids, settings: { ...chart?.settings, mainColor: color } }),
});
onSaved(saved);
} catch (error) { setError(message(error)); if (message(error).includes("session has expired")) expired.current(); }
@@ -121,6 +118,7 @@ function OverviewForm({ chart, date, onClose, onSaved, onExpired }: {
return
;
}
@@ -157,37 +155,33 @@ function OverviewCalendar({ chart, date, revision, disabled, onExpired, onHistor
return <>
{error && <>{error} setAttempt(value => value + 1)}>Retry calendar >}
{!calendar ? !error && Loading combined calendar…
: setSelection({ range, months })} historyLabel="Completed habits out of those due" selectedDayAction={selected => setInspecting(selected)}>View day details } />}
- {inspecting && setInspecting(null)} onExpired={onExpired} onSaved={() => { setAttempt(value => value + 1); onHistorySaved(); }} />}
+ {inspecting && setInspecting(null)} onExpired={onExpired} onSaved={() => { setAttempt(value => value + 1); onHistorySaved(); }} />}
>;
}
-function OverviewDay({ chartId, date, revision, onClose, onExpired, onSaved }: {
- chartId: string; date: string; revision: string; onClose: () => void; onExpired: () => void; onSaved: () => void;
+function OverviewDay({ color, chartName, chartId, date, revision, onClose, onExpired, onSaved }: {
+ color: string; chartName: string; chartId: string; date: string; revision: string; onClose: () => void; onExpired: () => void; onSaved: () => void;
}) {
const [day, setDay] = useState(null);
+ const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [attempt, setAttempt] = useState(0);
const [editing, setEditing] = useState(null);
const [busy, setBusy] = useState(false);
const expired = useRef(onExpired); expired.current = onExpired;
useEffect(() => {
- const controller = new AbortController(); setError(""); setDay(null);
+ const controller = new AbortController(); setError(""); setLoading(true);
habitRequest(`/charts/${chartId}/days/${date}`, { signal: controller.signal })
.then(data => { if (!controller.signal.aborted) setDay(data); })
- .catch(error => { if (!controller.signal.aborted) { setError(message(error)); if (message(error).includes("session has expired")) expired.current(); } });
+ .catch(error => { if (!controller.signal.aborted) { setError(message(error)); if (message(error).includes("session has expired")) expired.current(); } })
+ .finally(() => { if (!controller.signal.aborted) setLoading(false); });
return () => controller.abort();
}, [chartId, date, revision, attempt]);
- return
+ return
{error && <>{error} setAttempt(value => value + 1)}>Retry day details >}
{!day ? !error && Loading day details…
: <>
- {!day.due ? "Nothing scheduled" : day.future ? "Upcoming" : `${day.completed} of ${day.due} due habits completed`}
- {day.habits.map(habit =>
- {habit.name ?? "Habit"} {!habit.due ? "Nothing scheduled" : habit.future ? "Upcoming" : `${habit.value} of ${habit.target} ${habit.unit} · ${habit.complete ? "Complete" : "Incomplete"}`}
- {habit.due && !habit.future &&
setEditing(habit.habitId)}>Edit {habit.name ?? "habit"} check-in }
-
- {!!habit.tasks?.length && {habit.tasks.map(task => {task.name} · {task.done ? "Complete" : "Incomplete"} )} }
- )}
+
>}
- {editing && }
+ {editing && habit.habitId === editing)?.name ?? date} busy={busy} onClose={() => setEditing(null)}> }
;
}
diff --git a/src/components/design-system/HabitColorPicker.tsx b/src/components/design-system/HabitColorPicker.tsx
index e7ef245..06c00a0 100644
--- a/src/components/design-system/HabitColorPicker.tsx
+++ b/src/components/design-system/HabitColorPicker.tsx
@@ -38,7 +38,9 @@ export function isHabitColor(value: string) {
export function HabitColorPicker({
value,
onChange,
+ label = "Habit color",
}: {
+ label?: string;
value: string;
onChange: (color: string) => void;
mode?: "demo" | "create" | "edit";
@@ -50,11 +52,11 @@ export function HabitColorPicker({
const showCustom = expanded || !valid;
return (
- Habit color
+ {label}
Your color adapts to each theme. Custom colors use six-digit hex values, such as #426582.
-
+
{HABIT_COLOR_CHOICES.map((color) => (
onChange(color)}>
diff --git a/src/components/design-system/ItemActions.tsx b/src/components/design-system/ItemActions.tsx
index 6afa229..78f9031 100644
--- a/src/components/design-system/ItemActions.tsx
+++ b/src/components/design-system/ItemActions.tsx
@@ -3,7 +3,7 @@ import { Button } from "./primitives";
export function ItemActions({ name, kind, disabled, onEdit, onDelete }: {
name: string;
- kind: "habit" | "task";
+ kind: "habit" | "task" | "overview";
disabled?: boolean;
onEdit: () => void;
onDelete: () => void;
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index ed5cd05..8572d0b 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -179,6 +179,7 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
const [loading, setLoading] = useState(true);
const [habitView, setHabitView] = useState<"today" | "all">("today");
const [adding, setAdding] = useState(false);
+ const [addingOverview, setAddingOverview] = useState(false);
const [sharing, setSharing] = useState(false);
const [settings, setSettings] = useState(false);
const [archive, setArchive] = useState(false);
@@ -398,6 +399,7 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
>
Add a habit +
+ setAddingOverview(true)}>Create overview
{today.habits.length > 0 && (
<>
setSharing(true)}>Share progress
@@ -447,8 +449,7 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
}}
/>
)}
- {today.habits.length > 0 && (
- voi
setHabitView("all")}>All habits · {today.habits.length}
- {habitView === "today" && today.due === 0 && You’re all set for today. Choose All habits to view or edit your routines.
}
+ {habitView === "today" && today.habits.length > 0 && today.due === 0 && You’re all set for today. Choose All habits to view or edit your routines.
}
{today.habits.filter(habit => habitView === "all" || habit.due).map((habit) => (
voi
onExpired={expire}
/>
))}
+ setAddingOverview(false)} date={today.date} revision={revision + attempt} disabled={busy || loading || needsRefresh || pendingHabits.size > 0} onExpired={expire} onHistorySaved={refreshHistory} />
- )}
- 0} onExpired={expire} onHistorySaved={refreshHistory} />
>
)}
>
diff --git a/styles/design-system.css b/styles/design-system.css
index 9eeb246..91e537f 100644
--- a/styles/design-system.css
+++ b/styles/design-system.css
@@ -1761,3 +1761,43 @@
.ds-inline-item-fields:not(.ds-habit-form-grid) { display: grid; gap: 12px; }
.ds-inline-item-fields:not(.ds-habit-form-grid) .ds-field { margin: 0; }
.ds-inline-item-fields:not(.ds-habit-form-grid) .ds-schedule-editor { display: grid; gap: 12px; }
+
+/* Combined calendars live alongside habits; their color identifies the group. */
+.ds-overview .ds-overview-title { color: var(--habit-color); margin: 0 0 8px; }
+.ds-overview > .ds-calendar { min-width: 0; }
+.ds-overview-details { display: grid; gap: 24px; }
+.ds-overview-summary { display: flex; align-items: center; gap: 24px; padding: 24px; background: var(--ds-surface); }
+.ds-overview-summary .ds-supporting-copy { margin-top: 8px; }
+.ds-overview-ring { position: relative; flex: 0 0 112px; width: 112px; height: 112px; display: grid; place-items: center; color: var(--habit-color); }
+.ds-overview-ring svg { position: absolute; inset: 0; width: 100%; height: 100%; fill: none; stroke-width: 8; }
+.ds-overview-ring-track { stroke: var(--ds-rule); }
+.ds-overview-ring-value { stroke: currentColor; }
+.ds-overview-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
+.ds-overview-habit { display: flex; flex-direction: column; gap: 16px; padding: 20px; border: 1px solid var(--ds-rule); min-width: 0; }
+.ds-overview-habit header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 8px 16px; }
+.ds-overview-habit h3 { overflow-wrap: anywhere; }
+.ds-overview-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ds-secondary); @apply type-small; }
+.ds-overview-status svg { flex-shrink: 0; }
+.ds-overview-meter { display: grid; gap: 12px; }
+.ds-overview-meter p { color: var(--ds-secondary); }
+.ds-overview-meter strong { color: var(--habit-color); font-variant-numeric: tabular-nums; }
+.ds-overview-meter progress { appearance: none; width: 100%; height: 8px; border: 0; background: var(--ds-empty); color: var(--habit-color); accent-color: var(--habit-color); }
+.ds-overview-meter progress::-webkit-progress-bar { background: var(--ds-empty); }
+.ds-overview-meter progress::-webkit-progress-value { background: var(--habit-color); }
+.ds-overview-meter progress::-moz-progress-bar { background: var(--habit-color); }
+.ds-overview-check { display: flex; align-items: center; gap: 12px; padding-block: 8px; color: var(--ds-secondary); }
+.ds-overview-check.is-complete, .ds-overview-tasks .is-complete { color: var(--habit-color); }
+.ds-overview-tasks { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }
+.ds-overview-tasks li { display: flex; align-items: baseline; gap: 10px; overflow-wrap: anywhere; }
+.ds-overview-tasks li > span:first-child { flex-shrink: 0; align-self: flex-start; padding-top: 3px; }
+.ds-overview-habit > .ds-button { margin-top: auto; align-self: flex-start; }
+@media (max-width: 640px) {
+ .ds-overview-detail-grid { grid-template-columns: minmax(0, 1fr); }
+ .ds-overview-summary { padding: 16px; gap: 16px; flex-wrap: wrap; }
+ .ds-overview-ring { flex-basis: 88px; width: 88px; height: 88px; }
+}
+@media (forced-colors: active) {
+ .ds-overview-ring-track { stroke: GrayText; }
+ .ds-overview-ring-value { stroke: Highlight; }
+ .ds-overview-meter progress { appearance: auto; }
+}