diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8adf73..b8f2e14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,15 @@ BODY ZONES silhouette, and a compact Program header. A new Equipment workspace
lists factual references and transactionally absorbs a custom duplicate into a
canonical identity while retaining history/MAX context and recording a causal
deletion marker; immutable embedded drafts cause an explicit conflict.
+The post-review correction keeps acknowledged-generation capacity recoverable
+for legacy Android rows without ACK-ledger evidence, proves two successive
+Sleep-only generation/ACK exchanges, and never evicts an unacknowledged
+generation. Web can now causally remove only an orphaned zero-occurrence active
+draft while retaining protection for real active drafts. Mutation UUIDs use
+`crypto.getRandomValues()` on loopback HTTP origins. Sleep timeline geometry,
+Dashboard sizing, Program progress contrast, chart proportions, duration
+coverage messaging, and the right-aligned Settings/Sync action cluster are
+also refined.
## 0.1.5 — 2026-09-23
diff --git a/contracts/dashboard-layout-v1.json b/contracts/dashboard-layout-v1.json
index 61fb85a..bf0e8f3 100644
--- a/contracts/dashboard-layout-v1.json
+++ b/contracts/dashboard-layout-v1.json
@@ -4,12 +4,12 @@
"columns": 12,
"max_y": 200,
"tiles": [
- { "id": "next-session", "min_width": 4, "max_width": 8, "min_height": 3, "max_height": 7, "default": { "x": 0, "y": 0, "width": 5, "height": 4 } },
- { "id": "activity", "min_width": 5, "max_width": 12, "min_height": 3, "max_height": 7, "default": { "x": 5, "y": 0, "width": 7, "height": 4 } },
- { "id": "progression", "min_width": 6, "max_width": 12, "min_height": 4, "max_height": 9, "default": { "x": 0, "y": 4, "width": 8, "height": 5 } },
- { "id": "last-session", "min_width": 3, "max_width": 7, "min_height": 3, "max_height": 6, "default": { "x": 8, "y": 4, "width": 4, "height": 3 } },
- { "id": "max-records", "min_width": 3, "max_width": 8, "min_height": 3, "max_height": 7, "default": { "x": 8, "y": 7, "width": 4, "height": 3 } },
- { "id": "muscle-distribution", "min_width": 5, "max_width": 12, "min_height": 4, "max_height": 9, "default": { "x": 0, "y": 9, "width": 6, "height": 5 } },
- { "id": "cardio-recovery", "min_width": 5, "max_width": 12, "min_height": 4, "max_height": 8, "default": { "x": 6, "y": 10, "width": 6, "height": 4 } }
+ { "id": "next-session", "min_width": 4, "max_width": 12, "min_height": 3, "max_height": 10, "default": { "x": 0, "y": 0, "width": 5, "height": 4 } },
+ { "id": "activity", "min_width": 5, "max_width": 12, "min_height": 3, "max_height": 12, "default": { "x": 5, "y": 0, "width": 7, "height": 4 } },
+ { "id": "progression", "min_width": 6, "max_width": 12, "min_height": 4, "max_height": 12, "default": { "x": 0, "y": 4, "width": 8, "height": 5 } },
+ { "id": "last-session", "min_width": 3, "max_width": 12, "min_height": 3, "max_height": 10, "default": { "x": 8, "y": 4, "width": 4, "height": 3 } },
+ { "id": "max-records", "min_width": 3, "max_width": 12, "min_height": 3, "max_height": 10, "default": { "x": 8, "y": 7, "width": 4, "height": 3 } },
+ { "id": "muscle-distribution", "min_width": 5, "max_width": 12, "min_height": 4, "max_height": 12, "default": { "x": 0, "y": 9, "width": 6, "height": 5 } },
+ { "id": "cardio-recovery", "min_width": 5, "max_width": 12, "min_height": 4, "max_height": 12, "default": { "x": 6, "y": 10, "width": 6, "height": 4 } }
]
}
diff --git a/docs/current_state.md b/docs/current_state.md
index 72cf9f6..3a2e77a 100644
--- a/docs/current_state.md
+++ b/docs/current_state.md
@@ -315,12 +315,40 @@ exercise to individual detail. Exercise work time is derived exclusively from
Measurements first expose unit-safe relative change cards before real-unit
detail. Dashboard and Analyse reuse the same human-proportioned BODY ZONES SVG.
+The Dashboard layout keeps a 12-column canonical grid but permits every card
+to reach full width and several useful heights; per-card minima, overlap
+validation, exact save/cancel/reset semantics, and keyboard resizing remain
+authoritative. Settings and synchronization form the rightmost Header action
+cluster. Program progress uses the shared success color while retaining its
+factual completed/total text. Responsive charts preserve their viewBox aspect
+ratio, and Analyse explicitly distinguishes unavailable exercise timing from
+partial measured coverage instead of deriving time from sets or repetitions.
+The shared Sleep agenda owns one label/track/facts grid. Its five clock labels
+and every event, interval and medication marker use the same coordinates:
+18:00 at 0%, 00:00 at 25%, 06:00 at 50%, 12:00 at 75%, and 18:00 at 100%.
+
The Equipment route projects supplied and custom definitions with exercise,
history, preparation and Program reference counts. Its guarded merge accepts a
custom duplicate only, transfers relational references in one SQLite IMMEDIATE
transaction, preserves occurrence/MAX facts indirectly through occurrence
identity, and writes the existing causal deletion boundary. An immutable
execution-draft JSON reference rejects the operation rather than being edited.
+Browser mutation identities use the shared UUIDv4 helper backed by
+`crypto.getRandomValues()`; no Web mutation depends on `crypto.randomUUID()`,
+so `http://trainlog.perf` remains supported.
+
+An `execution_drafts` row marked active but containing no `exercises` or
+legacy `entries` is an orphaned projection, not a resumable workout. Web may
+delete that exact row through the existing revision-guarded causal transaction;
+the durable tombstone prevents synchronization replay from resurrecting it.
+An active draft with any occurrence remains protected from Web deletion.
+
+Generation admission archives only rows backed by an exact correlated consumed
+ACK and retains the newest two acknowledged lineage members. Legacy terminal
+Android rows that predate the durable ACK ledger are neither fabricated nor
+deleted, but no longer consume the active admission window. Captured,
+published, waiting-ACK, and current acknowledged rows continue to count until
+safe archival, so capacity recovery never prunes unacknowledged evidence.
`TRAINLOG_WEB_V1` architecture, API independence, local-network boundary,
browser shell, Dashboard, layout ownership, build/runtime separation, and
diff --git a/docs/tests.md b/docs/tests.md
index 1015eb3..ddaef35 100644
--- a/docs/tests.md
+++ b/docs/tests.md
@@ -13,6 +13,23 @@ touching real user equipment. Strict C compilation, the full Meson inventory,
Web typecheck/build/Vitest, JSON/import validation, changed-C formatting and
diff checks remain the closeout gates.
+Post-review regressions cover a validated Sleep Diary revision plus nocturnal
+BPM/RR capture through the production full-generation transport, desktop
+import, correlated ACK, exact ACK replay, a second Sleep-only revision and a
+second generation. The 24-conversation deployed-capacity regression removes
+legacy ACK-ledger rows deliberately, recovers capacity without deleting
+generation evidence, and continues through archive/replay. Native Web tests
+prove that an active zero-occurrence execution draft is deleted causally and
+that repeating its request is byte-identical, while a populated active draft
+still conflicts. The equipment client test removes `crypto.randomUUID`, uses
+deterministic `getRandomValues` bytes, and completes the protected merge HTTP
+request; native merge tests retain their transaction/rollback coverage.
+
+UI regressions assert the Sleep labels at exactly 0/25/50/75/100%, persisted
+Dashboard sizing and reset behavior, English preference persistence across a
+remount, centralized Dashboard English labels, non-stretched chart aspect
+ratios, and distinct missing/partial exercise-timing messages.
+
Sleep Diary coverage includes schema creation/migration, midnight and DST
offsets, revision conflict, logical deletion/reopen, companion replay and
rollback, Web periods/deep links, Android persistence, and vector PDF
diff --git a/tui/src/dashboard_layout.c b/tui/src/dashboard_layout.c
index 313f6aa..6a49aec 100644
--- a/tui/src/dashboard_layout.c
+++ b/tui/src/dashboard_layout.c
@@ -21,13 +21,16 @@ typedef struct TileContract {
} TileContract;
static const TileContract contracts[TRAINLOG_DASHBOARD_LAYOUT_TILE_COUNT] = {
- {"next-session", 4U, 8U, 3U, 7U},
- {"activity", 5U, 12U, 3U, 7U},
- {"progression", 6U, 12U, 4U, 9U},
- {"last-session", 3U, 7U, 3U, 6U},
- {"max-records", 3U, 8U, 3U, 7U},
- {"muscle-distribution", 5U, 12U, 4U, 9U},
- {"cardio-recovery", 5U, 12U, 4U, 8U}};
+ /* CONTRACT: every tile can reach full width and multiple useful vertical
+ * sizes. Per-tile minima preserve legibility; the shared 12-column grid
+ * and overlap validation remain the bounded layout authority. */
+ {"next-session", 4U, 12U, 3U, 10U},
+ {"activity", 5U, 12U, 3U, 12U},
+ {"progression", 6U, 12U, 4U, 12U},
+ {"last-session", 3U, 12U, 3U, 10U},
+ {"max-records", 3U, 12U, 3U, 10U},
+ {"muscle-distribution", 5U, 12U, 4U, 12U},
+ {"cardio-recovery", 5U, 12U, 4U, 12U}};
static const TrainlogDashboardTileLayout defaults[TRAINLOG_DASHBOARD_LAYOUT_TILE_COUNT] = {
{"next-session", 0U, 0U, 5U, 4U},
diff --git a/web/src/app/App.test.tsx b/web/src/app/App.test.tsx
index 2cc1e8e..4ceb28f 100644
--- a/web/src/app/App.test.tsx
+++ b/web/src/app/App.test.tsx
@@ -166,4 +166,21 @@ describe('shell Trainlog', () => {
expect(screen.getAllByRole('link').filter((link) => link.closest('nav'))).toHaveLength(6)
expect(screen.getByRole('link', { name: 'Dashboard' })).toHaveAttribute('aria-current', 'page')
})
+
+ it('persists English and applies centralized Dashboard navigation labels after reload', async () => {
+ mockHealth()
+ window.history.replaceState(null, '', '/parametres')
+ const first = render()
+ fireEvent.click(screen.getByRole('radio', { name: 'English' }))
+ fireEvent.click(screen.getByRole('link', { name: 'Dashboard' }))
+ expect(await screen.findByRole('button', { name: 'Edit layout' })).toBeInTheDocument()
+ expect(screen.getByText('Next session')).toBeInTheDocument()
+ expect(screen.queryByText('Prochaine séance')).not.toBeInTheDocument()
+ first.unmount()
+ window.history.replaceState(null, '', '/')
+ render()
+ expect(await screen.findByRole('button', { name: 'Edit layout' })).toBeInTheDocument()
+ expect(document.documentElement.lang).toBe('en')
+ window.localStorage.removeItem('trainlog.web.language.v1')
+ })
})
diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx
index 1c4212d..56f6631 100644
--- a/web/src/components/Header.tsx
+++ b/web/src/components/Header.tsx
@@ -38,9 +38,11 @@ export function Header({ activeRoute, onNavigate, onSyncCommitted }: HeaderProps
))}
- follow(event, '/parametres')}>⚙ {language === 'en' ? 'Settings' : 'Paramètres'}
-
+
)
}
diff --git a/web/src/dashboard/DashboardGrid.tsx b/web/src/dashboard/DashboardGrid.tsx
index 747b233..827d3b7 100644
--- a/web/src/dashboard/DashboardGrid.tsx
+++ b/web/src/dashboard/DashboardGrid.tsx
@@ -21,15 +21,17 @@ import {
import { ActivityTile, LastSessionTile, MuscleDistributionTile, NextSessionTile, ProgressionTile, type DashboardTileComponent } from './DashboardTiles'
import { MeasurementsTile, ProgramTile } from './DashboardTiles'
import type { AnalysisSnapshot } from '../api/analysis'
+import { useLanguagePreferences } from '../presentation/LanguagePreferences'
+import { dashboardMessages } from './dashboardMessages'
-const dashboardTiles: Readonly> = {
- 'next-session': { title: 'Prochaine séance', eyebrow: 'Planification', component: NextSessionTile },
- activity: { title: 'Activité', eyebrow: 'Régularité', component: ActivityTile },
- progression: { title: 'Progression', eyebrow: 'Évolution', component: ProgressionTile },
- 'last-session': { title: 'Dernière séance', eyebrow: 'Historique', component: LastSessionTile },
- 'max-records': { title: 'Mensurations', eyebrow: 'Évolution', component: MeasurementsTile },
- 'muscle-distribution': { title: 'Répartition musculaire', eyebrow: 'Zones', component: MuscleDistributionTile },
- 'cardio-recovery': { title: 'Programme actif', eyebrow: 'Planification', component: ProgramTile },
+const dashboardComponents: Readonly> = {
+ 'next-session': NextSessionTile,
+ activity: ActivityTile,
+ progression: ProgressionTile,
+ 'last-session': LastSessionTile,
+ 'max-records': MeasurementsTile,
+ 'muscle-distribution': MuscleDistributionTile,
+ 'cardio-recovery': ProgramTile,
}
function breakpoint(width: number): { columns: 12 | 6 | 1, rowHeight: number } {
@@ -57,6 +59,13 @@ export function DashboardGrid({
preparedItemsFailed = false,
analysis = null,
}: DashboardGridProps) {
+ const { language } = useLanguagePreferences()
+ const messages = dashboardMessages[language]
+ const dashboardTiles = Object.fromEntries(TILE_IDS.map((id) => [id, {
+ title: messages.tiles[id][0],
+ eyebrow: messages.tiles[id][1],
+ component: dashboardComponents[id],
+ }])) as Readonly>
const [layout, setLayout] = useState(() => cloneLayout(DEFAULT_DASHBOARD_LAYOUT))
const [editing, setEditing] = useState(false)
const [announcement, setAnnouncement] = useState('')
@@ -151,27 +160,27 @@ export function DashboardGrid({
const libraryLayout = toGridLayout(visibleLayout, editing && desktop, view.columns)
- if (loading) return Chargement de l’agencement…
+ if (loading) return {messages.loading}
return (
-
+
{!editing ? (
-
+
) : (
<>
-
Flèches : déplacer · Maj + flèches : redimensionner
+
{messages.help}
{!desktop &&
Revenez sur un écran large pour modifier la grille canonique.
}
-
-
-
+
+
+
>
)}
{preferenceError &&
{preferenceError}{preferenceError.includes('autre onglet') && }
}
- {pending &&
Chargement des données du Dashboard…
}
- {failed &&
Les données du Dashboard sont momentanément indisponibles.
}
- {dashboard?.meta.invalid_data &&
Certaines données invalides ont été écartées.
}
+ {pending &&
{messages.dashboardLoading}
}
+ {failed &&
{messages.dashboardFailed}
}
+ {dashboard?.meta.invalid_data &&
{messages.invalidData}
}
{announcement}
onTileKeyDown(event, id)}
- stateLabel={tilePending ? 'CHARGEMENT' : available ? (partial ? 'RÉCENT' : 'DISPONIBLE') : 'INDISPONIBLE'}
+ stateLabel={tilePending ? (language === 'fr' ? 'CHARGEMENT' : 'LOADING') : available ? (partial ? messages.recent : messages.available) : messages.unavailable}
stateTone={available ? (partial ? 'partial' : 'available') : 'unavailable'}
>
{id === 'next-session' ? (
diff --git a/web/src/dashboard/dashboardMessages.ts b/web/src/dashboard/dashboardMessages.ts
new file mode 100644
index 0000000..90ce665
--- /dev/null
+++ b/web/src/dashboard/dashboardMessages.ts
@@ -0,0 +1,56 @@
+import type { WebLanguage } from '../presentation/LanguagePreferences'
+
+export const dashboardMessages = {
+ fr: {
+ tiles: {
+ 'next-session': ['Prochaine séance', 'Planification'],
+ activity: ['Activité', 'Régularité'],
+ progression: ['Progression', 'Évolution'],
+ 'last-session': ['Dernière séance', 'Historique'],
+ 'max-records': ['Mensurations', 'Évolution'],
+ 'muscle-distribution': ['Répartition musculaire', 'Zones'],
+ 'cardio-recovery': ['Programme actif', 'Planification'],
+ },
+ layout: 'Agencement du Dashboard',
+ edit: 'Modifier l’agencement',
+ cancel: 'Annuler',
+ reset: 'Réinitialiser',
+ save: 'Enregistrer',
+ saving: 'Enregistrement…',
+ help: 'Flèches : déplacer · Maj + flèches : redimensionner',
+ loading: 'Chargement de l’agencement…',
+ dashboardLoading: 'Chargement des données du Dashboard…',
+ dashboardFailed: 'Les données du Dashboard sont momentanément indisponibles.',
+ invalidData: 'Certaines données invalides ont été écartées.',
+ unavailable: 'INDISPONIBLE',
+ available: 'DISPONIBLE',
+ recent: 'RÉCENT',
+ tileEditable: 'tuile modifiable',
+ },
+ en: {
+ tiles: {
+ 'next-session': ['Next session', 'Planning'],
+ activity: ['Activity', 'Consistency'],
+ progression: ['Progress', 'Evolution'],
+ 'last-session': ['Last session', 'History'],
+ 'max-records': ['Measurements', 'Evolution'],
+ 'muscle-distribution': ['Muscle distribution', 'Zones'],
+ 'cardio-recovery': ['Active program', 'Planning'],
+ },
+ layout: 'Dashboard layout',
+ edit: 'Edit layout',
+ cancel: 'Cancel',
+ reset: 'Reset',
+ save: 'Save',
+ saving: 'Saving…',
+ help: 'Arrow keys: move · Shift + arrow keys: resize',
+ loading: 'Loading layout…',
+ dashboardLoading: 'Loading Dashboard data…',
+ dashboardFailed: 'Dashboard data is temporarily unavailable.',
+ invalidData: 'Some invalid data was excluded.',
+ unavailable: 'UNAVAILABLE',
+ available: 'AVAILABLE',
+ recent: 'RECENT',
+ tileEditable: 'editable tile',
+ },
+} as const satisfies Record
diff --git a/web/src/presentation/LanguagePreferences.tsx b/web/src/presentation/LanguagePreferences.tsx
index f5bf8aa..f8aefaf 100644
--- a/web/src/presentation/LanguagePreferences.tsx
+++ b/web/src/presentation/LanguagePreferences.tsx
@@ -9,7 +9,7 @@ interface LanguageContextValue {
setLanguage: (language: WebLanguage) => void
}
-const LanguageContext = createContext(null)
+const LanguageContext = createContext({ language: 'fr', setLanguage: () => undefined })
export function LanguagePreferencesProvider({ children }: { children: ReactNode }) {
const [language, updateLanguage] = useState(() =>
@@ -29,7 +29,5 @@ export function LanguagePreferencesProvider({ children }: { children: ReactNode
}
export function useLanguagePreferences(): LanguageContextValue {
- const value = useContext(LanguageContext)
- if (value === null) throw new Error('LanguagePreferencesProvider is missing')
- return value
+ return useContext(LanguageContext)
}
diff --git a/web/src/routes/AnalysisPage.test.tsx b/web/src/routes/AnalysisPage.test.tsx
index bdd7bf1..0ae4538 100644
--- a/web/src/routes/AnalysisPage.test.tsx
+++ b/web/src/routes/AnalysisPage.test.tsx
@@ -188,6 +188,8 @@ describe("AnalysisPage", () => {
expect(delta.parentElement).toHaveClass("analysis-delta");
expect(screen.getByText(/Poids · kg · 2 points réels/)).toBeInTheDocument();
expect(document.querySelectorAll(".chart-x-label")).toHaveLength(2);
+ expect(document.querySelector(".analysis-chart svg"))
+ .toHaveAttribute("preserveAspectRatio", "xMidYMid meet");
});
it("uses a factual exercises-by-date histogram in the overview", async () => {
@@ -201,4 +203,36 @@ describe("AnalysisPage", () => {
expect.stringContaining("1 exercice(s)"),
);
});
+
+ it("reports missing and partial exercise timing without inventing duration", async () => {
+ const missing: AnalysisSnapshot = {
+ ...analysisFixture,
+ exercise_groups: [{
+ zone_id: "legs", label: "Jambes", exercises: [{
+ exercise_id: "ex_timing", name: "Presse", occurrences: 2, sessions: 2,
+ sets: 5, measured_seconds: 0, measured_occurrences: 0,
+ }],
+ }],
+ };
+ vi.mocked(globalThis.fetch).mockResolvedValue(
+ new Response(JSON.stringify(missing), { status: 200 }),
+ );
+ const { rerender } = render();
+ expect(await screen.findByTestId("duration-coverage")).toHaveTextContent(
+ "Temps non mesuré · durée indisponible",
+ );
+ vi.mocked(globalThis.fetch).mockResolvedValue(
+ new Response(JSON.stringify({
+ ...missing,
+ exercise_groups: [{
+ ...missing.exercise_groups[0],
+ exercises: [{ ...missing.exercise_groups[0].exercises[0], measured_seconds: 600, measured_occurrences: 1 }],
+ }],
+ }), { status: 200 }),
+ );
+ rerender();
+ expect(await screen.findByTestId("duration-coverage")).toHaveTextContent(
+ "Couverture temporelle partielle",
+ );
+ });
});
diff --git a/web/src/routes/AnalysisPage.tsx b/web/src/routes/AnalysisPage.tsx
index 4921326..14cdd7d 100644
--- a/web/src/routes/AnalysisPage.tsx
+++ b/web/src/routes/AnalysisPage.tsx
@@ -283,7 +283,7 @@ export function AnalysisLineChart({
viewBox="0 0 112 78"
role="img"
aria-label={`${label}: ${points.length} points`}
- preserveAspectRatio="none"
+ preserveAspectRatio="xMidYMid meet"
>
@@ -680,6 +680,23 @@ export function AnalysisPage() {
/>
)}
+ {(() => {
+ const occurrences = snapshot.exercise_groups.flatMap((group) => group.exercises)
+ .reduce((sum, exercise) => sum + exercise.occurrences, 0);
+ const measured = snapshot.exercise_groups.flatMap((group) => group.exercises)
+ .reduce((sum, exercise) => sum + exercise.measured_occurrences, 0);
+ if (occurrences === 0 || measured === occurrences) return null;
+ const coverage = measured === 0
+ ? (language === "fr"
+ ? "Temps non mesuré · durée indisponible"
+ : "Time not measured · duration unavailable")
+ : (language === "fr"
+ ? "Couverture temporelle partielle"
+ : "Partial time coverage");
+ return
+ {coverage}
+
;
+ })()}
{snapshot.overview.sessions === 0 && (
{t.noData}
)}
diff --git a/web/src/routes/SleepDiaryWorkspace.test.tsx b/web/src/routes/SleepDiaryWorkspace.test.tsx
index a244536..d4d4e25 100644
--- a/web/src/routes/SleepDiaryWorkspace.test.tsx
+++ b/web/src/routes/SleepDiaryWorkspace.test.tsx
@@ -639,9 +639,11 @@ describe("SleepDiaryWorkspace", () => {
9,
);
expect(intake).toHaveStyle({ left: "18.75%" });
- expect(document.querySelectorAll(".sleep-hour-axis > span")).toHaveLength(
- 25,
- );
+ const ticks = Array.from(document.querySelectorAll(".sleep-hour-axis > span"));
+ expect(ticks.map((tick) => [tick.textContent, tick.style.left])).toEqual([
+ ["18:00", "0%"], ["00:00", "25%"], ["06:00", "50%"],
+ ["12:00", "75%"], ["18:00", "100%"],
+ ]);
});
it("isolates an empty editor night while retaining the period agenda", async () => {
diff --git a/web/src/routes/SleepDiaryWorkspace.tsx b/web/src/routes/SleepDiaryWorkspace.tsx
index ad9f481..f95013e 100644
--- a/web/src/routes/SleepDiaryWorkspace.tsx
+++ b/web/src/routes/SleepDiaryWorkspace.tsx
@@ -717,12 +717,12 @@ export function SleepDiaryWorkspace({
- {Array.from({ length: 25 }, (_, index) => (
+ {[0, 6, 12, 18, 24].map((index) => (
- {(18 + index) % 24}
+ {String((18 + index) % 24).padStart(2, "0")}:00
))}
diff --git a/web/src/theme/app.css b/web/src/theme/app.css
index 0a4a172..0fa61e7 100644
--- a/web/src/theme/app.css
+++ b/web/src/theme/app.css
@@ -114,12 +114,17 @@
border-radius: 2px;
}
.sync-control {
- margin-left: auto;
display: flex;
align-items: center;
gap: 0.6rem;
position: relative;
}
+.header-actions {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+}
.sync-control button {
border: 1px solid var(--lavender);
border-radius: 0.55rem;
@@ -514,7 +519,7 @@ h2 {
.analysis-program progress {
width: 100%;
height: 0.7rem;
- accent-color: var(--lavender);
+ accent-color: var(--green);
}
.analysis-next-session {
padding: 0.75rem;
@@ -1558,7 +1563,7 @@ h2 {
.program-progress progress {
width: 100%;
height: 0.65rem;
- accent-color: var(--lavender);
+ accent-color: var(--green);
}
.program-progress .program-next {
margin-top: 0.35rem;
@@ -2941,6 +2946,11 @@ h2 {
margin-left: 0;
justify-content: space-between;
}
+ .header-actions {
+ width: 100%;
+ margin-left: 0;
+ justify-content: flex-end;
+ }
.sync-details[open] {
position: static;
min-width: 0;