PageSourceSearch

https://roxima.io/blocks/pomodoro/script.js?v=81

js roxima.io collected 2026-10-02 01:58:06 UTC 210,494 bytes, 4,786 lines download raw bytes

1/**
2 * Pomodoro Block with Advanced Settings
3 */
4import { audioDB } from '../../shared/indexeddb.js?v=2';
5import { sendGAEvent } from '../../shared/analytics.js';
6import { i18n } from '../../shared/i18n.js';
7import { objectifsStore } from '../../shared/objectifs_store.js';
8import { pomodoroStore } from '../../shared/store_pomodoro.js';
9import { getLocalDayString } from '../../shared/day_closure.mjs';
10import { buildFactoryStats } from '../../shared/factoryEconomy.mjs';
11import { loadSettledFactoryState } from '../../shared/factoryStore.mjs';
12import { calculateProductivityRank } from '../topbar/ranks.mjs?v=3';
13import { createFocusAmbience } from './ambience.js?v=5';
14import {
15    ALARM_RING_DURATION_MS,
16    getAlarmPlaybackPolicy,
17    getAlarmRepeatIntervalMs
18} from './alarm_runtime.mjs?v=1';
19import {
20    CUSTOM_TIMER_CATEGORY_ID_PREFIX,
21    CUSTOM_TIMER_CATEGORY_LIMITS,
22    CUSTOM_TIMER_CATEGORY_EMOJIS,
23    CUSTOM_TIMER_CATEGORY_COLORS,
24    CUSTOM_TIMER_LIMITS,
25    DEFAULT_CUSTOM_TIMER_CATEGORY_COLOR,
26    DEFAULT_CUSTOM_TIMER_CATEGORY_EMOJI,
27    getCustomTimerCategoryDefinition,
28    getCustomTimerSessionMode,
29    normalizeCustomTimerCategory,
30    normalizeCustomTimerCategories,
31    normalizeCustomTimerCategoryEmoji,
32    normalizeCustomTimerCategoryColor,
33    normalizeCustomTimerCategoryName,
34    normalizeCustomTimerMinutes,
35    normalizeCustomTimerName,
36    normalizeCustomTimers,
37    removeCustomTimer,
38    upsertCustomTimerCategory,
39    upsertCustomTimer
40} from './custom_timers.mjs?v=5';
41import {
42    addTaskToToday,
43    completeCurrentPriority,
44    getTodayPriorities,
45    reorderTodayPriorities,
46    resolveTaskReference
47} from '../../shared/daily_priorities.js';
48import {
49    buildDayPlanSegments,
50    getActualDayProgressSeconds,
51    getExpectedDayProgress,
52    reconcileDayPlanTasks,
53    summarizeDayProgress
54} from './day_progress.mjs?v=2';
55import {
56    DEFAULT_POMODORO_START_SHORTCUT,
57    getPomodoroStartShortcut,
58    normalizePomodoroStartShortcut,
59    shouldHandlePomodoroStartShortcut
60} from './keyboard_shortcut.mjs?v=1';
61import {
62    buildTwoDayPilotAssistanceMarker,
63    buildTwoDayPilotReminderIcsUrl,
64    buildTwoDayPilotReminderUrl,
65    buildTwoDayPilotServerEvidence,
66    claimTwoDayPilotForUser,
67    createTwoDayPilotServerRefreshCoordinator,
68    getPilotLocalDate,
69    getTwoDayPilotStage,
70    getTwoDayPilotState,
71    ORGANIC_TWO_DAY_PILOT_CAMPAIGN,
72    reconcileTwoDayPilotFromServerEvidenceOnce,
73    shouldRefreshTwoDayPilotForNewDay,
74    shouldReturnToPilotFocus,
75    TWO_DAY_PILOT_MIN_FOCUS_SECONDS,
76    TWO_DAY_PILOT_RETURN_SOURCE_PARAM,
77    TWO_DAY_PILOT_RETURN_TARGET_PARAM
78} from '../../shared/two_day_pilot.mjs?v=3';
79import {
80    loadTwoDayPilotServerSnapshot,
81    persistTwoDayPilotAssistance
82} from '../../shared/sync.js?v=19';
83import {
84    ROXIMA_BROWSER_FEEDBACK_EVENT,
85    attachBrowserFeedbackToHistory,
86    dispatchBrowserFocusState,
87    normalizeBrowserFeedback
88} from '../../shared/browser_focus_bridge.mjs';
89import {
90    POMODORO_TIMER_STORAGE_KEY,
91    POMODORO_TIMER_OWNER_TIMEOUT_MS,
92    readSharedPomodoroTimerState,
93    shouldClaimPomodoroTimerOwnership
94} from '../../shared/pomodoro_timer_sync.mjs?v=1';
95
96let pilotPriorityPickerReturnPending = false;
97let pilotResumeTaskPending = false;
98let pilotRenderedLocalDate = null;
99let pilotDismissedContextKey = null;
100let pilotServerEvidenceReadyOwnerId = null;
101let pilotServerRefreshOwnerId = null;
102let pilotAssistedReturnCaptureKey = null;
103let pilotPendingAssistedReturnMarker = null;
104const pilotServerRefreshCoordinator = createTwoDayPilotServerRefreshCoordinator({
105    getCurrentOwnerId: () => globalCtx?.currentUser?.id
106});
107let dailyPrioritySequence = [];
108let dailyCompletionFeedbackActive = false;
109let dayProgressPlanCache = null;
110let browserFeedbackListenerBound = false;
111let timerStorageListenerBound = false;
112let lastTimerStateUpdatedAt = 0;
113const pendingBrowserFeedback = new Map();
114
115const COMPLETION_CELEBRATION_MS = 1800;
116const DAY_PROGRESS_STORAGE_KEY = 'rox_pomo_day_progress_v1';
117const RANK_BADGE_ASSETS = Object.freeze({
118    seed: './assets/ranks/graine.webp',
119    momentum: './assets/ranks/elan.webp',
120    focus: './assets/ranks/focus.webp',
121    mastery: './assets/ranks/maitrise.webp',
122    elite: './assets/ranks/elite.webp',
123    summit: './assets/ranks/sommet.webp'
124});
125
126function getDailyPriorityPosition(priorities, currentTaskId) {
127    const taskIds = priorities
128        .map((item) => String(item?.taskId || '').trim())
129        .filter(Boolean);
130    const availableTaskIds = new Set(taskIds);
131
132    dailyPrioritySequence = dailyPrioritySequence
133        .filter((taskId) => availableTaskIds.has(taskId));
134    taskIds.forEach((taskId) => {
135        if (!dailyPrioritySequence.includes(taskId)) dailyPrioritySequence.push(taskId);
136    });
137
138    const currentIndex = dailyPrioritySequence.indexOf(String(currentTaskId || ''));
139    return currentIndex >= 0 ? currentIndex + 1 : 1;
140}
141
142function isDayClosed() {
143    return pomodoroStore.isDayClosed();
144}
145
146function generateUUID() {
147    if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();
148    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
149        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
150        return v.toString(16);
151    });
152}
153
154function escapeHtml(value = '') {
155    const span = document.createElement('span');
156    span.textContent = String(value);
157    return span.innerHTML;
158}
159
160function translatePomodoro(key, variables = {}) {
161    return Object.entries(variables).reduce((text, [name, value]) => (
162        text.replaceAll(`{${name}}`, String(value))
163    ), i18n.t(key));
164}
165
166function getDayProgressOwnerId() {
167    return String(globalCtx?.currentUser?.id || 'guest');
168}
169
170function readDayProgressPlan() {
171    const date = getLocalDayString();
172    const ownerId = getDayProgressOwnerId();
173    if (dayProgressPlanCache?.date === date && dayProgressPlanCache?.ownerId === ownerId) {
174        return dayProgressPlanCache;
175    }
176
177    try {
178        const parsed = JSON.parse(localStorage.getItem(DAY_PROGRESS_STORAGE_KEY) || 'null');
179        dayProgressPlanCache = parsed?.date === date && parsed?.ownerId === ownerId && Array.isArray(parsed.tasks)
180            ? parsed
181            : { date, ownerId, tasks: [] };
182    } catch (_) {
183        dayProgressPlanCache = { date, ownerId, tasks: [] };
184    }
185    return dayProgressPlanCache;
186}
187
188function saveDayProgressPlan(plan) {
189    dayProgressPlanCache = plan;
190    try {
191        localStorage.setItem(DAY_PROGRESS_STORAGE_KEY, JSON.stringify(plan));
192    } catch (error) {
193        console.warn('[Pomodoro] Impossible d’enregistrer la progression du jour.', error);
194    }
195    return plan;
196}
197
198function syncDayProgressPlan(priorities = getTodayPriorities()) {
199    const currentPlan = readDayProgressPlan();
200    const tasks = reconcileDayPlanTasks(currentPlan.tasks, priorities, state.settings.focusTime);
201    const unchanged = JSON.stringify(tasks) === JSON.stringify(currentPlan.tasks);
202    return unchanged ? currentPlan : saveDayProgressPlan({ ...currentPlan, tasks });
203}
204
205function markDayProgressTaskCompleted(task) {
206    const taskId = String(task?.taskId || task?.task_id || '').trim();
207    if (!taskId) return;
208
209    const currentPlan = readDayProgressPlan();
210    const existing = currentPlan.tasks.find((item) => item.taskId === taskId);
211    const completedTask = {
212        taskId,
213        title: String(task?.text || task?.title || existing?.title || '').trim(),
214        estimatedMinutes: Number(task?.estimatedMinutes ?? task?.estimated_minutes ?? task?.duration_minutes)
215            || existing?.estimatedMinutes
216            || state.settings.focusTime,
217        position: existing?.position || currentPlan.tasks.length + 1,
218        completed: true
219    };
220    const tasks = existing
221        ? currentPlan.tasks.map((item) => item.taskId === taskId ? completedTask : item)
222        : [...currentPlan.tasks, completedTask];
223    saveDayProgressPlan({ ...currentPlan, tasks });
224}
225
226function formatDayProgressDuration(seconds) {
227    const safeSeconds = Math.max(0, Number(seconds || 0));
228    if (safeSeconds > 0 && safeSeconds < 60) return `${Math.max(1, Math.round(safeSeconds))} s`;
229    const minutes = Math.max(0, Math.round(safeSeconds / 60));
230    const hours = Math.floor(minutes / 60);
231    const remainingMinutes = minutes % 60;
232    if (hours === 0) return `${minutes} min`;
233    return remainingMinutes > 0 ? `${hours} h ${remainingMinutes} min` : `${hours} h`;
234}
235
236function getDayProgressSnapshot(priorities = getTodayPriorities(), { syncPlan = false, now = new Date() } = {}) {
237    const plan = syncPlan ? syncDayProgressPlan(priorities) : readDayProgressPlan();
238    const segments = buildDayPlanSegments(plan.tasks, state.settings);
239    const localDate = getLocalDayString(now);
240    const duration = getDuration();
241    const actualSeconds = getActualDayProgressSeconds({
242        history: pomodoroStore.getHistory(),
243        localDate,
244        getLocalDate: getLocalDayString,
245        currentMode: getModeCategory(state.mode),
246        currentDurationSeconds: duration,
247        currentTimeLeftSeconds: state.timeLeft
248    });
249    const expectedRatio = getExpectedDayProgress(
250        now,
251        state.settings.sleepWakeup,
252        state.settings.sleepBedtime
253    );
254    return {
255        ...summarizeDayProgress({ segments, expectedRatio, actualSeconds }),
256        segments
257    };
258}
259
260function getDayProgressCopy(snapshot, now = new Date()) {
261    const language = i18n.getLang?.() === 'fr' ? 'fr' : 'en';
262    const actualPercent = Math.round(snapshot.actualRatio * 100);
263    const expectedPercent = Math.round(snapshot.expectedRatio * 100);
264    const time = now.toLocaleTimeString(language === 'fr' ? 'fr-FR' : 'en-US', {
265        hour: '2-digit',
266        minute: '2-digit'
267    });
268    if (language === 'fr') {
269        return `Plan du jour : ${formatDayProgressDuration(snapshot.totalSeconds)}. Avancement réel : ${formatDayProgressDuration(snapshot.actualSeconds)} (${actualPercent} %). Rythme conseillé à ${time} : ${formatDayProgressDuration(snapshot.expectedSeconds)} (${expectedPercent} %). Vert : focus, bleu : pause courte, violet : pause longue.`;
270    }
271    return `Day plan: ${formatDayProgressDuration(snapshot.totalSeconds)}. Actual progress: ${formatDayProgressDuration(snapshot.actualSeconds)} (${actualPercent}%). Suggested pace at ${time}: ${formatDayProgressDuration(snapshot.expectedSeconds)} (${expectedPercent}%). Green: focus, blue: short break, purple: long break.`;
272}
273
274function buildDayProgressSegmentsMarkup(segments, { showFocusNumbers = false } = {}) {
275    let focusIndex = 0;
276    return segments.map((segment) => {
277        const width = `${(segment.ratio * 100).toFixed(4)}%`;
278        const isFocus = segment.type === 'focus';
279        if (isFocus) focusIndex += 1;
280        const label = isFocus
281            ? `Focus · ${segment.minutes} min`
282            : segment.type === 'short-break'
283                ? `${i18n.t('pomo_mode_short')} · ${segment.minutes} min`
284                : `${i18n.t('pomo_mode_long')} · ${segment.minutes} min`;
285        const focusNumberMarkup = showFocusNumbers && isFocus
286            ? `<span class="pomo-day-progress__focus-number">${focusIndex}</span>`
287            : '';
288        return `<span class="pomo-day-progress__segment is-${segment.type}" style="--pomo-day-segment-width:${width}" title="${escapeHtml(label)}">${focusNumberMarkup}</span>`;
289    }).join('');
290}
291
292function buildDayProgressMarkup(snapshot) {
293    if (!snapshot.totalSeconds || snapshot.segments.length === 0) return '';
294    const expectedPercent = `${(snapshot.expectedRatio * 100).toFixed(3)}%`;
295    const actualPercent = `${(snapshot.actualRatio * 100).toFixed(3)}%`;
296    const segmentsMarkup = buildDayProgressSegmentsMarkup(snapshot.segments);
297    const focusNumbersMarkup = buildDayProgressSegmentsMarkup(snapshot.segments, { showFocusNumbers: true });
298    const copy = getDayProgressCopy(snapshot);
299    return `
300        <div class="pomo-day-progress" data-pomo-day-progress role="progressbar" tabindex="0" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${Math.round(snapshot.actualRatio * 100)}" aria-valuetext="${escapeHtml(copy)}" title="${escapeHtml(copy)}" style="--pomo-day-expected:${expectedPercent};--pomo-day-actual:${actualPercent}">
301            <div class="pomo-day-progress__layer is-plan" aria-hidden="true">${segmentsMarkup}</div>
302            <div class="pomo-day-progress__layer is-expected" aria-hidden="true">${segmentsMarkup}</div>
303            <div class="pomo-day-progress__layer is-actual" aria-hidden="true">${segmentsMarkup}</div>
304            <div class="pomo-day-progress__layer is-labels" aria-hidden="true">${focusNumbersMarkup}</div>
305        </div>
306    `;
307}
308
309function updateDayProgressGauge(now = new Date()) {
310    const gauge = containerEl?.querySelector('[data-pomo-day-progress]');
311    if (!gauge) return;
312    const snapshot = getDayProgressSnapshot(getTodayPriorities(), { now });
313    const copy = getDayProgressCopy(snapshot, now);
314    gauge.style.setProperty('--pomo-day-expected', `${(snapshot.expectedRatio * 100).toFixed(3)}%`);
315    gauge.style.setProperty('--pomo-day-actual', `${(snapshot.actualRatio * 100).toFixed(3)}%`);
316    gauge.setAttribute('aria-valuenow', String(Math.round(snapshot.actualRatio * 100)));
317    gauge.setAttribute('aria-valuetext', copy);
318    gauge.title = copy;
319}
320
321function getGeneralPriorityPresentation(priority) {
322    const normalized = String(priority || 'none').toLowerCase();
323    if (normalized === 'p1' || normalized === '3' || normalized === 'high') {
324        return { label: 'P1', className: 'is-p1', title: i18n.t('daily_priority_level_p1') };
325    }
326    if (normalized === 'p2' || normalized === '2' || normalized === 'medium') {
327        return { label: 'P2', className: 'is-p2', title: i18n.t('daily_priority_level_p2') };
328    }
329    if (normalized === 'p3' || normalized === '1' || normalized === 'low') {
330        return { label: 'P3', className: 'is-p3', title: i18n.t('daily_priority_level_p3') };
331    }
332    return null;
333}
334
335function getCurrentDailyPriority() {
336    return getTodayPriorities()[0] || null;
337}
338
339function getTaskFocusMinutes() {
340    const sessionMins = Number(state.activeTask?.sessionMinutes || state.activeTask?.session_minutes);
341    if (Number.isFinite(sessionMins) && sessionMins >
341 0) return Math.min(Math.max(Math.round(sessionMins), 1), 1440);
342    return normalizeNumericSetting('focusTime', state.settings.focusTime);
343}
344
345const NUMERIC_SETTING_RULES = {
346    focusTime: { fallback: 25, min: 1, max: 180 },
347    shortBreakTime: { fallback: 5, min: 1, max: 60 },
348    longBreakTime: { fallback: 15, min: 1, max: 180 },
349    longBreakInterval: { fallback: 4, min: 1, max: 10 },
350    dailyGoalMin: { fallback: 120, min: 10, max: 1440 },
351    volume: { fallback: 50, min: 0, max: 100 }
352};
353
354function normalizeNumericSetting(key, value, fallbackOverride = undefined) {
355    const rule = NUMERIC_SETTING_RULES[key];
356    if (!rule) return value;
357
358    const parsed = Number.parseInt(value, 10);
359    const parsedFallback = Number.parseInt(fallbackOverride ?? rule.fallback, 10);
360    const fallback = Number.isFinite(parsedFallback) ? parsedFallback : rule.fallback;
361    const safeValue = Number.isFinite(parsed) ? parsed : fallback;
362
363    return Math.min(Math.max(safeValue, rule.min), rule.max);
364}
365
366function syncNumberInputValue(input, key) {
367    const value = normalizeNumericSetting(key, input.value, state.settings[key]);
368    input.value = String(value);
369    return value;
370}
371
372let state = {
373    mode: 'focus', // focus, short-break, long-break
374    timeLeft: 25 * 60,
375    duration: 25 * 60,
376    isActive: false,
377    interval: null,
378    pomoCount: 0, // Number of completed pomodoros
379    activeTask: null,
380    pendingReview: null,
381    settingsOpen: false,
382    audioPreview: null, // Audio object for preview
383    sessionEndMs: null,
384    browserFocusSessionId: null,
385    timerOwnerTabId: null,
386    timerOwnerHeartbeatAt: 0,
387    timerHeartbeatTicks: 0,
388    isTimerFollower: false,
389    focusDurationOverrideSeconds: null,
390    isDemoSession: false,
391    settings: {
392        focusTime: 25,
393        shortBreakTime: 5,
394        longBreakTime: 15,
395        longBreakInterval: 4,
396        autoStartBreaks: false,
397        autoStartPomo: false,
398        dailyGoalMin: 120, // 2 hours default
399        alarmSound: 'bell', // 'none', 'bell', 'chime', 'digital', 'custom'
400        volume: 50,
401        customSoundId: null,
402        customSoundName: null,
403        sleepBedtime: "23:00",
404        sleepWakeup: "07:00",
405        startShortcut: DEFAULT_POMODORO_START_SHORTCUT,
406        customTimers: [],
407        customTimerCategories: []
408    }
409};
410let dailyActionPending = false;
411let dailyRestoreFocusId = null;
412let customTimerEditorId = null;
413let customTimerEditorReturnFocus = null;
414
415const MODES = {
416    'focus': { labelKey: 'pomo_mode_focus', color: '#10B981' },
417    'short-break': { labelKey: 'pomo_mode_short', color: '#3B82F6' },
418    'long-break': { labelKey: 'pomo_mode_long', color: '#8B5CF6' }
419};
420const CUSTOM_MODE_PREFIX = 'custom:';
421
422function getBrowserFocusTaskText() {
423    return String(
424        state.activeTask?.text
425        || getCurrentDailyPriority()?.text
426        || ''
427    ).trim();
428}
429
430function syncBrowserFocusProtection(active, status = active ? 'active' : 'paused') {
431    if (state.isDemoSession || !isFocusMode(state.mode) || !state.browserFocusSessionId) return false;
432
433    return dispatchBrowserFocusState({
434        active,
435        status,
436        sessionId: state.browserFocusSessionId,
437        taskText: getBrowserFocusTaskText(),
438        endsAt: active
439            ? (state.sessionEndMs || Date.now() + state.timeLeft * 1000)
440            : Date.now()
441    });
442}
443
444function handleBrowserFeedback(value) {
445    const feedback = normalizeBrowserFeedback(value);
446    if (!feedback) return false;
447
448    pendingBrowserFeedback.set(feedback.sessionId, feedback);
449    const result = attachBrowserFeedbackToHistory(pomodoroStore.getHistory(), feedback);
450    if (!result.updated) return false;
451
452    pomodoroStore.updateHistory(result.history);
453    pendingBrowserFeedback.delete(feedback.sessionId);
454    return true;
455}
456
457function getCustomTimers() {
458    return normalizeCustomTimers(state.settings.customTimers);
459}
460
461function getCustomTimerCategories() {
462    return normalizeCustomTimerCategories(state.settings.customTimerCategories);
463}
464
465function getTimerCategoryMeta(categoryId) {
466    const customCategory = getCustomTimerCategoryDefinition(categoryId, getCustomTimerCategories());
467    if (customCategory) {
468        return { ...customCategory, custom: true };
469    }
470
471    const builtIn = {
472        focus: { id: 'focus', name: i18n.t('pomo_mode_focus'), emoji: '', color: MODES.focus.color },
473        'short-break': { id: 'short-break', name: i18n.t('pomo_mode_short'), emoji: '', color: MODES['short-break'].color },
474        'long-break': { id: 'long-break', name: i18n.t('pomo_mode_long'), emoji: '', color: MODES['long-break'].color }
475    };
476    return builtIn[categoryId] || builtIn.focus;
477}
478
479function renderTimerCategoryOptionsMarkup(selectedCategory = 'focus') {
480    const builtInCategories = ['focus', 'short-break', 'long-break'].map((id) => getTimerCategoryMeta(id));
481    const categories = [
482        ...builtInCategories,
483        ...getCustomTimerCategories().map((category) => ({ ...category, custom: true }))
484    ];
485    const selected = categories.some((category) => category.id === selectedCategory)
486        ? selectedCategory
487        : 'focus';
488
489    return categories.map((category) => `
490        <label>
491            <input type="radio" name="category" value="${escapeHtml(category.id)}" ${category.id === selected ? 'checked' : ''}>
492            <span style="--category-color:${escapeHtml(category.color)}">${category.emoji ? `<b aria-hidden="true">${escapeHtml(category.emoji)}</b>` : ''}${escapeHtml(category.name)}</span>
493        </label>
494    `).join('');
495}
496
497function renderCustomCategoryEmojiPickerMarkup(value = DEFAULT_CUSTOM_TIMER_CATEGORY_EMOJI) {
498    const selectedEmoji = normalizeCustomTimerCategoryEmoji(value) || DEFAULT_CUSTOM_TIMER_CATEGORY_EMOJI;
499    const suggestedEmojis = new Set(CUSTOM_TIMER_CATEGORY_EMOJIS.map((option) => option.value));
500    const manualValue = suggestedEmojis.has(selectedEmoji) ? '' : selectedEmoji;
501
502    return `
503        <details class="pomo-category-emoji-picker" data-custom-category-emoji-picker>
504            <summary aria-label="${escapeHtml(i18n.t('pomo_custom_category_emoji_choose'))}" title="${escapeHtml(i18n.t('pomo_custom_category_emoji_choose'))}">
505                <span data-custom-category-emoji-preview aria-hidden="true">${escapeHtml(selectedEmoji)}</span>
506                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg>
507            </summary>
508            <div class="pomo-category-emoji-menu">
509                <div class="pomo-category-emoji-grid" role="group" aria-label="${escapeHtml(i18n.t('pomo_custom_category_emoji_suggestions'))}">
510                    ${CUSTOM_TIMER_CATEGORY_EMOJIS.map((option) => `
511                        <button type="button"
512                            data-custom-category-emoji-option="${escapeHtml(option.value)}"
513                            aria-label="${escapeHtml(i18n.t(option.labelKey))}"
514                            title="${escapeHtml(i18n.t(option.labelKey))}"
515                            aria-pressed="${String(option.value === selectedEmoji)}">${escapeHtml(option.value)}</button>
516                    `).join('')}
517                </div>
518                <label class="pomo-category-emoji-manual">
519                    <span data-i18n="pomo_custom_category_emoji_custom">${escapeHtml(i18n.t('pomo_custom_category_emoji_custom'))}</span>
520                    <input type="text" inputmode="text" maxlength="${CUSTOM_TIMER_CATEGORY_LIMITS.maxEmojiLength}" value="${escapeHtml(manualValue)}" placeholder="🙂" data-custom-category-emoji-manual>
521                </label>
522            </div>
523            <input id="pomo-custom-category-emoji" type="hidden" value="${escapeHtml(selectedEmoji)}" data-custom-category-emoji-value>
524        </details>
525    `;
526}
527
528function setCustomCategoryEmoji(picker, value, { emit = false, close = false } = {}) {
529    if (!picker) return '';
530    const emoji = normalizeCustomTimerCategoryEmoji(value);
531    if (!emoji) return '';
532
533    const valueInput = picker.querySelector('[data-custom-category-emoji-value]');
534    const preview = picker.querySelector('[data-custom-category-emoji-preview]');
535    const manualInput = picker.querySelector('[data-custom-category-emoji-manual]');
536    const isSuggested = CUSTOM_TIMER_CATEGORY_EMOJIS.some((option) => option.value === emoji);
537    if (valueInput) valueInput.value = emoji;
538    if (preview) preview.textContent = emoji;
539    if (manualInput && (isSuggested || manualInput.value !== emoji)) {
540        manualInput.value = isSuggested ? '' : emoji;
541    }
542    picker.querySelectorAll('[data-custom-category-emoji-option]').forEach((button) => {
543        button.setAttribute('aria-pressed', String(button.dataset.customCategoryEmojiOption === emoji));
544    });
545    if (close) picker.open = false;
546    if (emit && valueInput) valueInput.dispatchEvent(new Event('change', { bubbles: true }));
547    return emoji;
548}
549
550function renderCustomCategoryColorPickerMarkup(value = DEFAULT_CUSTOM_TIMER_CATEGORY_COLOR) {
551    const selectedColor = normalizeCustomTimerCategoryColor(value);
552    return `
553        <div class="pomo-category-color-options" role="group" aria-label="${escapeHtml(i18n.t('pomo_custom_category_color_options'))}">
554            ${CUSTOM_TIMER_CATEGORY_COLORS.map((option) => `
555                <button type="button"
556                    data-custom-category-color-option="${escapeHtml(option.value)}"
557                    style="--category-color:${escapeHtml(option.value)}"
558                    aria-label="${escapeHtml(i18n.t(option.labelKey))}"
559                    title="${escapeHtml(i18n.t(option.labelKey))}"
560                    aria-pressed="${String(option.value === selectedColor)}"><span aria-hidden="true"></span></button>
561            `).join('')}
562        </div>
563        <input id="pomo-custom-category-color" type="hidden" value="${escapeHtml(selectedColor)}" data-custom-category-color-value>
564    `;
565}
566
567function setCustomCategoryColor(root, value, { emit = false } = {}) {
568    if (!root) return DEFAULT_CUSTOM_TIMER_CATEGORY_COLOR;
569    const color = normalizeCustomTimerCategoryColor(value);
570    const valueInput = root.querySelector('[data-custom-category-color-value]');
571    if (valueInput) valueInput.value = color;
572    root.querySelectorAll('[data-custom-category-color-option]').forEach((button) => {
573        button.setAttribute('aria-pressed', String(button.dataset.customCategoryColorOption === color));
574    });
575    if (emit && valueInput) valueInput.dispatchEvent(new Event('change', { bubbles: true }));
576    return color;
577}
578
579function getCustomTimerId(mode) {
580    return String(mode || '').startsWith(CUSTOM_MODE_PREFIX)
581        ? String(mode).slice(CUSTOM_MODE_PREFIX.length)
582        : null;
583}
584
585function getCustomTimer(mode) {
586    const timerId = getCustomTimerId(mode);
587    return timerId ? getCustomTimers().find((timer) => timer.id === timerId) || null : null;
588}
589
590function isCustomMode(mode) {
591    return Boolean(getCustomTimer(mode));
592}
593
594function getModeCategory(mode) {
595    const customTimer = getCustomTimer(mode);
596    if (customTimer) return getCustomTimerSessionMode(customTimer) || 'focu
596s';
597    return MODES[mode] ? mode : 'focus';
598}
599
600function isFocusMode(mode) {
601    return getModeCategory(mode) === 'focus';
602}
603
604function getModeLabel(mode) {
605    const customTimer = getCustomTimer(mode);
606    if (customTimer) {
607        const category = getTimerCategoryMeta(customTimer.category);
608        return `${category.custom && category.emoji ? `${category.emoji} ` : ''}${customTimer.name}`;
609    }
610    return i18n.t(MODES[mode]?.labelKey || MODES.focus.labelKey);
611}
612
613function getModeColor(mode) {
614    return getTimerCategoryMeta(getModeCategory(mode)).color;
615}
616
617function syncModeBadge(badge, mode) {
618    if (!badge) return;
619
620    const modeDefinition = MODES[mode];
621    if (modeDefinition) {
622        badge.dataset.i18n = modeDefinition.labelKey;
623    } else {
624        badge.removeAttribute('data-i18n');
625    }
626    badge.textContent = getModeLabel(mode);
627    badge.className = 'badge-pomo active';
628    badge.dataset.mode = mode;
629    badge.dataset.timerCategory = getModeCategory(mode);
630}
631
632function renderModeOptionsMarkup() {
633    const defaultModes = Object.keys(MODES).map((mode) => `
634        <button class="badge-pomo pomo-mode-trigger" type="button" data-mode="${mode}" data-timer-category="${mode}">${escapeHtml(getModeLabel(mode))}</button>
635    `).join('');
636    const customModes = getCustomTimers().map((timer) => {
637        const mode = `${CUSTOM_MODE_PREFIX}${timer.id}`;
638        const category = getTimerCategoryMeta(timer.category);
639        return `
640            <button class="badge-pomo pomo-mode-trigger" type="button" data-mode="${escapeHtml(mode)}" data-timer-category="${escapeHtml(timer.category)}" style="--custom-category-color:${escapeHtml(category.color)}">${escapeHtml(timer.name)}</button>
641        `;
642    }).join('');
643    const atLimit = getCustomTimers().length >= CUSTOM_TIMER_LIMITS.maxCount;
644    const addLabel = atLimit ? i18n.t('pomo_custom_timer_limit_reached') : i18n.t('pomo_custom_timer_add_aria');
645
646    return `${defaultModes}${customModes}
647        <button class="pomo-mode-add pomo-mode-trigger" type="button" data-custom-timer-add aria-label="${escapeHtml(addLabel)}" title="${escapeHtml(addLabel)}" ${atLimit ? 'disabled' : ''}>
648            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"></path></svg>
649        </button>`;
650}
651const FOCUS_GUIDE_COPY = {
652    fr: {
653        activeTaskFallback: 'Action en focus',
654        button: 'Choisir la priorite du jour',
655        chooseAction: 'Choisir une priorit\u00e9',
656        changeTaskLabel: 'Changer la priorite',
657        flowLabel: 'Parcours guide vers le bilan de focus',
658        focus: '2. Focus',
659        hint: 'Pas besoin de tout organiser. Choisissez une seule action, lancez 25 min, puis notez ce qui a avance.',
660        kicker: 'Prochaine etape',
661        pauseAction: 'Mettre en pause',
662        pausedHint: 'Ta prochaine etape : reprends cette session, sans changer de priorite.',
663        pausedKicker: 'Session en pause',
664        priority: '1. Priorite',
665        recommendationAction: 'Adopter cette priorit\u00e9',
666        recommendationHint: 'Roxima te propose la premi\u00e8re action de ta liste. Adopte-la pour passer au focus, ou touche-la pour en choisir une autre.',
667        recommendationKicker: 'Suggestion Roxima',
668        recommendationLabel: 'Priorit\u00e9 sugg\u00e9r\u00e9e',
669        quickStartAction: 'Commencer 5 min',
670        quickStartAria: 'Commencer une session Focus de 5 minutes sur {task}',
671        quickStartTitle: "Commencer petit, sans s'engager sur toute la dur\u00e9e",
672        readyHint: 'Ta prochaine etape : lance 25 min sur cette action.',
673        readyKicker: 'Priorite choisie',
674        resumeAction: 'Reprendre',
675        reviewHint: 'Note ce qui a avance avant de repartir sur une autre action.',
676        reviewKicker: 'Apr\u00e8s la session',
677        reviewCta: 'Voir le bilan',
678        reviewError: "La note n'a pas pu \u00eatre enregistr\u00e9e.",
679        reviewHelper: 'Cette note reste dans ce navigateur avec la session.',
680        reviewLater: 'Plus tard',
681        reviewPlaceholder: 'Ex. brouillon clarifi\u00e9, d\u00e9cision envoy\u00e9e, prochaine \u00e9tape d\u00e9finie...',
682        reviewQuestion: "Qu'est-ce qui a avanc\u00e9 concr\u00e8tement ?",
683        reviewSaved: 'Note enregistr\u00e9e localement.',
684        runningHint: 'Reste sur cette seule action. A la fin, note ce qui a vraiment avance.',
685        runningKicker: 'Focus en cours',
686        startBreakAction: 'Lancer la pause',
687        startFocusAction: 'Lancer {minutes} min',
688        review: '3. Bilan'
689    },
690    en: {
691        activeTaskFallback: 'Focus action',
692        button: 'Choose today priority',
693        chooseAction: 'Choose one priority',
694        changeTaskLabel: 'Change priority',
695        flowLabel: 'Guided path to the focus review',
696        focus: '2. Focus',
697        hint: 'No need to organize everything. Choose one action, run 25 min, then note what moved forward.',
698        kicker: 'Next step',
699        pauseAction: 'Pause',
700        pausedHint: 'Your next step: resume this session without changing priority.',
701        pausedKicker: 'Session paused',
702        priority: '1. Priority',
703        recommendationAction: 'Use this priority',
704        recommendationHint: 'Roxima suggests the first action on your list. Use it to move into focus, or tap it to choose another.',
705        recommendationKicker: 'Roxima suggestion',
706        recommendationLabel: 'Suggested priority',
707        quickStartAction: 'Start with 5 min',
708        quickStartAria: 'Start a 5-minute Focus session on {task}',
709        quickStartTitle: 'Start small without committing to the full duration',
710        readyHint: 'Your next step: run 25 min on this action.',
711        readyKicker: 'Priority chosen',
712        resumeAction: 'Resume',
713        reviewHint: 'Note what moved forward before starting another action.',
714        reviewKicker: 'After the session',
715        reviewCta: 'View the review',
716        reviewError: 'The note could not be saved.',
717        reviewHelper: 'This note stays in this browser with the session.',
718        reviewLater: 'Later',
719        reviewPlaceholder: 'E.g. draft clarified, decision sent, next step defined...',
720        reviewQuestion: 'What moved forward concretely?',
721        reviewSaved: 'Note saved locally.',
722        runningHint: 'Stay on this one action. At the end, note what really moved forward.',
723        runningKicker: 'Focus running',
724        startBreakAction: 'Start the break',
725        startFocusAction: 'Start {minutes} min',
726        review: '3. Review'
727    }
728};
729
730const SETTINGS_FOCUSABLE_SELECTOR = 'a[href], button, input, select, textarea, [tabindex]';
731const SETTINGS_PREVIOUS_TABINDEX_ATTRIBUTE = 'data-pomo-previous-tabindex';
732const SETTINGS_NO_TABINDEX_VALUE = '__none__';
733
734let containerEl, ringEl, timeEl, playBtn, resetBtn, settingsPanel;
735let mobileMiniEl = null;
736let globalCtx;
737let focusAmbience = null;
738
739function restorePomodoroAfterDayClosure(ctx = globalCtx) {
740    state.isActive = false;
741    if (state.interval) {
742        clearInterval(state.interval);
743        state.interval = null;
744    }
745
746    renderLayout();
747    bindEvents(ctx);
748    bindSettingsEvents();
749    setMode(state.mode, false);
750    updateUI();
751
752    if (ctx?.bus) {
753        ctx.bus.emit('history:changed');
754    }
755}
756
757function refreshDayClosureForCurrentDate(ctx = globalCtx) {
758    const isClosedScreenVisible = Boolean(containerEl?.querySelector('.pomo-closed-panel'));
759    if (!isClosedScreenVisible || isDayClosed()) return false;
760
761    restorePomodoroAfterDayClosure(ctx);
762    return true;
763}
764
765function getFocusGuideCopy() {
766    return FOCUS_GUIDE_COPY[i18n.getLang?.()] || FOCUS_GUIDE_COPY.en;
767}
768
769function getRecommendedTask() {
770    const todos = globalCtx?.store?.get('todos') || [];
771    const todoItems = todos.filter(todo => todo && typeof todo === 'object' && !todo.completed && String(todo.text || '').trim());
772    const nextTask = todoItems[0] || null;
773    const taskText = String(nextTask?.text || '').trim();
774
775    return nextTask && taskText
776        ? { ...nextTask, text: taskText }
777        : null;
778}
779
780function buildSelectedTaskButtonHtml(taskText) {
781    return `
782        <div class="pomo-focus-task-selected">
783            <span>${escapeHtml(taskText)}</span>
784            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false">
785                <path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
786            </svg>
787        </div>
788    `;
789}
790
791function buildRecommendedTaskButtonHtml(taskText, label) {
792    return `
793        <div class="pomo-focus-task-recommended">
794            <small>${escapeHtml(label)}</small>
795            <span>${escapeHtml(taskText)}</span>
796        </div>
797    `;
798}
799
800function getFocusGuideState() {
801    const guideCopy = getFocusGuideCopy();
802    const taskText = String(state.activeTask?.text || '').trim();
803
804    if (!taskText) {
805        const recommendedTask = getRecommendedTask();
806        if (recommendedTask) {
807            return {
808                buttonAria: `${guideCopy.recommendationLabel}: ${recommendedTask.text}`,
809                buttonHtml: buildRecommendedTaskButtonHtml(recommendedTask.text, guideCopy.recommendationLabel),
810                completeSteps: [],
811                hint: guideCopy.recommendationHint,
812                kicker: guideCopy.recommendationKicker,
813                recommended: true,
814                selected: false,
815                step: 'priority'
816            };
817        }
818
819        return {
820            buttonAria: guideCopy.button,
821            buttonHtml: escapeHtml(guideCopy.button),
822            completeSteps: [],
823            hint: guideCopy.hint,
824            kicker: guideCopy.kicker,
825            recommended: false,
826            selected: false,
827            step: 'priority'
828        };
829    }
830
831    if (!isFocusMode(state.mode)) {
832        return {
833            buttonAria: `${guideCopy.changeTaskLabel}: ${taskText}`,
834            buttonHtml: buildSelectedTaskButtonHtml(taskText || guideCopy.activeTaskFallback),
835            completeSteps: ['priority', 'focus'],
836            hint: guideCopy.reviewHint,
837            kicker: guideCopy.reviewKicker,
838            recommended: false,
839            selected: true,
840            step: 'review'
841        };
842    }
843
844    const isPaused = !state.isActive && state.timeLeft < getDuration();
845
846    return {
847        buttonAria: `${guideCopy.changeTaskLabel}: ${taskText}`,
848        buttonHtml: buildSelectedTaskButtonHtml(taskText || guideCopy.activeTaskFallback),
849        completeSteps: ['priority'],
850        hint: state.isActive
851            ? guideCopy.runningHint
852            : isPaused
853                ? guideCopy.pausedHint
854                : guideCopy.readyHint,
855        kicker: state.isActive
856            ? guideCopy.runningKicker
857            : isPaused
858                ? guideCopy.pausedKicker
859                : guideCopy.readyKicker,
860        recommended: false,
861        selected: true,
862        step: 'focus'
863    };
864}
865
866function getActiveProjects() {
867    return objectifsStore.getPages() || [];
868}
869
870function getSelectedProject() {
871    const projects = getActiveProjects();
872    if (!projects.length) return null;
873    
874    const selectedId = state.settings.selectedProjectId;
875    const project = projects.find(p => p.id === selectedId);
876    return project || projects[0];
877}
878
879function updateProjectSelectorUI() {
880    const roots = [document];
881    if ('documentPictureInPicture' in window && window.documentPictureInPicture.window) {
882        roots.push(window.documentPictureInPicture.window.document);
883    }
884    
885    const currentProj = getSelectedProject();
886    
887    roots.forEach(root => {
888        const iconEl = root.querySelector('#pomo-project-icon');
889        const nameEl = root.querySelector('#pomo-project-name');
890        const iconPipEl = root.querySelector('#pomo-project-icon-pip');
891        
892        if (currentProj) {
893            if (iconEl) iconEl.textContent = currentProj.icon || '📄';
894            if (nameEl) nameEl.textContent = currentProj.title || i18n.t('common_untitled');
895            if (iconPipEl) iconPipEl.textContent = currentProj.icon || '📄';
896        } else {
897            if (iconEl) iconEl.textContent = '📁';
898            if (nameEl) nameEl.textContent = i18n.t('pomo_project_none');
899            if (iconPipEl) iconPipEl.textContent = '📁';
900        }
901    });
902}
903
904function showSingleProjectTooltip(targetEl) {
905    if (!targetEl) return;
906    
907    // Prevent multiple tooltips
908    if (targetEl._hasTooltip) return;
909    targetEl._hasTooltip = true;
910
911    const tooltip = document.createElement('div');
912    tooltip.className = 'pomo-single-project-tooltip';
913    tooltip.textContent = "Vous avez que un seul projet.";
914    
915    // Style tooltip dynamically
916    Object.assign(tooltip.style, {
917        position: 'absolute',
918        background: 'rgba(28, 28, 30, 0.95)',
919        color: '#fff',
920        fontSize: '0.78rem',
921        fontWeight: '600',
922        padding: '6px 10px',
923        borderRadius: '8px',
924        boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
925        whiteSpace: 'nowrap',
926        pointerEvents: 'none',
927        opacity: '0',
928        transform: 'translate(-50%, -100%) translateY(8px)',
929        transition: 'opacity 0.2s ease, transform 0.2s ease',
930        zIndex: '99999'
931    });
932
933    document.body.appendChild(tooltip);
934
935    // Position tooltip relative to targetEl
936    const rect = targetEl.getBoundingClientRect();
937    
938    // Center above the targetEl using translate(-50%, -100%)
939    tooltip.style.left = `${rect.left + window.scrollX + (rect.width / 2)}px`;
940    tooltip.style.top = `${rect.top + window.scrollY - 8}px`;
941
942    // Trigger animate-in
943    requestAnimationFrame(() => {
944        tooltip.style.opacity = '1';
945        tooltip.style.transform = 'translate(-50%, -100%) translateY(0)';
946    });
947
948    // Animate out and remove
949    setTimeout(() => {
950        tooltip.style.opacity = '0';
951        tooltip.style.transform = 'translate(-50%, -100%) translateY(-4px)';
952        const onTransitionEnd = () => {
953            tooltip.removeEventListener('transitionend', onTransitionEnd);
954            tooltip.remove();
955            targetEl._hasTooltip = false;
956        };
957        tooltip.addEventListener('transitionend', onTransitionEnd);
958    }, 2000);
959}
960
961function cycleProject(direction, e) {
962    const projects = getActiveProjects();
963    if (projects.length === 1) {
964        if (e && e.currentTarget) {
965            showSingleProjectTooltip(e.currentTarget);
966        }
967        return;
968    }
969    if (!projects.length) return;
970    
971    const currentProj = getSelectedProject();
972    const currentIndex = projects.findIndex(p => p.id === currentProj.id);
973    
974    let nextIndex = currentIndex + direction;
975    if (nextIndex < 0) nextIndex = projects.length - 1;
976    if (nextIndex >= projects.length) nextIndex = 0;
977    
978    state.settings.selectedProjectId = projects[nextIndex].id;
979    saveSettings();
980    updateProjectSelectorUI();
981}
982
983function syncFocusGuide() {
984    if (!containerEl) return;
985
986    const guideCopy = getFocusGuideCopy();
987    const projectChoice = containerEl.querySelector('#pomo-project-choice');
988    const projectSelector = containerEl.querySelector('#pomo-project-selector');
989    const reviewPanel = containerEl.querySelector('#pomo-session-review');
990    const pendingReview = state.pendingReview;
991
992    if (pendingReview) {
993        if (projectChoice) projectChoice.hidden = true;
994        if (projectSelector) projectSelector.style.display = 'none';
995        if (reviewPanel) reviewPanel.hidden = false;
996
997        const taskText = pendingReview.taskText || guideCopy.activeTaskFallback;
998        const reviewQuestion = containerEl.querySelector('#pomo-review-question');
999        const reviewInput = containerEl.querySelector('#pomo-review-input');
1000        const reviewCta = containerEl.querySelector('#pomo-review-bilan');
1001        const reviewLater = containerEl.querySelector('#pomo-review-later');
1002
1003        if (reviewQuestion) reviewQuestion.textContent = guideCopy.reviewQuestion;
1004        if (reviewInput) {
1005            reviewInput.placeholder = guideCopy.reviewPlaceholder;
1006            if (document.activeElement !== reviewInput) {
1007                reviewInput.value = pendingReview.outcome || '';
1008            }
1009        }
1010        if (reviewCta) reviewCta.querySelector('span').textContent = guideCopy.reviewCta;
1011        if (reviewLater) reviewLater.textContent = guideCopy.reviewLater;
1012        syncSessionReviewStatus();
1013        return;
1014    }
1015
1016    const hasCurrentPriority = Boolean(getCurrentDailyPriority());
1017    if (projectChoice) projectChoice.hidden = hasCurrentPriority;
1018    if (projectSelector) projectSelector.style.display = hasCurrentPriority ? 'none' : 'flex';
1019    if (reviewPanel) reviewPanel.hidden = true;
1020    updateProjectSelectorUI();
1021}
1022
1023function openPilotPriorityPicker() {
1024    pilotPriorityPickerReturnPending = true;
1025    globalCtx?.bus?.emit('app:show-panel', { panelId: 'region-projects' });
1026    document.querySelector('.v2-dock button[data-target="v2-future"]')?.click();
1027    globalCtx?.bus?.emit('ui:toast', i18n.t('pilot_progress_priority_required'));
1028}
1029
1030function returnToPilotFocusAfterPrioritySelection(items = []) {
1031    const pilotState = getTwoDayPilotState();
1032    const currentUserId = String(globalCtx?.currentUser?.id || '');
1033    const ownsPilot = Boolean(currentUserId && pilotState?.ownerId === currentUserId);
1034    const shouldReturn = ownsPilot && shouldReturnToPilotFocus({
1035        pending: pilotPriorityPickerReturnPending,
1036        state: pilotState,
1037        items
1038    });
1039
1040    if (!shouldReturn) return;
1041
1042    pilotPriorityPickerReturnPending = false;
1043    renderTwoDayPilotCard();
1044    document.querySelector('.v2-dock button[data-target="v2-present"]')?.click();
1045    requestAnimationFrame(() => {
1046        containerEl?.querySelector('#pomo-pilot-action')?.focus({ preventScroll: true });
1047    });
1048}
1049
1050function startFiveMinuteFocusSession({
1051    eventAction = 'priority_quick_start_5min',
1052    eventLabel = 'daily_priority'
1053} = {}) {
1054    const current = getCurrentDailyPriority();
1055    if (!current || current.missing || state.isActive) return false;
1056    if (!isFocusMode(state.mode)) setMode('focus', true);
1057
1058    const hasPausedProgress = state.timeLeft > 0 && state.timeLeft < getDuration();
1059    if (!hasPausedProgress) {
1060        state.focusDurationOverrideSeconds = TWO_DAY_PILOT_MIN_FOCUS_SECONDS;
1061        state.timeLeft = TWO_DAY_PILOT_MIN_FOCUS_SECONDS;
1062        state.sessionEndMs = null;
1063        updateUI();
1064        updateControls();
1065        saveTimerState();
1066    }
1067
1068    sendGAEvent(eventAction, 'activation', eventLabel);
1069    startTimer();
1070    return true;
1071}
1072
1073function startExternalFocusSession({ durationMinutes = 25, source = 'link' } = {}) {
1074    const safeDurationMinutes = Math.min(Math.max(Number(durationMinutes) || 25, 1), 180);
1075
1076    if (state.isActive) {
1077        globalCtx?.bus?.emit('ui:toast', i18n.t('pomo_external_focus_already_active'));
1078        return false;
1079    }
1080    if (isDayClosed()) {
1081        globalCtx?.bus?.emit('ui:toast', i18n.t('pomo_alert_closed_desc'));
1082        return false;
1083    }
1084    if (isFocusMode(state.mode) && getCurrentDailyPriority()?.missing) {
1085        globalCtx?.bus?.emit('ui:toast', 'Cette priorité doit être resynchronisée ou retirée avant de lancer un focus.');
1086        return false;
1087    }
1088
1089    const currentDuration = getDuration();
1090    const hasPausedFocus = isFocusMode(state.mode)
1091        && state.timeLeft > 0
1092        && state.timeLeft < currentDuration;
1093
1094    if (!hasPausedFocus) {
1095        if (state.mode !== 'focus') setMode('focus', true);
1096        state.focusDurationOverrideSeconds = safeDurationMinutes * 60;
1097        state.timeLeft = state.focusDurationOverrideSeconds;
1098        state.sessionEndMs = null;
1099        updateModeButtons();
1100        updateUI();
1101        updateControls();
1102        saveTimerState();
1103    }
1104
1105    startTimer();
1106    globalCtx?.bus?.emit(
1107        'ui:toast',
1108        hasPausedFocus
1109            ? i18n.t('pomo_external_focus_resumed')
1110            : i18n.t('pomo_external_focus_started').replace('{minutes}', String(safeDurationMinutes))
1111    );
1112    sendGAEvent(
1113        hasPausedFocus ? 'external_focus_resumed' : 'external_focus_started',
1114        'productivity',
1115        ['nfc', 'qr', 'link'].includes(source) ? source : 'link'
1116    );
1117    return true;
1118}
1119
1120function startPilotFocusSession() {
1121    const current = getCurrentDailyPriority();
1122    if (!current || current.missing) {
1123        openPilotPriorityPicker();
1124        return;
1125    }
1126
1127    const pilotState = getTwoDayPilotState();
1128    startFiveMinuteFocusSession({
1129        eventAction: 'pilot_focus_started',
1130        eventLabel: pilotState?.campaign || 'two_day_pilot'
1131    });
1132    renderTwoDayPilotCard();
1133}
1134
1135function isCompletedPilotResumeTask(task) {
1136    const executionStatus = String(task?.executionStatus ?? task?.execution_status ?? '').toLowerCase();
1137    return Boolean(task?.checked || task?.completed || executionStatus === 'done');
1138}
1139
1140function getClosedDayPilotResumeContext() {
1141    const pilotState = getTwoDayPilotState();
1142    const currentUserId = String(globalCtx?.currentUser?.id || '').trim();
1143    if (
1144        !currentUserId
1145        || !pilotState
1146        || pilotState.ownerId !== currentUserId
1147        || getTwoDayPilotStage(pilotState) !== 'day2'
1148    ) {
1149        return null;
1150    }
1151
1152    const resumeTaskId = String(pilotState.resumeTaskId || '').trim();
1153    if (!resumeTaskId) return null;
1154
1155    const task = resolveTaskReference(resumeTaskId);
1156    if (!task || task.missing || isCompletedPilotResumeTask(task)) return null;
1157
1158    return { pilotState, task };
1159}
1160
1161async function restorePilotTaskAndStart(pilotState) {
1162    const resumeTaskId = String(pilotState?.resumeTaskId || '').trim();
1163    if (
1164        pilotResumeTaskPending
1165        || getTwoDayPilotStage(pilotState) !== 'day2'
1166        || !resumeTaskId
1167    ) {
1168        return false;
1169    }
1170
1171    const task = resolveTaskReference(resumeTaskId);
1172    if (!task || task.missing || isCompletedPilotResumeTask(task)) return false;
1173
1174    pilotResumeTaskPending = true;
1175    const actionButton = containerEl?.querySelector('#pomo-pilot-action');
1176    if (actionButton) actionButton.disabled = true;
1177
1178    try {
1179        const result = await addTaskToToday(task.taskId);
1180        let restored = result?.item;
1181        let current = getCurrentDailyPriority();
1182        if (
1183            restored
1184            && !restored.missing
1185            && current
1186            && !current.missing
1187            && String(current.taskId) !== String(restored.taskId)
1188        ) {
1189            const orderedTaskIds = getTodayPriorities()
1190                .filter((item) => item && !item.missing)
1191                .map((item) => String(item.taskId));
1192            const restoredTaskId = String(restored.taskId);
1193            if (orderedTaskIds.includes(restoredTaskId)) {
1194                await reorderTodayPriorities([
1195                    restoredTaskId,
1196                    ...orderedTaskIds.filter((taskId) => taskId !== restoredTaskId)
1197                ]);
1198                restored = resolveTaskReference(restoredTaskId);
1199                current = getCurrentDailyPriority();
1200            }
1201        }
1202        const restoredIsCurrent = Boolean(
1203            restored
1204            && !restored.missing
1205            && !isCompletedPilotResumeTask(restored)
1206            && current
1207            && !current.missing
1208            && !isCompletedPilotResumeTask(current)
1209            && String(current.taskId) === String(restored.taskId)
1210        );
1211        if (!restoredIsCurrent) return false;
1212
1213        syncDailyPriorityState();
1214
1215        return startFiveMinuteFocusSession({
1216            eventAction: 'pilot_focus_resumed_from_previous_task',
1217            eventLabel: pilotState.campaign || 'two_day_pilot'
1218        });
1219    } catch (error) {
1220        if (!['TASK_NOT_FOUND', 'MAX_PRIORITIES'].includes(error?.code)) {
1221            console.warn('[Pomodoro] Unable to restore the Day 1 task for Day 2.', error);
1222        }
1223        return false;
1224    } finally {
1225        pilotResumeTaskPending = false;
1226    }
1227}
1228
1229const timerTabId = generateUUID();
1230
1231function getAssistedReturnMarkerFromUrl(ownerId) {
1232    try {
1233        const params = new URLSearchParams(window.location.search);
1234        const type = String(params.get(TWO_DAY_PILOT_RETURN_SOURCE_PARAM) || '').trim();
1235        if (!['calendar_reminder', 'human_reminder'].includes(type)) return null;
1236        const now = new Date();
1237        const targetDate = String(params.get(TWO_DAY_PILOT_RETURN_TARGET_PARAM) || '').trim()
1238            || getPilotLocalDate(now);
1239        return buildTwoDayPilotAssistanceMarker({
1240            ownerId,
1241            type,
1242            targetDate,
1243            now
1244        });
1245    } catch (_) {
1246        return null;
1247    }
1248}
1249
1250function clearAssistedReturnParams() {
1251    try {
1252        const url = new URL(window.location.href);
1253        url.searchParams.delete(TWO_DAY_PILOT_RETURN_SOURCE_PARAM);
1254        url.searchParams.delete(TWO_DAY_PILOT_RETURN_TARGET_PARAM);
1255        window.history.replaceState({}, document.title, `${url.pathname}${url.search}${url.hash}`);
1256    } catch (_) {
1257        // The marker is already captured locally/server-side; URL cleanup is cosmetic.
1258    }
1259}
1260
1261async function captureAssistedReturnFromUrl(ownerId) {
1262    const urlMarker = getAssistedReturnMarkerFromUrl(ownerId);
1263    if (!urlMarker) return { marker: null, durable: true };
1264
1265    const pendingMarker = pilotPendingAssistedReturnMarker;
1266    const marker = pendingMarker
1267        && pendingMarker.ownerId === urlMarker.ownerId
1268        && pendingMarker.type === urlMarker.type
1269        && pendingMarker.targetDate === urlMarker.targetDate
1270        ? pendingMarker
1271        : urlMarker;
1272    pilotPendingAssistedReturnMarker = marker;
1273
1274    const captureKey = `${marker.ownerId}:${marker.type}:${marker.targetDate}:${marker.createdAt}`;
1275    if (pilotAssistedReturnCaptureKey === captureKey) {
1276        clearAssistedReturnParams();
1277        return { marker, durable: true };
1278    }
1279
1280    const result = await persistTwoDayPilotAssistance(ownerId, marker);
1281    if (result.durable) {
1282        pilotAssistedReturnCaptureKey = captureKey;
1283        clearAssistedReturnParams();
1284    }
1285    return { marker, durable: result.durable === true };
1286}
1287
1288function recordPilotReminderAssistance(ownerId) {
1289    const marker = buildTwoDayPilotAssistanceMarker({
1290        ownerId,
1291        type: 'calendar_reminder',
1292        now: new Date()
1293    });
1294    if (!marker) return;
1295    void persistTwoDayPilotAssistance(ownerId, marker);
1296}
1297
1298async function runTwoDayPilotServerRefresh(ownerId, {
1299    flushHistory = false,
1300    resumeTaskId = null
1301} = {}, isCurrent = () => true) {
1302    const assistedReturn = await captureAssistedReturnFromUrl(ownerId);
1303    if (!isCurrent()) {
1304        return { state: null, advanced: false, evidence: null, snapshot: null, stale: true };
1305    }
1306    const snapshot = await loadTwoDayPilotServerSnapshot(ownerId, { flushHistory });
1307    if (!isCurrent()) {
1308        return { state: null, advanced: false, evidence: null, snapshot, stale: true };
1309    }
1310
1311    if (!snapshot.ok) {
1312        pilotServerEvidenceReadyOwnerId = null;
1313        renderTwoDayPilotCard();
1314        return { state: null, advanced: false, evidence: null, snapshot };
1315    }
1316
1317    const evidence = buildTwoDayPilotServerEvidence(snapshot.sessions, {
1318        ownerId,
1319        assistance: snapshot.assistance || assistedReturn.marker,
1320        assistanceKnown: snapshot.assistanceKnown && assistedReturn.durable !== false
1321    });
1322    const result = await reconcileTwoDayPilotFromServerEvidenceOnce(evidence, {
1323        resumeTaskId,
1324        now: new Date()
1325    });
1326    pilotServerEvidenceReadyOwnerId = ownerId;
1327    renderTwoDayPilotCard();
1328    globalCtx?.bus?.emit('two-day-pilot:changed');
1329    return { ...result, evidence, snapshot };
1330}
1331
1332function refreshTwoDayPilotFromServer(options = {}) {
1333    const ownerId = String(globalCtx?.currentUser?.id || '').trim();
1334    if (!ownerId) {
1335        pilotServerEvidenceReadyOwnerId = null;
1336        renderTwoDayPilotCard();
1337        return Promise.resolve({ state: null, advanced: false, evidence: null, snapshot: null });
1338    }
1339
1340    // A completed Focus must own the reconciliation that attributes J1/J2.
1341    // Invalidate any background hydration already reading an older snapshot;
1342    // it can finish its network request but cannot update local evidence.
1343    if (options.flushHistory === true) pilotServerRefreshCoordinator.invalidate();
1344
1345    return pilotServerRefreshCoordinator
1346        .request(ownerId, ({ isCurrent }) => (
1347            runTwoDayPilotServerRefresh(ownerId, options, isCurrent)
1348        ))
1349        .catch((error) => {
1350            if (String(globalCtx?.currentUser?.id || '').trim() === ownerId) {
1351                pilotServerEvidenceReadyOwnerId = null;
1352                renderTwoDayPilotCard();
1353            }
1354            console.warn('[Pomodoro] Unable to refresh server-confirmed J1/J2 evidence.', error);
1355            return {
1356                state: null,
1357                advanced: false,
1358                evidence: null,
1359                snapshot: { ok: false, historyPersisted: false, reason: 'snapshot_exception', error }
1360            };
1361        });
1362}
1363
1364function renderTwoDayPilotCard() {
1365    pilotRenderedLocalDate = getPilotLocalDate();
1366    const root = containerEl?.querySelector('#pomo-pilot-progress');
1367    if (!root) return;
1368
1369    const pilotState = getTwoDayPilotState();
1370    const currentUserId = String(globalCtx?.currentUser?.id || '');
1371    if (
1372        !currentUserId
1373        || pilotServerEvidenceReadyOwnerId !== currentUserId
1374        || !pilotState
1375        || pilotState.ownerId !== currentUserId
1376        || pilotState.serverConfirmed !== true
1377    ) {
1378        root.hidden = true;
1379        root.innerHTML = '';
1380        return;
1381    }
1382    const dismissalContextKey = `${currentUserId}:${pilotRenderedLocalDate}`;
1383    if (pilotDismissedContextKey === dismissalContextKey) {
1384        root.hidden = true;
1385        return;
1386    }
1387
1388    // The pilot state is owner-scoped. The general Pomodoro history is not,
1389    // so it must never decide whether another user's J+1 invitation is shown.
1390    const stage = getTwoDayPilotStage(pilotState);
1391
1392    if (stage === 'inactive') {
1393        root.hidden = true;
1394        root.innerHTML = '';
1395        return;
1396    }
1397    const copyByStage = {
1398        day1: {
1399            badge: 'pilot_progress_day1_badge',
1400            title: 'pilot_progress_day1_title',
1401            copy: 'pilot_progress_day1_copy'
1402        },
1403        wait_day2: {
1404            badge: 'pilot_progress_wait_badge',
1405            title: 'pilot_progress_wait_title',
1406            copy: 'pilot_progress_wait_copy'
1407        },
1408        day2: {
1409            badge: 'pilot_progress_day2_badge',
1410            title: 'pilot_progress_day2_title',
1411            copy: 'pilot_progress_day2_copy'
1412        },
1413        complete: {
1414            badge: 'pilot_progress_complete_badge',
1415            title: 'pilot_progress_complete_title',
1416            copy: 'pilot_progress_complete_copy'
1417        }
1418    };
1419    const organicCopyByStage = {
1420        wait_day2: {
1421            badge: 'organic_progress_wait_badge',
1422            title: 'organic_progress_wait_title',
1423            copy: 'organic_progress_wait_copy'
1424        },
1425        day2: {
1426            badge: 'organic_progress_day2_badge',
1427            title: 'organic_progress_day2_title',
1428            copy: 'organic_progress_day2_copy'
1429        },
1430        complete: {
1431            badge: 'organic_progress_complete_badge',
1432            title: 'organic_progress_complete_title',
1433            copy: 'organic_progress_complete_copy'
1434        }
1435    };
1436    const copy = pilotState.campaign === ORGANIC_TWO_DAY_PILOT_CAMPAIGN
1437        ? (organicCopyByStage[stage] || copyByStage[stage])
1438        : copyByStage[stage];
1439    if (!copy) {
1440        root.hidden = true;
1441        root.innerHTML = '';
1442        return;
1443    }
1444
1445    const current = getCurrentDailyPriority();
1446    const actionable = stage === 'day1' || stage === 'day2';
1447    const canResumePreviousTask = stage === 'day2' && Boolean(pilotState.resumeTaskId);
1448    const reminderOptions = stage === 'wait_day2'
1449        ? {
1450            google: buildTwoDayPilotReminderUrl({
1451                title: i18n.t('pilot_progress_reminder_title'),
1452                details: i18n.t('pilot_progress_reminder_details')
1453            }),
1454            calendarFile: buildTwoDayPilotReminderIcsUrl({
1455                title: i18n.t('pilot_progress_reminder_title'),
1456                details: i18n.t('pilot_progress_reminder_details')
1457            })
1458        }
1459        : { google: null, calendarFile: null };
1460    const hasPausedProgress = isFocusMode(state.mode)
1461        && !state.isActive
1462        && state.timeLeft > 0
1463        && state.timeLeft < getDuration();
1464    const actionLabel = (!current || current.missing) && !canResumePreviousTask
1465        ? i18n.t('pilot_progress_choose_cta')
1466        : hasPausedProgress
1467            ? i18n.t('pilot_progress_resume_cta')
1468            : i18n.t('pilot_progress_start_cta');
1469
1470    root.hidden = false;
1471    root.dataset.stage = stage;
1472    root.setAttribute('aria-label', i18n.t('pilot_progress_aria'));
1473    root.innerHTML = `
1474        <button class="pomo-pilot-progress__close" type="button" aria-label="Fermer la notification" title="Fermer">
1475            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
1476        </button>
1477        <div class="pomo-pilot-progress__body">
1478            <span class="pomo-pilot-progress__badge">${escapeHtml(i18n.t(copy.badge))}</span>
1479            <strong>${escapeHtml(i18n.t(copy.title))}</strong>
1480            <p>${escapeHtml(i18n.t(copy.copy))}</p>
1481        </div>
1482        ${actionable
1483            ? (state.isActive
1484                ? `<span class="pomo-pilot-progress__status is-running" aria-label="${escapeHtml(i18n.t('pilot_progress_running'))}">…</span>`
1485                : `<button id="pomo-pilot-action" class="pomo-pilot-progress__action" type="button">${escapeHtml(actionLabel)}</button>`)
1486            : (reminderOptions.google || reminderOptions.calendarFile)
1487                ? `<span class="pomo-pilot-progress__actions">
1488                    ${reminderOptions.google
1489                        ? `<a id="pomo-pilot-reminder" class="pomo-pilot-progress__action pomo-pilot-progress__action--secondary" href="${escapeHtml(reminderOptions.google)}" target="_blank" rel="noopener noreferrer">${escapeHtml(i18n.t('pilot_progress_reminder_google_cta'))}</a>`
1490                        : ''}
1491                    ${reminderOptions.calendarFile
1492                        ? `<a id="pomo-pilot-reminder-file" class="pomo-pilot-progress__calendar-file" href="${escapeHtml(reminderOptions.calendarFile)}" download="roxima-focus-jour-2.ics" type="text/calendar">${escapeHtml(i18n.t('pilot_progress_reminder_file_cta'))}</a>`
1493                        : ''}
1494                </span>`
1495            : '<span class="pomo-pilot-progress__status" aria-hidden="true">✓</span>'}
1496    `;
1497
1498    root.querySelector('.pomo-pilot-progress__close')?.addEventListener('click', (e) => {
1499        e.stopPropagation();
1500        pilotDismissedContextKey = dismissalContextKey;
1501        root.hidden = true;
1502    });
1503
1504    root.querySelector('#pomo-pilot-action')?.addEventListener('click', async () => {
1505        if (pilotResumeTaskPending) return;
1506        if (!current || current.missing) {
1507            if (await restorePilotTaskAndStart(pilotState)) {
1508                renderTwoDayPilotCard();
1509                return;
1510            }
1511            openPilotPriorityPicker();
1512            return;
1513        }
1514        startPilotFocusSession();
1515    });
1516
1517    root.querySelector('#pomo-pilot-reminder')?.addEventListener('click', () => {
1518        recordPilotReminderAssistance(currentUserId);
1519        sendGAEvent('pilot_day2_reminder_opened', 'activation', `${pilotState.campaign || 'two_day_pilot'}:google`);
1520    });
1521
1522    root.querySelector('#pomo-pilot-reminder-file')?.addEventListener('click', () => {
1523        recordPilotReminderAssistance(currentUserId);
1524        sendGAEvent('pilot_day2_reminder_opened', 'activation', `${pilotState.campaign || 'two_day_pilot'}:ics`);
1525    });
1526}
1527
1528function refreshTwoDayPilotCardForCurrentDay(now = new Date()) {
1529    if (!shouldRefreshTwoDayPilotForNewDay(pilotRenderedLocalDate, now)) return false;
1530
1531    // A dismissal belongs to one account and one local day. If Roxima stays
1532    // open overnight, the Day 2 invitation must become visible again.
1533    pilotDismissedContextKey = null;
1534    pilotRenderedLocalDate = getPilotLocalDate(now);
1535    renderTwoDayPilotCard();
1536    return true;
1537}
1538
1539function renderDailyPriorityCard() {
1540    const root = containerEl?.querySelector('#pomo-daily-priority');
1541    if (!root) return;
1542    if (dailyCompletionFeedbackActive && dailyActionPending) return;
1543    const focusedControlId = root.contains(document.activeElement) ? document.activeElement.id : null;
1544    const restoreControlId = focusedControlId || dailyRestoreFocusId;
1545    const restoreControlFocus = () => {
1546        if (!restoreControlId) return;
1547        const nextControl = root.querySelector(`#${CSS.escape(restoreControlId)}`);
1548        nextControl?.focus?.();
1549        if (dailyRestoreFocusId === restoreControlId) dailyRestoreFocusId = null;
1550    };
1551
1552    const priorities = getTodayPriorities();
1553    const dayProgress = getDayProgressSnapshot(priorities, { syncPlan: true });
1554    const dayProgressMarkup = buildDayProgressMarkup(dayProgress);
1555    const current = priorities[0] || null;
1556
1557    if (!current) {
1558        dailyPrioritySequence = [];
1559        root.innerHTML = `
1560            <div class="pomo-daily-empty">
1561                ${dayProgressMarkup}
1562                <strong>${escapeHtml(i18n.t('pomo_daily_empty_title'))}</strong>
1563                <p>${escapeHtml(i18n.t('pomo_daily_empty_copy'))}</p>
1564                <button id="pomo-choose-daily-priority" class="pomo-daily-secondary pomo-daily-secondary--primary" type="button">
1565                    <span>${escapeHtml(i18n.t('pomo_daily_open_project'))}</span>
1566                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
1567                        <path d="M5 12h14"></path>
1568                        <path d="m13 6 6 6-6 6"></path>
1569                    </svg>
1570                </button>
1571            </div>
1572        `;
1573        root.querySelector('#pomo-choose-daily-priority')?.addEventListener('click', () => {
1574            globalCtx?.bus?.emit('app:show-panel', { panelId: 'region-projects' });
1575            document.querySelector('.v2-dock button[data-target="v2-future"]')?.click();
1576        });
1577        restoreControlFocus();
1578        return;
1579    }
1580
1581    if (current.missing) {
1582        root.innerHTML = `
1583            <div class="pomo-daily-empty is-unavailable">
1584                ${dayProgressMarkup}
1585                <strong>${escapeHtml(i18n.t('pomo_daily_missing_title'))}</strong>
1586                <p>${escapeHtml(i18n.t('pomo_daily_missing_copy'))}</p>
1587                <button id="pomo-open-missing-priority" class="pomo-daily-secondary" type="button">${escapeHtml(i18n.t('pomo_daily_open_project'))}</button>
1588            </div>
1589        `;
1590        root.querySelector('#pomo-open-missing-priority')?.addEventListener('click', () => {
1591            globalCtx?.bus?.emit('app:show-panel', { panelId: 'region-projects' });
1592            document.querySelector('.v2-dock button[data-target="v2-future"]')?.click();
1593        });
1594        restoreControlFocus();
1595        return;
1596    }
1597
1598    const priority = getGeneralPriorityPresentation(current.generalPriority);
1599    const breakActive = state.isActive && !isFocusMode(state.mode);
1600    const actionBusyAttributes = dailyActionPending
1601        ? ' disabled aria-busy="true"'
1602        : (breakActive ? ' disabled' : '');
1603
1604    const rawTitle = current.text || current.title || i18n.t('daily_priority_untitled_task');
1605    const cleanTitle = rawTitle.replace(/\s*-\s*\d+\s*min\b/gi, '').replace(/\s*\(\d+\s*min\)/gi, '').trim() || rawTitle;
1606    const projectIcon = current.projectIcon || '📄';
1607    const currentPosition = getDailyPriorityPosition(priorities, current.taskId);
1608    const guideCopy = getFocusGuideCopy();
1609    const canQuickStart = !state.isActive
1610        && isFocusMode(state.mode)
1611        && state.timeLeft === getDuration()
1612        && !dailyActionPending;
1613    const navigationDisabledAttributes = priorities.length <= 1
1614        ? ' disabled'
1615        : actionBusyAttributes;
1616
1617    const flagSvg = `<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line></svg>`;
1618    const previousSvg = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="lucide lucide-chevron-left"><path d="m15 18-6-6 6-6"></path></svg>`;
1619    const nextSvg = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="lucide lucide-chevron-right"><path d="m9 18 6-6-6-6"></path></svg>`;
1620    const quickStartSvg = `<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m13 2-9 12h7l-1 8 9-12h-7l1-8Z"></path></svg>`;
1621
1622    root.innerHTML = `
1623        <div class="pomo-daily-current">
1624            ${dayProgressMarkup}
1625            <div class="pomo-daily-heading">
1626                <div class="pomo-daily-context">
1627                    <span class="pomo-daily-label">${escapeHtml(i18n.t('pomo_daily_priority_label'))}</span>
1628                    <span class="pomo-daily-celebration" role="status" aria-live="polite" hidden>${escapeHtml(i18n.t('pomo_daily_celebration'))}</span>
1629                </div>
1630                <div class="pomo-daily-controls">
1631                    <div class="pomo-daily-navigation" aria-label="${escapeHtml(i18n.t('pomo_daily_navigation'))}">
1632                        <span class="pomo-daily-counter" aria-label="${escapeHtml(translatePomodoro('pomo_daily_counter_aria', { current: currentPosition, total: priorities.length }))}">${escapeHtml(translatePomodoro('pomo_daily_counter', { current: currentPosition, total: priorities.length }))}</span>
1633                        <button id="pomo-previous-current-priority" class="pomo-daily-nav-button" type="button" aria-label="${escapeHtml(i18n.t('pomo_daily_previous'))}" title="${escapeHtml(i18n.t('pomo_daily_previous'))}"${navigationDisabledAttributes}>${previousSvg}</button>
1634                        <button id="pomo-next-current-priority" class="pomo-daily-nav-button" type="button" aria-label="${escapeHtml(i18n.t('pomo_daily_next'))}" title="${escapeHtml(i18n.t('pomo_daily_next'))}"${navigationDisabledAttributes}>${nextSvg}</button>
1635                    </div>
1636                    ${priority ? `<span class="pomo-daily-flag ${priority.className}" title="${escapeHtml(translatePomodoro('pomo_daily_priority_title', { priority: priority.title }))}">${flagSvg}</span>` : ''}
1637                </div>
1638            </div>
1639            <div class="pomo-daily-task-line">
1640                <input id="pomo-complete-current-priority" class="pomo-daily-complete" type="checkbox" aria-label="${escapeHtml(translatePomodoro('pomo_daily_complete_aria', { task: cleanTitle }))}" title="${escapeHtml(i18n.t('pomo_daily_complete_title'))}"${actionBusyAttributes}>
1641                <button class="pomo-daily-title" id="pomo-open-current-priority" type="button" title="${escapeHtml(i18n.t('pomo_daily_open_title'))}">
1642                    <span class="pomo-daily-project-icon" aria-hidden="true">${escapeHtml(projectIcon)}</span><span>${escapeHtml(cleanTitle)}</span>
1643                </button>
1644                ${canQuickStart ? `
1645                    <button
1646                        id="pomo-quick-start-current-priority"
1647                        class="pomo-daily-quick-start"
1648                        type="button"
1649                        aria-label="${escapeHtml(guideCopy.quickStartAria.replace('{task}', cleanTitle))}"
1650                        title="${escapeHtml(guideCopy.quickStartTitle)}"
1651                    >
1652                        ${quickStartSvg}
1653                        <span>${escapeHtml(guideCopy.quickStartAction)}</span>
1654                    </button>
1655                ` : ''}
1656            </div>
1657        </div>
1658    `;
1659
1660    const runAsyncAction = async (button, action, successMessage) => {
1661        if (!button || button.disabled || dailyActionPending) return;
1662        dailyActionPending = true;
1663        dailyRestoreFocusId = button.id || null;
1664        button.disabled = true;
1665        button.setAttribute('aria-busy', 'true');
1666        const preserveCompletionCheck = button.classList?.contains('pomo-daily-complete');
1667        if (preserveCompletionCheck) {
1668            dailyCompletionFeedbackActive = true;
1669            root.querySelector('.pomo-daily-current')?.classList.add('is-completing');
1670            const celebration = root.querySelector('.pomo-daily-celebration');
1671            if (celebration) celebration.hidden = false;
1672            root.querySelectorAll('.pomo-daily-nav-button').forEach((navigationButton) => {
1673                navigationButton.disabled = true;
1674            });
1675        } else {
1676            renderDailyPriorityCard();
1677        }
1678        try {
1679            await action();
1680            if (successMessage) globalCtx?.bus?.emit('ui:toast', successMessage);
1681        } catch (error) {
1682            console.error('[Daily priorities]', error);
1683            globalCtx?.bus?.emit('ui:toast', error?.message || "L'action n'a pas pu être enregistrée. Réessayez.");
1684        } finally {
1685            dailyActionPending = false;
1686            dailyCompletionFeedbackActive = false;
1687            if (button.isConnected) {
1688                button.disabled = false;
1689                button.removeAttribute('aria-busy');
1690            }
1691            renderDailyPriorityCard();
1692        }
1693    };
1694
1695    root.querySelector('#pomo-open-current-priority')?.addEventListener('click', () => {
1696        globalCtx?.bus?.emit('daily-priority:open-source', { taskId: current.taskId });
1697    });
1698
1699    root.querySelector('#pomo-quick-start-current-priority')?.addEventListener('click', () => {
1700        startFiveMinuteFocusSession();
1701    });
1702
1703    const completeButton = root.querySelector('#pomo-complete-current-priority');
1704    completeButton?.addEventListener('change', () => {
1705        if (!completeButton.checked) return;
1706        const celebrationStartedAt = Date.now();
1707        runAsyncAction(
1708            completeButton,
1709            async () => {
1710                if (state.isActive) pauseTimer();
1711                await completeCurrentPriority();
1712                markDayProgressTaskCompleted(current);
1713                const remainingCelebrationMs = Math.max(0, COMPLETION_CELEBRATION_MS - (Date.now() - celebrationStartedAt));
1714                if (remainingCelebrationMs > 0) {
1715                    await new Promise((resolve) => window.setTimeout(resolve, remainingCelebrationMs));
1716                }
1717                resetTimer();
1718            },
1719            'Tâche terminée. La priorité suivante est prête.'
1720        );
1721    });
1722
1723    const navigatePriority = (button, direction) => runAsyncAction(
1724        button,
1725        async () => {
1726            if (state.isActive) pauseTimer();
1727            const orderedTaskIds = direction === 'previous'
1728                ? [priorities.at(-1).taskId, ...priorities.slice(0, -1).map((item) => item.taskId)]
1729                : [...priorities.slice(1).map((item) => item.taskId), priorities[0].taskId];
1730            await reorderTodayPriorities(orderedTaskIds);
1731            resetTimer();
1732        },
1733        direction === 'previous' ? 'Tâche précédente activée.' : 'Tâche suivante activée.'
1734    );
1735
1736    const previousButton = root.querySelector('#pomo-previous-current-priority');
1737    previousButton?.addEventListener('click', () => navigatePriority(previousButton, 'previous'));
1738
1739    const nextButton = root.querySelector('#pomo-next-current-priority');
1740    nextButton?.addEventListener('click', () => navigatePriority(nextButton, 'next'));
1741    restoreControlFocus();
1742}
1743
1744function syncDailyPriorityState({ preserveTimer = true } = {}) {
1745    const current = getCurrentDailyPriority();
1746    const previousDuration = getDuration();
1747    const wasIdle = !state.isActive && state.timeLeft === previousDuration;
1748
1749    if (!current) {
1750        const hadDailyPriority = Boolean(state.activeTask?.dailyPriority);
1751        if (hadDailyPriority && state.isActive && isFocusMode(state.mode)) pauseTimer();
1752        if (hadDailyPriority) {
1753            state.activeTask = null;
1754            state.focusDurationOverrideSeconds = null;
1755            if (isFocusMode(state.mode)) resetTimer();
1756        }
1757        renderDailyPriorityCard();
1758        syncFocusGuide();
1759        updateControls();
1760        saveTimerState();
1761        return;
1762    }
1763
1764    if (current.missing) {
1765        const hadDailyPriority = Boolean(state.activeTask?.dailyPriority);
1766        if (hadDailyPriority && state.isActive && isFocusMode(state.mode)) pauseTimer();
1767        state.activeTask = null;
1768        state.focusDurationOverrideSeconds = null;
1769        if (hadDailyPriority && isFocusMode(state.mode)) resetTimer();
1770        renderDailyPriorityCard();
1771        syncFocusGuide();
1772        updateControls();
1773        saveTimerState();
1774        return;
1775    }
1776
1777    const changedTask = String(state.activeTask?.taskId || '') !== String(current.taskId);
1778    const changedWhileActive = changedTask && state.isActive && isFocusMode(state.mode);
1779    if (changedWhileActive) pauseTimer();
1780    if (changedTask) state.focusDurationOverrideSeconds = null;
1781    state.activeTask = {
1782        ...current,
1783        id: current.sourceBlockId || current.taskId,
1784        text: current.text || current.title,
1785        dailyPriority: true
1786    };
1787
1788    if (current.sourcePageId && state.settings.selectedProjectId !== current.sourcePageId) {
1789        state.settings.selectedProjectId = current.sourcePageId;
1790        saveSettings();
1791    }
1792
1793    const durationChanged = previousDuration !== getDuration();
1794    if (isFocusMode(state.mode) && (
1795        changedWhileActive ||
1796        changedTask ||
1797        (!state.isActive && wasIdle && durationChanged)
1798    )) {
1799        state.timeLeft = getDuration();
1800        state.sessionEndMs = null;
1801        updateUI();
1802    }
1803
1804    renderDailyPriorityCard();
1805    syncFocusGuide();
1806    updateControls();
1807    saveTimerState();
1808}
1809
1810function syncSessionReviewStatus() {
1811    if (!containerEl || !state.pendingReview) return;
1812
1813    const guideCopy = getFocusGuideCopy();
1814    const helper = containerEl.querySelector('#pomo-review-helper');
1815    const count = containerEl.querySelector('#pomo-review-count');
1816    const outcomeLength = String(state.pendingReview.outcome || '').length;
1817
1818    if (helper) {
1819        helper.textContent = state.pendingReview.saveError
1820            ? guideCopy.reviewError
1821            : state.pendingReview.saved
1822                ? guideCopy.reviewSaved
1823                : guideCopy.reviewHelper;
1824        helper.classList.toggle('is-error', Boolean(state.pendingReview.saveError));
1825    }
1826    if (count) count.textContent = `${outcomeLength}/180`;
1827}
1828
1829export function mount(container, ctx) {
1830    containerEl = container;
1831    globalCtx = ctx;
1832    pomodoroStore.setBus(ctx.bus);
1833    const mountedOwnerId = String(ctx.currentUser?.id || '').trim();
1834    if (pilotServerRefreshOwnerId !== mountedOwnerId) {
1835        pilotServerRefreshOwnerId = mountedOwnerId || null;
1836        pilotServerRefreshCoordinator.invalidate();
1837        pilotAssistedReturnCaptureKey = null;
1838        pilotPendingAssistedReturnMarker = null;
1839    }
1840    if (ctx.currentUser?.id) {
1841        claimTwoDayPilotForUser(ctx.currentUser.id);
1842        pilotServerEvidenceReadyOwnerId = null;
1843        void refreshTwoDayPilotFromServer();
1844    }
1845
1846    if (!browserFeedbackListenerBound) {
1847        window.addEventListener(ROXIMA_BROWSER_FEEDBACK_EVENT, (event) => {
1848            handleBrowserFeedback(event?.detail);
1849        });
1850        browserFeedbackListenerBound = true;
1851    }
1852
1853    if (!timerStorageListenerBound) {
1854        window.addEventListener('storage', handlePomodoroTimerStorageEvent);
1855        timerStorageListenerBound = true;
1856    }
1857
1858    // Load Styles
1859    const link = document.createElement('link');
1860    link.rel = 'stylesheet';
1861    link.href = new URL('./styles.css?v=37', import.meta.url).href;
1862    document.head.appendChild(link);
1863
1864    if (!document.querySelector('link[data-pomo-ambience-styles]')) {
1865        const ambienceLink = document.createElement('link');
1866        ambienceLink.rel = 'stylesheet';
1867        ambienceLink.dataset.pomoAmbienceStyles = 'true';
1868        ambienceLink.href = new URL('./ambience.css?v=13', import.meta.url).href;
1869        document.head.appendChild(ambienceLink);
1870    }
1871
1872    focusAmbience = createFocusAmbience({
1873        getSnapshot: () => ({
1874            mode: state.mode,
1875            timeLeft: state.timeLeft,
1876            duration: getDuration(),
1877            timeText: formatTimerText(state.timeLeft),
1878            isActive: state.isActive,
1879            taskText: state.activeTask?.text || getCurrentDailyPriority()?.text || '',
1880            completedFocusCount: getTodayStats().focusCount
1881        }),
1882        getLanguage: () => i18n.getLang(),
1883        onToggleTimer: toggleTimer,
1884        onReset: resetTimer,
1885        onModeChange: (mode) => setMode(mode, true),
1886        onOpen: () => {
1887            sendGAEvent('focus_ambience_opened', 'productivity', state.mode);
1888            globalCtx?.bus?.emit('pomodoro:ambience-state', { open: true });
1889        },
1890        onClose: () => globalCtx?.bus?.emit('pomodoro:ambience-state', { open: false })
1891    });
1892
1893    loadSettings();
1894    renderLayout();
1895    bindEvents(ctx);
1896    
1897    if (!isDayClosed()) {
1898        bindSettingsEvents();
1899        restorePersistedCustomAlarm();
1900        // Restore first: resetting the default mode here would overwrite an
1901        // active timer started in another Roxima tab.
1902        if (!restoreTimerState()) setMode(state.mode, false);
1903        syncDailyPriorityState();
1904        renderTwoDayPilotCard();
1905    }
1906
1907    if (globalCtx.bus) {
1908        globalCtx.bus.on('pomodoro:external-start', startExternalFocusSession);
1909        globalCtx.bus.on('i18n:changed', () => {
1910            // Re-render layout but keep state
1911            // Not doing full re-render to preserve input focus and timer, just update texts
1912            updateI18nTexts();
1913            renderTwoDayPilotCard();
1914            updateProjectSelectorUI();
1915        });
1916        globalCtx.bus.on('two-day-pilot:changed', renderTwoDayPilotCard);
1917    }
1918
1919    // Periodically refresh ETA when timer is inactive to keep it aligned with actual time
1920    setInterval(() => {
1921        if (refreshDayClosureForCurrentDate()) return;
1922        refreshTwoDayPilotCardForCurrentDay();
1923        if (containerEl && !state.isActive) {
1924            updateUI();
1925        }
1926    }, 10000);
1927
1928    document.addEventListener('keydown', (event) => {
1929        if (!shouldHandlePomodoroStartShortcut(event, {
1930            shortcut: state.settings.startShortcut,
1931            blocked: isPomodoroKeyboardShortcutBlocked()
1932        })) return;
1933
1934        event.preventDefault();
1935        toggleTimer();
1936        sendGAEvent('pomodoro_keyboard_shortcut_used', 'productivity', state.settings.startShortcut);
1937    });
1938
1939    // Global Keyboard Shortcut: Double Press Shift (2x ⇧) to toggle PiP mode
1940    let lastShiftTime = 0;
1941    document.addEventListener('keydown', (e) => {
1942        if (isDayClosed()) return;
1943        
1944        if (e.key === 'Shift') {
1945            const currentTime = Date.now();
1946            if (currentTime - lastShiftTime < 500) { // 500ms window (generous margin of error)
1947                e.preventDefault();
1948                if (window.documentPictureInPicture && window.documentPictureInPicture.window) {
1949                    window.documentPictureInPicture.window.close();
1950                } else {
1951                    requestPiP();
1952                }
1953                lastShiftTime = 0;
1954            } else {
1955                lastShiftTime = currentTime;
1956            }
1957        } else {
1958            // Any other key resets the double press sequence
1959            lastShiftTime = 0;
1960        }
1961    });
1962}
1963
1964function updateI18nTexts() {
1965    if (!containerEl) return;
1966    // Modes
1967    renderModeOptions();
1968    
1969    // Settings title
1970    const settingsTitle = containerEl.querySelector('.pomo-settings-title');
1971    if (settingsTitle) settingsTitle.textContent = i18n.t('pomo_settings_title');
1972
1973    const modeBadge = containerEl.querySelector('#pomo-mode-badge');
1974    syncModeBadge(modeBadge, state.mode);
1975    
1976    // Settings Labels
1977    const labels = containerEl.querySelectorAll('.pomo-label');
1978    if (labels.length > 0) {
1979        labels[0].textContent = i18n.t('pomo_focus_min');
1980        labels[1].textContent = i18n.t('pomo_short_min');
1981        labels[2].textContent = i18n.t('pomo_long_min');
1982        labels[3].textContent = i18n.t('pomo_long_interval');
1983        labels[4].textContent = i18n.t('pomo_auto_breaks');
1984        labels[5].textContent = i18n.t('pomo_auto_pomo');
1985        labels[6].textContent = i18n.t('pomo_daily_goal');
1986        labels[7].textContent = i18n.t('pomo_alarm');
1987        // Volume label needs special care because of the span
1988    }
1989    
1990    // Preview btn
1991    const previewBtn = containerEl.querySelector('#preview-custom-btn');
1992    if (previewBtn) previewBtn.textContent = i18n.t('pomo_custom_preview');
1993
1994    const shortcutLabel = containerEl.querySelector('.pomo-shortcut-label');
1995    if (shortcutLabel) shortcutLabel.textContent = i18n.t('pomo_start_shortcut');
1996    const shortcutHelp = containerEl.querySelector('.pomo-shortcut-sett
1996ing-help');
1997    if (shortcutHelp) shortcutHelp.textContent = i18n.t('pomo_start_shortcut_help');
1998    const shortcutSelect = containerEl.querySelector('#set-start-shortcut');
1999    if (shortcutSelect) shortcutSelect.innerHTML = renderStartShortcutOptionsMarkup();
2000    updateControls();
2001    
2002    const priorityStep = containerEl.querySelector('[data-pomo-guide-step="priority"]');
2003    const focusStep = containerEl.querySelector('[data-pomo-guide-step="focus"]');
2004    const reviewStep = containerEl.querySelector('[data-pomo-guide-step="review"]');
2005    const guideCopy = getFocusGuideCopy();
2006    if (priorityStep) priorityStep.textContent = guideCopy.priority;
2007    if (focusStep) focusStep.textContent = guideCopy.focus;
2008    if (reviewStep) reviewStep.textContent = guideCopy.review;
2009
2010    const pipTitle = containerEl.querySelector('#pomo-pip-info-title');
2011    if (pipTitle) pipTitle.textContent = i18n.t('pomo_pip_info_title');
2012    const pipDesc = containerEl.querySelector('#pomo-pip-info-desc');
2013    if (pipDesc) pipDesc.textContent = i18n.t('pomo_pip_info_desc');
2014
2015    const closedStatus = containerEl.querySelector('[data-pomo-day-closed-time]');
2016    if (closedStatus) {
2017        closedStatus.textContent = i18n.t('pomo_day_closed_status')
2018            .replace('{time}', closedStatus.dataset.pomoDayClosedTime || '');
2019    }
2020
2021    syncFocusGuide();
2022    updateControls();
2023}
2024
2025function loadSettings() {
2026    try {
2027        const saved = pomodoroStore.getSettings();
2028        if (saved) {
2029            state.settings = { ...state.settings, ...saved };
2030            Object.keys(NUMERIC_SETTING_RULES).forEach((key) => {
2031                state.settings[key] = normalizeNumericSetting(key, state.settings[key]);
2032            });
2033            state.settings.customTimers = normalizeCustomTimers(state.settings.customTimers);
2034            state.settings.customTimerCategories = normalizeCustomTimerCategories(state.settings.customTimerCategories);
2035            state.settings.startShortcut = normalizePomodoroStartShortcut(state.settings.startShortcut);
2036
2037            // Adjust current state if needed (re-init timer if idle)
2038            if (!state.isActive) {
2039                // We don't force reset here to avoid annoying jumps, but next reset will use new times
2040            }
2041        }
2042    } catch (e) {
2043        console.error("Failed to load settings", e);
2044    }
2045}
2046
2047function saveSettings() {
2048    try {
2049        pomodoroStore.updateSettings(state.settings);
2050    } catch (e) {
2051        console.warn("Failed to save settings", e);
2052    }
2053}
2054
2055function renderLayout() {
2056    const guideCopy = getFocusGuideCopy();
2057    const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
2058    const shortcutLabel = '2x ⇧';
2059
2060    if (isDayClosed()) {
2061        let closureTime = '';
2062        let closureNote = '';
2063        const closedDayResume = getClosedDayPilotResumeContext();
2064        const closedDescriptionKey = closedDayResume
2065            ? 'pomo_day_closed_resume_desc'
2066            : 'pomo_day_closed_desc';
2067        const closedActionKey = closedDayResume
2068            ? 'pomo_btn_resume_day2'
2069            : 'pomo_btn_reopen';
2070        try {
2071            const parsed = pomodoroStore.getDayClosure();
2072            if (parsed) {
2073                closureTime = new Date(parsed.closedAt).toLocaleTimeString(i18n.getLang() === 'fr' ? 'fr-FR' : 'en-US', { hour: '2-digit', minute: '2-digit' });
2074                closureNote = parsed.shutdownNote || '';
2075            }
2076        } catch (e) {}
2077
2078        containerEl.innerHTML = `
2079            <div class="card" style="height: 100%; display:flex; flex-direction:column; position:relative; overflow:hidden;">
2080                 <div class="card-header">
2081                    <div style="display:flex; align-items:center; gap:8px;">
2082                        <h2 class="card-title" data-i18n="pomo_title">Pomodoro</h2>
2083                        <span class="badge" style="background: rgba(16, 185, 129, 0.1); color: #10B981; border: 1px solid rgba(16, 185, 129, 0.2);" data-i18n="pomo_day_closed_title">${i18n.t('pomo_day_closed_title')}</span>
2084                    </div>
2085                 </div>
2086                 <div class="pomo-container pomo-closed-panel" style="justify-content: center; gap: 16px; padding: 24px; text-align: center; height:100%; box-sizing:border-box;">
2087                     <div style="width: 72px; height: 72px; border-radius: 50%; background: rgba(16, 185, 129, 0.1); color: #10B981; display: flex; align-items: center; justify-content: center; margin: 0 auto 8px;">
2088                         <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
2089                             <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
2090                             <polyline points="22 4 12 14.01 9 11.01"></polyline>
2091                         </svg>
2092                     </div>
2093                     
2094                     <h3 style="margin: 0; font-size: 1.25rem; font-weight: 700; color: var(--text-main);" data-i18n="pomo_day_closed_title">${i18n.t('pomo_day_closed_title')}</h3>
2095                     <p style="margin: 0; font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; max-width: 280px; margin: 0 auto;" data-i18n="${closedDescriptionKey}">${i18n.t(closedDescriptionKey)}</p>
2096
2097                     ${closedDayResume ? `
2098                     <div class="pomo-closed-resume-task" aria-label="${escapeHtml(i18n.t('pomo_day_closed_resume_task_aria'))}">
2099                         <span data-i18n="pomo_day_closed_resume_task_label">${i18n.t('pomo_day_closed_resume_task_label')}</span>
2100                         <strong>${escapeHtml(closedDayResume.task.text || closedDayResume.task.title || '')}</strong>
2101                         ${closedDayResume.task.projectName
2102                             ? `<small>${escapeHtml(closedDayResume.task.projectName)}</small>`
2103                             : ''}
2104                     </div>
2105                     ` : ''}
2106                     
2107                     <div style="margin-top: 12px; font-size: 0.8rem; color: var(--text-muted);">
2108                         <span data-pomo-day-closed-time="${escapeHtml(closureTime)}">${i18n.t('pomo_day_closed_status').replace('{time}', closureTime)}</span>
2109                     </div>
2110                     
2111                     ${closureNote ? `
2112                     <div style="margin-top: 16px; padding: 12px; background: rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.03); border-radius: 12px; text-align: left; font-size: 0.85rem; max-width: 320px; width: 100%; box-sizing: border-box; margin-left: auto; margin-right: auto;">
2113                         <div style="font-weight: 600; color: var(--text-main); margin-bottom: 4px;" data-i18n="pomo_modal_closure_q">${i18n.t('pomo_modal_closure_q')}</div>
2114                         <div style="color: #555; font-style: italic; word-break: break-word;">${escapeHtml(closureNote)}</div>
2115                     </div>
2116                     ` : ''}
2117                     
2118                     <button class="pomo-reopen-btn${closedDayResume ? ' pomo-reopen-btn--resume' : ''}" id="pomo-btn-reopen" type="button" data-pomo-resume-day2="${closedDayResume ? 'true' : 'false'}" data-i18n="${closedActionKey}">
2119                         ${i18n.t(closedActionKey)}
2120                     </button>
2121                 </div>
2122            </div>
2123        `;
2124        return;
2125    }
2126
2127    containerEl.innerHTML = `
2128        <div class="card" style="height: 100%; display:flex; flex-direction:column; position:relative; overflow:hidden;">
2129             <div class="card-header" style="position: relative;">
2130                <div style="display:flex; align-items:center; gap:8px;">
2131                    <h2 class="card-title" data-i18n="pomo_title">Pomodoro</h2>
2132                </div>
2133                <div style="position: absolute; left: 50%; transform: translateX(-50%);">
2134                    <button class="pomo-end-day-btn" id="pomo-end-day-btn" type="button" data-i18n-aria-label="pomo_end_day_hold_aria" aria-label="${escapeHtml(i18n.t('pomo_end_day_hold_aria'))}">
2135                        <div class="pomo-end-day-progress" id="pomo-end-day-progress"></div>
2136                        <span class="pomo-end-day-text" data-i18n="pomo_btn_day_closure">${i18n.t('pomo_btn_day_closure')}</span>
2137                    </button>
2138                </div>
2139                <div class="card-actions" style="display:flex; align-items:center; gap:4px;">
2140                    <span class="pomo-shortcut-hint" style="font-size: 0.65rem; color: var(--text-muted); opacity: 0.65; font-weight: 500; user-select: none; margin-right: 2px;">${shortcutLabel}</span>
2141                    <button class="icon-btn" id="pomo-btn-pip" type="button" data-i18n-title="pomo_float" data-i18n-aria-label="pomo_float" title="${i18n.t('pomo_float')}" aria-label="${i18n.t('pomo_float')}">
2142                        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h9"></path><rect x="15" y="5" width="6" height="6" rx="1" ry="1"></rect></svg>
2143                    </button>
2144                    <button class="icon-btn" id="pomo-btn-settings" type="button" data-i18n-title="common_settings" data-i18n-aria-label="common_settings" title="${i18n.t('common_settings')}" aria-label="${i18n.t('common_settings')}">
2145                        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 
21450 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
2146                    </button>
2147                </div>
2148             </div>
2149             
2150             <div class="pomo-container">
2151                    <div class="pomo-ring" id="pomo-ring">
2152                        <svg class="pomo-ring-svg" viewBox="0 0 100 100" style="position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); pointer-events: none; z-index: 1;">
2153                            <circle cx="50" cy="50" r="25" style="fill: none; stroke: #E5E5EA; stroke-width: 50;"></circle>
2154                            <circle cx="50" cy="50" r="25" id="pomo-progress-circle" style="fill: none; stroke: #10B981; stroke-width: 50; stroke-dasharray: 157.08; stroke-dashoffset: 157.08; transition: stroke-dashoffset 1s linear, stroke 0.3s ease;"></circle>
2155                        </svg>
2156                        <div class="pomo-time" id="pomo-time">25:00</div>
2157                        <div class="pomo-eta" id="pomo-eta">
2158                            <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;">
2159                                <path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/>
2160                                <path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>
2161                            </svg>
2162                            <span id="pomo-eta-time">--:--</span>
2163                        </div>
2164                    </div>
2165  
2166                  <div class="pomo-controls">
2167                      <button class="btn-pomo filled" id="pomo-play-btn" type="button" aria-label="${escapeHtml(guideCopy.chooseAction)}">
2168                          <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path d="M8 5v14l11-7z" /></svg>
2169                      </button>
2170                      <button class="btn-pomo secondary" id="pomo-reset-btn" type="button" data-i18n-aria-label="pomo_reset" aria-label="${i18n.t('pomo_reset')}">
2171                          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
2172                      </button>
2173                  </div>
2174                  
2175                  <div class="pomo-modes" aria-label="${escapeHtml(i18n.t('pomo_timer_modes_aria'))}">
2176                      ${renderModeOptionsMarkup()}
2177                  </div>
2178             </div>
2179
2180             <div id="pomo-focus-task-area" class="pomo-focus-task-area">
2181                 <section id="pomo-pilot-progress" class="pomo-pilot-progress" aria-live="polite" hidden></section>
2182                 <section id="pomo-daily-priority" class="pomo-daily-priority" aria-live="polite" data-i18n-aria-label="pomo_focus_task_aria" aria-label="${escapeHtml(i18n.t('pomo_focus_task_aria'))}"></section>
2183                 <div id="pomo-project-choice" class="pomo-project-choice">
2184                     <span class="pomo-project-choice__label">${escapeHtml(i18n.t('pomo_project_session_choice'))}</span>
2185                     <div id="pomo-project-selector" class="pomo-project-selector">
2186                         <button id="pomo-project-prev" class="pomo-project-arrow-btn" type="button" data-i18n-aria-label="pomo_project_previous" aria-label="${escapeHtml(i18n.t('pomo_project_previous'))}">
2187                             <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
2188                                 <polyline points="15 18 9 12 15 6"></polyline>
2189                             </svg>
2190                         </button>
2191                         <div class="pomo-project-display">
2192                             <span id="pomo-project-icon" class="pomo-project-icon">📂</span>
2193                             <span id="pomo-project-name" class="pomo-project-name">${escapeHtml(i18n.t('pomo_project_none'))}</span>
2194                         </div>
2195                         <button id="pomo-project-next" class="pomo-project-arrow-btn" type="button" data-i18n-aria-label="pomo_project_next" aria-label="${escapeHtml(i18n.t('pomo_project_next'))}">
2196                             <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
2197                                 <polyline points="9 18 15 12 9 6"></polyline>
2198                             </svg>
2199                         </button>
2200                     </div>
2201                 </div>
2202                  <div id="pomo-session-review" class="pomo-session-review" hidden aria-live="polite">
2203                       <label id="pomo-review-question" for="pomo-review-input" class="pomo-session-review-question">${escapeHtml(guideCopy.reviewQuestion)}</label>
2204                       <textarea id="pomo-review-input" maxlength="180" rows="2" placeholder="${escapeHtml(guideCopy.reviewPlaceholder)}"></textarea>
2205                       <div class="pomo-session-review-meta">
2206                           <span id="pomo-review-helper">${escapeHtml(guideCopy.reviewHelper)}</span>
2207                           <span id="pomo-review-count">0/180</span>
2208                       </div>
2209                       <div class="pomo-session-review-actions">
2210                           <button id="pomo-review-bilan" class="pomo-session-review-primary" type="button">
2211                                <span>${escapeHtml(guideCopy.reviewCta)}</span>
2212                                <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
2213                                    <path d="M5 12h14"></path>
2214                                    <path d="m13 6 6 6-6 6"></path>
2215                                </svg>
2216                           </button>
2217                           <button id="pomo-review-later" class="pomo-session-review-secondary" type="button">${escapeHtml(guideCopy.reviewLater)}</button>
2218                        </div>
2219                   </div>
2220               </div>
2221 
2222
2223                 <div id="pomo-project-selector-pip" class="pomo-project-selector-pip">
2224                     <button id="pomo-project-prev-pip" class="pomo-project-pip-arrow-btn" type="button" data-i18n-aria-label="pomo_project_previous" aria-label="${escapeHtml(i18n.t('pomo_project_previous'))}">
2225                         <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
2226                             <polyline points="18 15 12 9 6 15"></polyline>
2227                         </svg>
2228                     </button>
2229                     <span id="pomo-project-icon-pip" class="pomo-project-pip-icon">📂</span>
2230                     <button id="pomo-project-next-pip" class="pomo-project-pip-arrow-btn" type="button" data-i18n-aria-label="pomo_project_next" aria-label="${escapeHtml(i18n.t('pomo_project_next'))}">
2231                         <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
2232                             <polyline points="6 9 12 15 18 9"></polyline>
2233                         </svg>
2234                     </button>
2235                 </div>
2236             </div>
2237
2238             <div class="pomo-custom-timer-layer" id="pomo-custom-timer-layer" hidden aria-hidden="true">
2239                 <button class="pomo-custom-timer-backdrop" type="button" data-custom-timer-close tabindex="-1" aria-hidden="true"></button>
2240                 <section class="pomo-custom-timer-dialog" role="dialog" aria-modal="true" aria-labelledby="pomo-custom-timer-title">
2241                     <form id="pomo-custom-timer-form" novalidate>
2242                         <div class="pomo-custom-timer-header">
2243                             <div>
2244                                 <span class="pomo-custom-timer-kicker" data-i18n="pomo_custom_timer_kicker">${i18n.t('pomo_custom_timer_kicker')}</span>
2245                                 <h3 id="pomo-custom-timer-title" data-i18n="pomo_custom_timer_create_title">${i18n.t('pomo_custom_timer_create_title')}</h3>
2246                             </div>
2247                             <button class="pomo-custom-timer-close" type="button" data-custom-timer-close aria-label="${escapeHtml(i18n.t('common_close'))}">
2248                                 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"></path></svg>
2249                             </button>
2250                         </div>
2251                         <div class="pomo-custom-timer-body">
2252                             <label class="pomo-custom-timer-field" for="pomo-custom-timer-name">
2253                                 <span data-i18n="pomo_custom_timer_name">${i18n.t('pomo_custom_timer_name')}</span>
2254                                 <input id="pomo-custom-timer-name" name="name" type="text" maxlength="${CUSTOM_TIMER_LIMITS.maxNameLength}" autocomplete="off" data-i18n-placeholder="pomo_custom_timer_name_placeholder" placeholder="${escapeHtml(i18n.t('pomo_custom_timer_name_placeholder'))}">
2255                             </label>
2256                             <label class="pomo-custom-timer-field" for="pomo-custom-timer-minutes">
2257                                 <span data-i18n="pomo_custom_timer_minutes">${i18n.t('pomo_custom_timer_minutes')}</span>
2258                                 <div class="pomo-custom-timer-duration">
2259                                     <input id="pomo-custom-timer-minutes" name="minutes" type="number" inputmode="numeric" min="${CUSTOM_TIMER_LIMITS.minMinutes}" max="${CUSTOM_TIMER_LIMITS.maxMinutes}" step="1" value="25">
2260                                     <span data-i18n="pomo_custom_timer_min_unit">${i18n.t('pomo_custom_timer_min_unit')}</span>
2261                                 </div>
2262                             </label>
2263                             <fieldset class="pomo-custom-timer-category">
2264                                 <legend data-i18n="pomo_custom_timer_category">${i18n.t('pomo_custom_timer_category')}</legend>
2265                                 <div class="pomo-custom-timer-category-options" id="pomo-custom-timer-category-options">
2266                                     ${renderTimerCategoryOptionsMarkup('focus')}
2267                                 </div>
2268                                 <button class="pomo-custom-category-toggle" type="button" data-custom-category-toggle>
2269                                     <span aria-hidden="true">+</span>
2270                                     <span data-i18n="pomo_custom_category_new">${i18n.t('pomo_custom_category_new')}</span>
2271                                 </button>
2272                                 <div class="pomo-custom-category-creator" id="pomo-custom-category-creator" hidden>
2273                                     <div class="pomo-custom-category-emoji-field">
2274                                         <span data-i18n="pomo_custom_category_emoji">${i18n.t('pomo_custom_category_emoji')}</span>
2275                                         ${renderCustomCategoryEmojiPickerMarkup()}
2276                                     </div>
2277                                     <label class="pomo-custom-category-name-field">
2278                                         <span data-i18n="pomo_custom_category_name">${i18n.t('pomo_custom_category_name')}</span>
2279                                         <input id="pomo-custom-category-name" type="text" maxlength="${CUSTOM_TIMER_CATEGORY_LIMITS.maxNameLength}" autocomplete="off" data-i18n-placeholder="pomo_custom_category_name_placeholder" placeholder="${escapeHtml(i18n.t('pomo_custom_category_name_placeholder'))}">
2280                                     </label>
2281                                     <button class="pomo-custom-category-add" type="button" data-custom-category-add data-i18n="pomo_custom_category_add">${i18n.t('pomo_custom_category_add')}</button>
2282                                     <div class="pomo-custom-category-color-field">
2283                                         <span data-i18n="pomo_custom_category_color">${i18n.t('pomo_custom_category_color')}</span>
2284                                         ${renderCustomCategoryColorPickerMarkup()}
2285                                     </div>
2286                                 </div>
2287                             </fieldset>
2288                             <p class="pomo-custom-timer-note" data-i18n="pomo_custom_timer_focus_note">${i18n.t('pomo_custom_timer_focus_note')}</p>
2289                             <p class="pomo-custom-timer-error" id="pomo-custom-timer-error" role="alert" hidden></p>
2290                         </div>
2291                         <div class="pomo-custom-timer-actions">
2292                             <button class="pomo-custom-timer-delete" id="pomo-custom-timer-delete" type="button" data-i18n="pomo_custom_timer_delete" hidden>${i18n.t('pomo_custom_timer_delete')}</button>
2293                             <span class="pomo-custom-timer-actions-spacer"></span>
2294                             <button class="pomo-custom-timer-cancel" type="button" data-custom-timer-close data-i18n="common_cancel">${i18n.t('common_cancel')}</button>
2295                             <button class="pomo-custom-timer-submit" id="pomo-custom-timer-submit" type="submit" data-i18n="pomo_custom_timer_create">${i18n.t('pomo_custom_timer_create')}</button>
2296                         </div>
2297                     </form>
2298                 </section>
2299             </div>
2300 
2301             <!-- Settings Panel -->
2302             <div class="pomo-settings-panel" id="pomo-settings-panel">
2303                 <div class="pomo-settings-header">
2304                     <div class="pomo-settings-title">${i18n.t('pomo_settings_title')}</div>
2305                     <button class="icon-btn" id="pomo-settings-close" type="button" data-i18n-aria-label="common_close" aria-label="${i18n.t('common_close')}"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="2.5" y1="2.5" x2="9.5" y2="9.5"></line><line x1="9.5" y1="2.5" x2="2.5" y2="9.5"></line></svg></button>
2306                 </div>
2307                 <div class="pomo-settings-body">
2308                     
2309                     <!-- SECTION TIMER -->
2310                     <div class="pomo-section">
2311                         <div class="pomo-section-title">${i18n.t('pomo_timer_section')}</div>
2312                         
2313                         <div class="pomo-form-group">
2314                             <label class="pomo-label">${i18n.t('pomo_focus_min')}</label>
2315                             <input type="number" class="pomo-input" id="set-focus-time" value="${state.settings.focusTime}" min="1" max="180">
2316                         </div>
2317                         <div class="pomo-form-group">
2318                             <label class="pomo-label">${i18n.t('pomo_short_min')}</label>
2319                             <input type="number" class="pomo-input" id="set-short-break" value="${state.settings.shortBreakTime}" min="1" max="60">
2320                         </div>
2321                         <div class="pomo-form-group">
2322                             <label class="pomo-label">${i18n.t('pomo_long_min')}</label>
2323                             <input type="number" class="pomo-input" id="set-long-break" value="${state.settings.longBreakTime}" min="1" max="180">
2324                         </div>
2325                          <div class="pomo-form-group pomo-shortcut-sett
2325ing">
2326                              <div class="pomo-setting-copy">
2327                                  <label class="pomo-shortcut-label" for="set-start-shortcut">${i18n.t('pomo_start_shortcut')}</label>
2328                                  <p class="pomo-shortcut-setting-help">${i18n.t('pomo_start_shortcut_help')}</p>
2329                              </div>
2330                              <select class="pomo-select pomo-shortcut-select" id="set-start-shortcut">
2331                                  ${renderStartShortcutOptionsMarkup()}
2332                              </select>
2333                          </div>
2334                          <div class="pomo-form-group pomo-form-group-with-help">
2335                             <div class="pomo-setting-copy">
2336                                 <div class="pomo-setting-label-row">
2337                                     <label class="pomo-label" for="set-long-interval">${i18n.t('pomo_long_interval')}</label>
2338                                     <button class="pomo-help-toggle" type="button"
2339                                         data-help-target="pomo-help-long-interval"
2340                                         aria-expanded="false"
2341                                         aria-controls="pomo-help-long-interval"
2342                                         data-i18n-aria-label="pomo_long_interval_help_label"
2343                                         aria-label="${i18n.t('pomo_long_interval_help_label')}">
2344                                         <span aria-hidden="true">i</span>
2345                                     </button>
2346                                 </div>
2347                                 <p class="pomo-setting-help" id="pomo-help-long-interval" data-i18n="pomo_long_interval_help" hidden>${i18n.t('pomo_long_interval_help')}</p>
2348                             </div>
2349                             <input type="number" class="pomo-input" id="set-long-interval" value="${state.settings.longBreakInterval}" min="1" max="10">
2350                         </div>
2351                         
2352                         <div class="pomo-form-group pomo-form-group-with-help">
2353                             <div class="pomo-setting-copy">
2354                                 <div class="pomo-setting-label-row">
2355                                     <label class="pomo-label" for="toggle-auto-break">${i18n.t('pomo_auto_breaks')}</label>
2356                                     <button class="pomo-help-toggle" type="button"
2357                                         data-help-target="pomo-help-auto-breaks"
2358                                         aria-expanded="false"
2359                                         aria-controls="pomo-help-auto-breaks"
2360                                         data-i18n-aria-label="pomo_auto_breaks_help_label"
2361                                         aria-label="${i18n.t('pomo_auto_breaks_help_label')}">
2362                                         <span aria-hidden="true">i</span>
2363                                     </button>
2364                                 </div>
2365                                 <p class="pomo-setting-help" id="pomo-help-auto-breaks" data-i18n="pomo_auto_breaks_help" hidden>${i18n.t('pomo_auto_breaks_help')}</p>
2366                             </div>
2367                             <button class="pomo-toggle ${state.settings.autoStartBreaks ? 'active' : ''}" id="toggle-auto-break" type="button" role="switch" aria-checked="${state.settings.autoStartBreaks}" data-i18n-aria-label="pomo_auto_breaks" aria-label="${i18n.t('pomo_auto_breaks')}">
2368                                 <span class="pomo-toggle-handle" aria-hidden="true"></span>
2369                             </button>
2370                         </div>
2371                          <div class="pomo-form-group pomo-form-group-with-help">
2372                             <div class="pomo-setting-copy">
2373                                 <div class="pomo-setting-label-row">
2374                                     <label class="pomo-label" for="toggle-auto-pomo">${i18n.t('pomo_auto_pomo')}</label>
2375                                     <button class="pomo-help-toggle" type="button"
2376                                         data-help-target="pomo-help-auto-pomo"
2377                                         aria-expanded="false"
2378                                         aria-controls="pomo-help-auto-pomo"
2379                                         data-i18n-aria-label="pomo_auto_pomo_help_label"
2380                                         aria-label="${i18n.t('pomo_auto_pomo_help_label')}">
2381                                         <span aria-hidden="true">i</span>
2382                                     </button>
2383                                 </div>
2384                                 <p class="pomo-setting-help" id="pomo-help-auto-pomo" data-i18n="pomo_auto_pomo_help" hidden>${i18n.t('pomo_auto_pomo_help')}</p>
2385                             </div>
2386                             <button class="pomo-toggle ${state.settings.autoStartPomo ? 'active' : ''}" id="toggle-auto-pomo" type="button" role="switch" aria-checked="${state.settings.autoStartPomo}" data-i18n-aria-label="pomo_auto_pomo" aria-label="${i18n.t('pomo_auto_pomo')}">
2387                                 <span class="pomo-toggle-handle" aria-hidden="true"></span>
2388                             </button>
2389                         </div>
2390                     </div>
2391 
2392                     <!-- SECTION GOAL -->
2393                     <div class="pomo-section">
2394                         <div class="pomo-section-title">${i18n.t('pomo_goal_section')}</div>
2395                         <div class="pomo-form-group">
2396                             <label class="pomo-label">${i18n.t('pomo_daily_goal')}</label>
2397                              <input type="number" class="pomo-input" id="set-daily-goal" value="${state.settings.dailyGoalMin}" min="10" step="10">
2398                         </div>
2399                     </div>
2400
2401                     <!-- SECTION SLEEP GOALS -->
2402                     <div class="pomo-section">
2403                         <div class="pomo-section-title">Objectif Sommeil</div>
2404                         <div class="pomo-form-group">
2405                             <label class="pomo-label">Coucher</label>
2406                             <input type="time" class="pomo-input" id="set-sleep-bedtime" value="${state.settings.sleepBedtime || '23:00'}">
2407                         </div>
2408                         <div class="pomo-form-group" style="margin-top: 10px;">
2409                             <label class="pomo-label">Réveil</label>
2410                             <input type="time" class="pomo-input" id="set-sleep-wakeup" value="${state.settings.sleepWakeup || '07:00'}">
2411                         </div>
2412                     </div>
2413 
2414                     <!-- SECTION SOUND -->
2415                     <div class="pomo-section">
2416                         <div class="pomo-section-title">${i18n.t('pomo_sound_section')}</div>
2417                         
2418                         <div class="pomo-form-group">
2419                             <label class="pomo-label">${i18n.t('pomo_alarm')}</label>
2420                             <select class="pomo-select" id="set-alarm-sound">
2421                                 <option value="none">${i18n.t('pomo_sound_none')}</option>
2422                                 <option value="bell">${i18n.t('pomo_sound_bell')}</option>
2423                                 <option value="chime">${i18n.t('pomo_sound_chime')}</option>
2424                                 <option value="digital">${i18n.t('pomo_sound_digital')}</option>
2425                                 <option value="custom" ${state.settings.alarmSound === 'custom' ? 'selected' : ''}>
2426                                     ${state.settings.customSoundName ? i18n.t('pomo_sound_custom') + ' (' + state.settings.customSoundName + ')' : i18n.t('pomo_sound_custom')}
2427                                 </option>
2428                             </select>
2429                         </div>
2430 
2431                          <div class="pomo-form-group-volume">
2432                              <label class="pomo-label">${i18n.t('pomo_volume')}: <span id="vol-display">${state.settings.volume}%</span></label>
2433                              <input type="range" class="pomo-slider" id="set-volume" min="0" max="100" value="${state.settings.volume}">
2434                          </div>
2435
2436                          <div class="pomo-custom-sound-box">
2437                              <label class="pomo-custom-sound-btn">
2438                                  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
2439                                  ${i18n.t('pomo_custom_file')}
2440                                  <input type="file" id="upload-alarm" accept="audio/mpeg,audio/mp3" style="display:none;">
2441                              </label>
2442                              <div id="upload-info" class="pomo-custom-sound-info">
2443                                  ${state.settings.customSoundName || 'Max 10MB'}
2444                              </div>
2445                              <div id="custom-sound-actions" class="pomo-custom-sound-actions">
2446                                  <button class="pomo-custom-sound-preview-btn" id="preview-custom-btn" type="button">${i18n.t('pomo_custom_preview')}</button>
2447                              </div>
2448                          </div>
2449 
2450                     </div>
2451 
2452                     <!-- SECTION PIP INFO -->
2453                     <div class="pomo-section" style="border-top: 1px solid #f3f4f6; padding-top: 12px; margin-top: 12px;">
2454                         <div class="pomo-section-title" id="pomo-pip-info-title">${i18n.t('pomo_pip_info_title')}</div>
2455                         <div class="pomo-pip-tip-text" id="pomo-pip-info-desc" style="font-size: 0.75rem; line-height: 1.4; color: #666; margin-top: 6px;">
2456                             ${i18n.t('pomo_pip_info_desc')}
2457                         </div>
2458                     </div>
2459 
2460                 </div>
2461             </div>
2462         </div>
2463     `;
2464 
2465     ringEl = containerEl.querySelector('#pomo-ring');
2466     timeEl = containerEl.querySelector('#pomo-time');
2467     playBtn = containerEl.querySelector('#pomo-play-btn');
2468     resetBtn = containerEl.querySelector('#pomo-reset-btn');
2469     settingsPanel = containerEl.querySelector('#pomo-settings-panel');
2470     syncSettingsPanelVisibility();
2471 }
2472
2473function renderStartShortcutOptionsMarkup() {
2474    const options = [
2475        ['space', 'pomo_shortcut_space'],
2476        ['keyP', 'pomo_shortcut_key_p'],
2477        ['keyF', 'pomo_shortcut_key_f'],
2478        ['disabled', 'pomo_shortcut_disabled']
2479    ];
2480    const selectedShortcut = normalizePomodoroStartShortcut(state.settings.startShortcut);
2481
2482    return options.map(([value, labelKey]) => (
2483        `<option value="${value}" ${selectedShortcut === value ? 'selected' : ''}>${i18n.t(labelKey)}</option>`
2484    )).join('');
2485}
2486
2487function syncCustomAlarmSettingsUI() {
2488    if (!containerEl) return;
2489
2490    const name = state.settings.customSoundName;
2491    const alarmSelect = containerEl.querySelector('#set-alarm-sound');
2492    const customOption = alarmSelect?.querySelector('option[value="custom"]');
2493    const uploadInfo = containerEl.querySelector('#upload-info');
2494
2495    if (customOption) {
2496        customOption.textContent = name
2497            ? `${i18n.t('pomo_sound_custom')} (${name})`
2498            : i18n.t('pomo_sound_custom');
2499    }
2500    if (uploadInfo && name) uploadInfo.textContent = name;
2501    if (alarmSelect) alarmSelect.value = state.settings.alarmSound;
2502}
2503
2504async function restorePersistedCustomAlarm() {
2505    try {
2506        const saved = await audioDB.getCustomAlarm(state.settings.customSoundId);
2507        if (!saved?.blob) return null;
2508
2509        let restored = saved;
2510        if (saved.legacy) {
2511            restored = await audioDB.saveCustomAlarm(saved.blob, {
2512                selected: state.settings.alarmSound === 'custom',
2513                name: saved.name
2514            });
2515        }
2516
2517        const changed = state.settings.customSoundId !== restored.id
2518            || state.settings.customSoundName !== restored.name;
2519        state.settings.customSoundId = restored.id;
2520        state.settings.customSoundName = restored.name;
2521
2522        if (changed) saveSettings();
2523        syncCustomAlarmSettingsUI();
2524        return restored;
2525    } catch (error) {
2526        console.warn('Failed to restore custom Pomodoro alarm', error);
2527        return null;
2528    }
2529}
2530
2531function renderModeOptions() {
2532    const roots = [containerEl];
2533    if ('documentPictureInPicture' in window && window.documentPictureInPicture.window) {
2534        roots.push(window.documentPictureInPicture.window.document);
2535    }
2536
2537    roots.forEach((root) => {
2538        const modes = root?.querySelector?.('.pomo-modes');
2539        if (!modes) return;
2540        modes.setAttribute('aria-label', i18n.t('pomo_timer_modes_aria'));
2541        modes.innerHTML = renderModeOptionsMarkup();
2542        bindModeButtons(modes);
2543    });
2544    updateModeButtons();
2545}
2546
2547function renderCustomTimerCategoryOptions(selectedCategory = 'focus') {
2548    const options = containerEl?.querySelector('#pomo-custom-timer-category-options');
2549    if (!options) return;
2550    options.innerHTML = renderTimerCategoryOptionsMarkup(selectedCategory);
2551}
2552
2553function showCustomTimerError(message, field = null) {
2554    const error = containerEl?.querySelector('#pomo-custom-timer-error');
2555    if (error) {
2556        error.textContent = message;
2557        error.hidden = false;
2558    }
2559    field?.setAttribute('aria-invalid', 'true');
2560    field?.focus();
2561}
2562
2563function clearCustomTimerError() {
2564    const error = containerEl?.querySelector('#pomo-custom-timer-error');
2565    if (error) {
2566        error.textContent = '';
2567        error.hidden = true;
2568    }
2569    containerEl?.querySelectorAll('#pomo-custom-timer-form [aria-invalid="true"]').forEach((field) => {
2570        field.removeAttribute('aria-invalid');
2571    });
2572}
2573
2574function openCustomTimerEditor(timerId = null, trigger = null) {
2575    const timers = getCustomTimers();
2576    const timer = timerId ? timers.find((item) => item.id === timerId) : null;
2577    if (timerId && !timer) return;
2578    if (!timer && timers.length >= CUSTOM_TIMER_LIMITS.maxCount) {
2579        globalCtx?.bus?.emit('ui:toast', i18n.t('pomo_custom_timer_limit_reached'));
2580        return;
2581    }
2582
2583    const layer = containerEl?.querySelector('#pomo-custom-timer-layer');
2584    const nameInput = containerEl?.querySelector('#pomo-custom-timer-name');
2585    const minutesInput = containerEl?.querySelector('#pomo-custom-timer-minutes');
2586    const title = containerEl?.querySelector('#pomo-custom-timer-title');
2587    const submit = containerEl?.querySelector('#pomo-custom-timer-submit');
2588    const deleteButton = containerEl?.querySelector('#pomo-custom-timer-delete');
2589    if (!layer || !nameInput || !minutesInput || !title || !submit || !deleteButton) return;
2590
2591    customTimerEditorId = timer?.id || null;
2592    customTimerEditorReturnFocus = trigger || document.activeElement;
2593    clearCustomTimerError();
2594
2595    const titleKey = timer ? 'pomo_custom_timer_edit_title' : 'pomo_custom_timer_create_title';
2596    const submitKey = timer ? 'pomo_custom_timer_save' : 'pomo_custom_timer_create';
2597    title.dataset.i18n = titleKey;
2598    title.textContent = i18n.t(titleKey);
2599    submit.dataset.i18n = submitKey;
2600    submit.textContent = i18n.t(submitKey);
2601    deleteButton.hidden = !timer;
2602    nameInput.value = timer?.name || '';
2603    minutesInput.value = String(timer?.minutes || 25);
2604    const selectedCategory = timer?.category || 'focus';
2605    renderCustomTimerCategoryOptions(selectedCategory);
2606
2607    const categoryCreator = containerEl?.querySelector('#pomo-custom-category-creator');
2608    if (categoryCreator) {
2609        categoryCreator.hidden = true;
2610        setCustomCategoryEmoji(
2611            categoryCreator.querySelector('[data-custom-category-emoji-picker]'),
2612            DEFAULT_CUSTOM_TIMER_CATEGORY_EMOJI
2613        );
2614        setCustomCategoryColor(categoryCreator, DEFAULT_CUSTOM_TIMER_CATEGORY_COLOR);
2615    }
2616
2617    layer.hidden = false;
2618    layer.setAttribute('aria-hidden', 'false');
2619    window.setTimeout(() => {
2620        nameInput.focus();
2621        if (timer) nameInput.select();
2622    }, 0);
2623}
2624
2625function closeCustomTimerEditor() {
2626    const layer = containerEl?.querySelector('#pomo-custom-timer-layer');
2627    if (!layer || layer.hidden) return;
2628
2629    layer.hidden = true;
2630    layer.setAttribute('aria-hidden', 'true');
2631    clearCustomTimerError();
2632    customTimerEditorId = null;
2633
2634    const returnFocus = customTimerEditorReturnFocus;
2635    customTimerEditorReturnFocus = null;
2636    if (returnFocus?.isConnected) returnFocus.focus();
2637}
2638
2639function persistCustomTimers(timers) {
2640    state.settings.customTimers = normalizeCustomTimers(timers);
2641    saveSettings();
2642    renderModeOptions();
2643}
2644
2645function persistCustomTimerCategories(categories) {
2646    state.settings.customTimerCategories = normalizeCustomTimerCategories(categories);
2647    saveSettings();
2648    renderModeOptions();
2649}
2650
2651function bindCustomTimerEditorEvents() {
2652    const layer = containerEl?.querySelector('#pomo-custom-timer-layer');
2653    const form = containerEl?.querySelector('#pomo-custom-timer-form');
2654    const deleteButton = containerEl?.querySelector('#pomo-custom-timer-delete');
2655    if (!layer || !form || !deleteButton) return;
2656
2657    const categoryCreator = layer.querySelector('#pomo-custom-category-creator');
2658    const categoryNameInput = layer.querySelector('#pomo-custom-category-name');
2659    const categoryEmojiInput = layer.querySelector('#pomo-custom-category-emoji');
2660    const categoryColorInput = layer.querySelector('#pomo-custom-category-color');
2661    const categoryEmojiPicker = layer.querySelector('[data-custom-category-emoji-picker]');
2662    const categoryEmojiManualInput = layer.querySelector('[data-custom-category-emoji-manual]');
2663
2664    categoryEmojiPicker?.querySelectorAll('[data-custom-category-emoji-option]').forEach((button) => {
2665        button.addEventListener('click', () => {
2666            setCustomCategoryEmoji(categoryEmojiPicker, button.dataset.customCategoryEmojiOption, { close: true });
2667            categoryEmojiPicker.querySelector('summary')?.focus();
2668        });
2669    });
2670    categoryEmojiManualInput?.addEventListener('input', () => {
2671        setCustomCategoryEmoji(categoryEmojiPicker, categoryEmojiManualInput.value);
2672    });
2673    categoryEmojiManualInput?.addEventListener('keydown', (event) => {
2674        if (event.key !== 'Enter') return;
2675        event.preventDefault();
2676        if (setCustomCategoryEmoji(categoryEmojiPicker, categoryEmojiManualInput.value, { close: true })) {
2677            categoryEmojiPicker.querySelector('summary')?.focus();
2678        }
2679    });
2680    layer.querySelectorAll('[data-custom-category-color-option]').forEach((button) => {
2681        button.addEventListener('click', () => {
2682            setCustomCategoryColor(categoryCreator, button.dataset.customCategoryColorOption);
2683        });
2684    });
2685
2686    layer.querySelector('[data-custom-category-toggle]')?.addEventListener('click', () => {
2687        if (!categoryCreator) return;
2688        const opening = categoryCreator.hidden;
2689        categoryCreator.hidden = !opening;
2690        if (!opening && categoryEmojiPicker) categoryEmojiPicker.open = false;
2691        if (opening) categoryNameInput?.focus();
2692    });
2693
2694    layer.querySelector('[data-custom-category-add]')?.addEventListener('click', () => {
2695        clearCustomTimerError();
2696        const categories = getCustomTimerCategories();
2697        if (categories.length >= CUSTOM_TIMER_CATEGORY_LIMITS.maxCount) {
2698            showCustomTimerError(i18n.t('pomo_custom_category_limit'), categoryNameInput);
2699            return;
2700        }
2701
2702        const name = normalizeCustomTimerCategoryName(categoryNameInput?.value);
2703        const emoji = normalizeCustomTimerCategoryEmoji(categoryEmojiInput?.value);
2704        const color = normalizeCustomTimerCategoryColor(categoryColorInput?.value);
2705        if (!name) {
2706            showCustomTimerError(i18n.t('pomo_custom_category_name_required'), categoryNameInput);
2707            return;
2708        }
2709        if (!emoji) {
2710            showCustomTimerError(i18n.t('pomo_custom_category_emoji_required'), categoryEmojiInput);
2711            return;
2712        }
2713
2714        const category = {
2715            id: `${CUSTOM_TIMER_CATEGORY_ID_PREFIX}${generateUUID()}`,
2716            name,
2717            emoji,
2718            color
2719        };
2720        persistCustomTimerCategories(upsertCustomTimerCategory(categories, category));
2721        renderCustomTimerCategoryOptions(category.id);
2722        if (categoryNameInput) categoryNameInput.value = '';
2723        setCustomCategoryEmoji(categoryEmojiPicker, DEFAULT_CUSTOM_TIMER_CATEGORY_EMOJI);
2724        setCustomCategoryColor(categoryCreator, DEFAULT_CUSTOM_TIMER_CATEGORY_COLOR);
2725        if (categoryCreator) categoryCreator.hidden = true;
2726    });
2727
2728    layer.querySelectorAll('[data-custom-timer-close]').forEach((button) => {
2729        button.addEventListener('click', closeCustomTimerEditor);
2730    });
2731    layer.addEventListener('keydown', (event) => {
2732        if (event.key !== 'Escape') return;
2733        event.preventDefault();
2734        if (categoryEmojiPicker?.open) {
2735            categoryEmojiPicker.open = false;
2736            categoryEmojiPicker.querySelector('summary')?.focus();
2737            return;
2738        }
2739        closeCustomTimerEditor();
2740    });
2741    layer.addEventListener('click', (event) => {
2742        if (categoryEmojiPicker?.open && !event.target.closest('[data-custom-category-emoji-picker]')) {
2743            categoryEmojiPicker.open = false;
2744        }
2745    });
2746
2747    form.addEventListener('submit', (event) => {
2748        event.preventDefault();
2749        clearCustomTimerError();
2750
2751        const nameInput = form.elements.namedItem('name');
2752        const minutesInput = form.elements.namedItem('minutes');
2753        const categoryInput = form.elements.namedItem('category');
2754        const name = normalizeCustomTimerName(nameInput?.value);
2755        const minutes = normalizeCustomTimerMinutes(minutesInput?.value);
2756        const category = normalizeCustomTimerCategory(categoryInput?.value);
2757
2758        if (!name) {
2759            showCustomTimerError(i18n.t('pomo_custom_timer_name_required'), nameInput);
2760            return;
2761        }
2762        if (minutes === null) {
2763            showCustomTimerError(
2764                i18n.t('pomo_custom_timer_minutes_invalid')
2765                    .replace('{min}', CUSTOM_TIMER_LIMITS.minMinutes)
2766                    .replace('{max}', CUSTOM_TIMER_LIMITS.maxMinutes),
2767                minutesInput
2768            );
2769            return;
2770        }
2771        if (!category) {
2772            showCustomTimerError(i18n.t('pomo_custom_timer_category_required'), form.querySelector('input[name="category"]'));
2773            return;
2774        }
2775
2776        const timerId = customTimerEditorId || `timer-${generateUUID()}`;
2777        const mode = `${CUSTOM_MODE_PREFIX}${timerId}`;
2778        const wasEditing = Boolean(customTimerEditorId);
2779        const timers = upsertCustomTimer(getCustomTimers(), { id: timerId, name, minutes, category });
2780
2781        persistCustomTimers(timers);
2782        closeCustomTimerEditor();
2783        if (!wasEditing || state.mode === mode) {
2784            setMode(mode, true);
2785        } else {
2786            updateModeButtons();
2787        }
2788    });
2789
2790    deleteButton.addEventListener('click', () => {
2791        const timer = customTimerEditorId
2792            ? getCustomTimers().find((item) => item.id === customTimerEditorId)
2793            : null;
2794        if (!timer) return;
2795
2796        const confirmation = i18n.t('pomo_custom_timer_delete_confirm').replace('{name}', timer.name);
2797        if (!window.confirm(confirmation)) return;
2798
2799        const deletedMode = `${CUSTOM_MODE_PREFIX}${timer.id}`;
2800        persistCustomTimers(removeCustomTimer(getCustomTimers(), timer.id));
2801        closeCustomTimerEditor();
2802        if (state.mode === deletedMode) {
2803            setMode('focus', true);
2804        }
2805    });
2806}
2807
2808function syncSettingsPanelVisibility() {
2809    if (!settingsPanel) return;
2810
2811    settingsPanel.classList.toggle('open', state.settingsOpen);
2812    settingsPanel.setAttribute('aria-hidden', String(!state.settingsOpen));
2813    settingsPanel.inert = !state.settingsOpen;
2814
2815    settingsPanel.querySelectorAll(SETTINGS_FOCUSABLE_SELECTOR).forEach((element) => {
2816        if (!state.settingsOpen) {
2817            if (!element.hasAttribute(SETTINGS_PREVIOUS_TABINDEX_ATTRIBUTE)) {
2818                const previousTabIndex = element.hasAttribute('tabindex')
2819                    ? element.getAttribute('tabindex')
2820                    : SETTINGS_NO_TABINDEX_VALUE;
2821                element.setAttribute(SETTINGS_PREVIOUS_TABINDEX_ATTRIBUTE, previousTabIndex);
2822            }
2823            element.setAttribute('tabindex', '-1');
2824            return;
2825        }
2826
2827        const previousTabIndex = element.getAttribute(SETTINGS_PREVIOUS_TABINDEX_ATTRIBUTE);
2828        if (previousTabIndex === null) return;
2829
2830        if (previousTabIndex === SETTINGS_NO_TABINDEX_VALUE) {
2831            element.removeAttribute('tabindex');
2832        } else {
2833            element.setAttribute('tabindex', previousTabIndex);
2834        }
2835        element.removeAttribute(SETTINGS_PREVIOUS_TABINDEX_ATTRIBUTE);
2836    });
2837}
2838
2839function isPomodoroKeyboardShortcutBlocked() {
2840    if (isDayClosed() || state.settingsOpen || document.hidden) return true;
2841    if (document.getElementById('modal-layer')?.classList.contains('active')) return true;
2842    if (document.getElementById('v2-quick-capture-layer')?.classList.contains('active')) return true;
2843    if (document.getElementById('onboarding-layer')?.classList.contains('active')) return true;
2844    if (containerEl?.querySelector('#pomo-custom-timer-layer:not([hidden])')) return true;
2845    return false;
2846}
2847
2848function bindEvents(ctx) {
2849    if (isDayClosed()) {
2850        const reopenBtn = containerEl.querySelector('#pomo-btn-reopen');
2851        if (reopenBtn) {
2852            reopenBtn.addEventListener('click', async () => {
2853                if (reopenBtn.dataset.pomoResumeDay2 === 'true') {
2854                    const pilotState = getTwoDayPilotState();
2855                    pomodoroStore.startNewDay();
2856                    restorePomodoroAfterDayClosure(ctx);
2857                    if (!await restorePilotTaskAndStart(pilotState)) {
2858                        openPilotPriorityPicker();
2859                    }
2860                    return;
2861                }
2862
2863                if (confirm(`${i18n.t('pomo_confirm_reopen_title')}\n\n${i18n.t('pomo_confirm_reopen_desc')}`)) {
2864                    pomodoroStore.startNewDay();
2865                    restorePomodoroAfterDayClosure(ctx);
2866                }
2867            });
2868        }
2869        return;
2870    }
2871
2872    playBtn.addEventListener('click', toggleTimer);
2873    resetBtn.addEventListener('click', resetTimer);
2874
2875    // Settings Toggle
2876    const settingsBtn = containerEl.querySelector('#pomo-btn-settings');
2877    const closeSettingsBtn = containerEl.querySelector('#pomo-settings-close');
2878
2879    settingsBtn.addEventListener('click', () => {
2880        state.settingsOpen = !state.settingsOpen;
2881        syncSettingsPanelVisibility();
2882    });
2883
2884    closeSettingsBtn.addEventListener('click', () => {
2885        state.settingsOpen = false;
2886        syncSettingsPanelVisibility();
2887        settingsBtn.focus();
2888    });
2889
2890    // PiP
2891    containerEl.querySelector('#pomo-btn-pip').addEventListener('click', () => requestPiP());
2892
2893    // Project navigation (including PiP)
2894    bindProjectNavigation(containerEl);
2895
2896    // Global Mode Helper
2897    window.setPomoMode = (m) => setMode(m, true);
2898
2899    // Mode buttons (event delegation - works in both main window and PiP)
2900    bindModeButtons(containerEl);
2901    bindCustomTimerEditorEvents();
2902
2903    // Event Bus Listeners
2904    if (ctx.bus) {
2905        ctx.bus.on('pomodoro:ambience:open', () => focusAmbience?.open());
2906        ctx.bus.on('pomodoro:ambience:close', () => focusAmbience?.close());
2907        ctx.bus.on('pomodoro:custom-timer:open', ({ timerId = null } = {}) => {
2908            containerEl?.scrollIntoView({ behavior: 'smooth', block: 'center' });
2909            window.setTimeout(() => openCustomTimerEditor(timerId), 120);
2910        });
2911        ctx.bus.on('settings:changed', ({ settings } = {}) => {
2912            if (!settings || typeof settings !== 'object') return;
2913            state.settings = { ...state.settings, ...settings };
2914            state.settings.customTimers = normalizeCustomTimers(state.settings.customTimers);
2915            state.settings.customTimerCategories = normalizeCustomTimerCategories(state.settings.customTimerCategories);
2916            state.settings.startShortcut = normalizePomodoroStartShortcut(state.settings.startShortcut);
2917            renderModeOptions();
2918            renderDailyPriorityCard();
2919            updateUI();
2920        });
2921        ctx.bus.on('focus:task:changed', (task) => {
2922            const currentPriority = getCurrentDailyPriority();
2923            state.activeTask = currentPriority
2924                ? { ...currentPriority, id: currentPriority.sourceBlockId || currentPriority.taskId, text: currentPriority.text || currentPriority.title, dailyPriority: true }
2925                : (task || null);
2926            updateControls();
2927            renderDailyPriorityCard();
2928            saveTimerState();
2929        });
2930        ctx.bus.on('daily-priorities:changed', ({ items = [] } = {}) => {
2931            syncDailyPriorityState();
2932            renderTwoDayPilotCard();
2933            returnToPilotFocusAfterPrioritySelection(items);
2934        });
2935        ctx.bus.on('history:changed', () => updateDayProgressGauge());
2936        ctx.bus.on('auth:state', ({ user }) => {
2937            const nextOwnerId = String(user?.id || '').trim();
2938            if (pilotServerRefreshOwnerId !== nextOwnerId) {
2939                pilotServerRefreshOwnerId = nextOwnerId || null;
2940                pilotServerRefreshCoordinator.invalidate();
2941                pilotAssistedReturnCaptureKey = null;
2942                pilotPendingAssistedReturnMarker = null;
2943            }
2944            if (user?.id) claimTwoDayPilotForUser(user.id);
2945            pilotServerEvidenceReadyOwnerId = null;
2946            dayProgressPlanCache = null;
2947            renderDailyPriorityCard();
2948            renderTwoDayPilotCard();
2949            if (user?.id) void refreshTwoDayPilotFromServer();
2950        });
2951        ctx.bus.on('store:changed', () => {
2952            if (!state.activeTask) updateControls();
2953        });
2954        ctx.bus.on('sync:loaded', () => {
2955            dayProgressPlanCache = null;
2956            if (!state.activeTask) updateControls();
2957            else updateDayProgressGauge();
2958        });
2959        ctx.bus.on('sync:account-hydrated', ({ userId } = {}) => {
2960            if (String(userId || '') !== String(globalCtx?.currentUser?.id || '')) return;
2961            void refreshTwoDayPilotFromServer();
2962        });
2963        
2964        const handleProjectsChange = () => {
2965            updateProjectSelectorUI();
2966        };
2967        ctx.bus.on('objectifs:changed', handleProjectsChange);
2968        ctx.bus.on('objectifs:refresh', handleProjectsChange);
2969    }
2970
2971    const reviewInput = containerEl.querySelector('#pomo-review-input');
2972    const reviewBilanBtn = containerEl.querySelector('#pomo-review-bilan');
2973    const reviewLaterBtn = containerEl.querySelector('#pomo-review-later');
2974
2975    reviewInput?.addEventListener('input', (event) => {
2976        if (!state.pendingReview) return;
2977
2978        state.pendingReview.outcome = event.target.value.slice(0, 180);
2979        state.pendingReview.saved = false;
2980        state.pendingReview.saveError = false;
2981        syncSessionReviewStatus();
2982    });
2983    reviewInput?.addEventListener('blur', savePendingReviewOutcome);
2984    reviewBilanBtn?.addEventListener('click', () => {
2985        savePendingReviewOutcome();
2986        document.querySelector('.v2-dock button[data-target="v2-past"]')?.click();
2987    });
2988    reviewLaterBtn?.addEventListener('click', () => {
2989        savePendingReviewOutcome();
2990        state.pendingReview = null;
2991        syncFocusGuide();
2992    });
2993
2994    const endDayBtn = containerEl.querySelector('#pomo-end-day-btn');
2995    if (endDayBtn) {
2996        let pressTimer = null;
2997        let progressInterval = null;
2998        const holdDuration = 3000; // Prevent accidental day closure.
2999        const updateRate = 50;
3000        let currentProgress = 0;
3001
3002        const startPress = (e) => {
3003            if (e.button !== undefined && e.button !== 0) return; // Only left click
3004            if (pressTimer) return;
3005
3006            currentProgress = 0;
3007            endDayBtn.style.setProperty('--progress', '0%');
3008
3009            const progressStep = (updateRate / holdDuration) * 100;
3010
3011            progressInterval = setInterval(() => {
3012                currentProgress += progressStep;
3013                if (currentProgress >= 100) {
3014                    currentProgress = 100;
3015                    endDayBtn.style.setProperty('--progress', '100%');
3016                    clearInterval(progressInterval);
3017                } else {
3018                    endDayBtn.style.setProperty('--progress', `${currentProgress}%`);
3019                }
3020            }, updateRate);
3021
3022            pressTimer = setTimeout(() => {
3023                cancelPress();
3024                openDayClosureModal();
3025            }, holdDuration);
3026        };
3027
3028        const cancelPress = () => {
3029            if (pressTimer) {
3030                clearTimeout(pressTimer);
3031                pressTimer = null;
3032            }
3033            if (progressInterval) {
3034                clearInterval(progressInterval);
3035                progressInterval = null;
3036            }
3037            currentProgress = 0;
3038            endDayBtn.style.setProperty('--progress', '0%');
3039        };
3040
3041        endDayBtn.addEventListener('mousedown', startPress);
3042        endDayBtn.addEventListener('touchstart', startPress, { passive: true });
3043
3044        window.addEventListener('mouseup', cancelPress);
3045        endDayBtn.addEventListener('mouseleave', cancelPress);
3046        endDayBtn.addEventListener('touchend', cancelPress);
3047        endDayBtn.addEventListener('touchcancel', cancelPress);
3048        endDayBtn.addEventListener('contextmenu', e => e.preventDefault());
3049    }
3050}
3051
3052function bindSettingsEvents() {
3053    const helpToggles = [...containerEl.querySelectorAll('.pomo-help-toggle')];
3054    const setHelpOpen = (button, isOpen) => {
3055        const helpId = button.dataset.helpTarget;
3056        const help = helpId ? containerEl.querySelector(`#${helpId}`) : null;
3057        if (!help) return;
3058
3059        button.setAttribute('aria-expanded', String(isOpen));
3060        help.hidden = !isOpen;
3061    };
3062    const closeOtherHelp = (activeButton = null) => {
3063        helpToggles.forEach((button) => {
3064            if (button !== activeButton) setHelpOpen(button, false);
3065        });
3066    };
3067
3068    helpToggles.forEach((button) => {
3069        button.addEventListener('click', () => {
3070            const shouldOpen = button.getAttribute('aria-expanded') !== 'true';
3071            closeOtherHelp(button);
3072            setHelpOpen(button, shouldOpen);
3073        });
3074    });
3075
3076    settingsPanel?.addEventListener('keydown', (event) => {
3077        if (event.key !== 'Escape') return;
3078        const openButton = helpToggles.find((button) => button.getAttribute('aria-expanded') === 'true');
3079        if (!openButton) return;
3080
3081        event.preventDefault();
3082        setHelpOpen(openButton, false);
3083        openButton.focus();
3084    });
3085
3086    // Inputs Change (Live real-time updates)
3087    const bindInput = (id, key) => {
3088        const el = containerEl.querySelector(id);
3089        if (!el) return;
3090        
3091        const handleUpdate = (e, isFinal) => {
3092            const rawVal = e.target.value ? e.target.value.trim() : '';
3093            if (!rawVal && !isFinal) return;
3094            const val = isFinal
3095                ? syncNumberInputValue(e.target, key)
3096                : normalizeNumericSetting(key, e.target.value, state.settings[key]);
3097            state.settings[key] = val;
3098            saveSettings();
3099            if (state.mode === 'focus' && key === 'focusTime' && !state.isActive) resetTimer();
3100            if (state.mode === 'short-break' && key === 'shortBreakTime' && !state.isActive) resetTimer();
3101            if (state.mode === 'long-break' && key === 'longBreakTime' && !state.isActive) resetTimer();
3102            updateModeButtons();
3103            syncFocusGuide();
3104        };
3105
3106        el.addEventListener('input', (e) => handleUpdate(e, false));
3107        el.addEventListener('change', (e) => handleUpdate(e, true));
3108    };
3109
3110    bindInput('#set-focus-time', 'focusTime');
3111    bindInput('#set-short-break', 'shortBreakTime');
3112    bindInput('#set-long-break', 'longBreakTime');
3113    bindInput('#set-long-interval', 'longBreakInterval');
3114    bindInput('#set-daily-goal', 'dailyGoalMin');
3115
3116    const startShortcutSelect = containerEl.querySelector('#set-start-shortcut');
3117    startShortcutSelect?.addEventListener('change', (event) => {
3118        state.settings.startShortcut = normalizePomodoroStartShortcut(event.target.value);
3119        saveSettings();
3120        updateControls();
3121    });
3122
3123    const bindStringInput = (id, key) => {
3124        const el = containerEl.querySelector(id);
3125        if (!el) return;
3126        el.addEventListener('change', (e) => {
3127            state.settings[key] = e.target.value;
3128            saveSettings();
3129        });
3130    };
3131
3132    bindStringInput('#set-sleep-bedtime', 'sleepBedtime');
3133    bindStringInput('#set-sleep-wakeup', 'sleepWakeup');
3134
3135    // Toggles
3136    const bindToggle = (id, key) => {
3137        const el = containerEl.querySelector(id);
3138        if (!el) return;
3139        const syncToggleState = () => {
3140            el.classList.toggle('active', state.settings[key]);
3141            el.setAttribute('aria-checked', String(state.settings[key]));
3142        };
3143
3144        syncToggleState();
3145        el.addEventListener('click', () => {
3146            state.settings[key] = !state.settings[key];
3147            syncToggleState();
3148            saveSettings();
3149        });
3150    };
3151
3152    bindToggle('#toggle-auto-break', 'autoStartBreaks');
3153    bindToggle('#toggle-auto-pomo', 'autoStartPomo');
3154
3155    // Volume
3156    const volSlider = containerEl.querySelector('#set-volume');
3157    const volDisplay = containerEl.querySelector('#vol-display');
3158    if (volSlider && volDisplay) {
3159        volSlider.addEventListener('input', (e) => {
3160            state.settings.volume = syncNumberInputValue(e.target, 'volume');
3161            volDisplay.textContent = state.settings.volume + '%';
3162            saveSettings();
3163            // Preview volume change?
3164            // beep(440, 100, state.settings.volume);
3165        });
3166    }
3167
3168    // Alarm Select
3169    const alarmSelect = containerEl.querySelector('#set-alarm-sound');
3170    alarmSelect.addEventListener('change', (e) => {
3171        state.settings.alarmSound = e.target.value;
3172        saveSettings();
3173        if (state.settings.customSoundId) {
3174            audioDB.setCustomAlarmSelected(e.target.value === 'custom').catch((error) => {
3175                console.warn('Failed to persist custom alarm selection', error);
3176            });
3177        }
3178        playAlarm(true); // Preview selection
3179    });
3180
3181    // --- File Upload & DnD ---
3182    const uploadArea = containerEl.querySelector('#upload-alarm').closest('div');
3183    const fileInput = containerEl.querySelector('#upload-alarm');
3184    const infoText = containerEl.querySelector('#upload-info');
3185
3186    // Helper: Handle File
3187    const handleFileUpload = async (file) => {
3188        if (!file) return;
3189        if (!file.type.startsWith('audio/')) {
3190            alert(i18n.t('pomo_audio_required'));
3191            return;
3192        }
3193        if (file.size > 10 * 1024 * 1024) {
3194            alert(i18n.t('pomo_file_too_big'));
3195            return;
3196        }
3197
3198        infoText.textContent = i18n.t('pomo_uploading');
3199
3200        try {
3201            const previousId = state.settings.customSoundId;
3202            const saved = await audioDB.saveCustomAlarm(file);
3203
3204            state.settings.customSoundId = saved.id;
3205            state.settings.customSoundName = saved.name;
3206            state.settings.alarmSound = 'custom';
3207            saveSettings();
3208
3209            syncCustomAlarmSettingsUI();
3210
3211            // Visual confirmation
3212            uploadArea.style.background = '#f0fdf4'; // Light green
3213            setTimeout(() => uploadArea.style.background = '#f9fafb', 1000);
3214
3215            if (previousId && previousId !== saved.id) {
3216                audioDB.deleteFile(previousId).catch(() => {});
3217            }
3218        } catch (err) {
3219            console.error("Upload failed", err);
3220            infoText.textContent = "Erreur: " + err.message;
3221            alert(i18n.t('pomo_save_error'));
3222        }
3223    };
3224
3225    // Input Change
3226    fileInput.addEventListener('change', (e) => handleFileUpload(e.target.files[0]));
3227
3228    // Drag and Drop Events
3229    uploadArea.addEventListener('dragover', (e) => {
3230        e.preventDefault();
3231        uploadArea.style.background = '#eef2ff'; // Light blue highlight
3232        uploadArea.style.border = '1px dashed var(--accent-blue)';
3233    });
3234
3235    uploadArea.addEventListener('dragleave', (e) => {
3236        e.preventDefault();
3237        uploadArea.style.background = '#f9fafb';
3238        uploadArea.style.border = 'none';
3239    });
3240
3241    uploadArea.addEventListener('drop', (e) => {
3242        e.preventDefault();
3243        uploadArea.style.background = '#f9fafb';
3244        uploadArea.style.border = 'none';
3245
3246        if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
3247            handleFileUpload(e.dataTransfer.files[0]);
3248        }
3249    });
3250
3251    // Preview Button (Custom Sound Specific)
3252    containerEl.querySelector('#preview-custom-btn').addEventListener('click', async () => {
3253        if (!state.settings.customSoundId) {
3254            alert(i18n.t('pomo_no_custom_sound'));
3255            return;
3256        }
3257        // Force play custom sound even if another preset is selected globally
3258        playAlarm(true, true);
3259    });
3260}
3261
3262
3263function getTodayStats() {
3264    const todayStr = getLocalDayString();
3265    
3266    // 1. Focus sessions and breaks from history
3267    let focusDuration = 0;
3268    let focusCount = 0;
3269    let shortBreakDuration = 0;
3270    let longBreakDuration = 0;
3271    
3272    try {
3273        const history = pomodoroStore.getHistory();
3274        if (Array.isArray(history)) {
3275            history.forEach(session => {
3276                if (session && session.timestamp) {
3277                    const sessionDay = getLocalDayString(new Date(session.timestamp));
3278                    if (sessionDay === todayStr) {
3279                        const duration = Number(session.duration) || 0;
3280                        if (session.mode === 'focus') {
3281                            focusDuration += duration;
3282                            focusCount++;
3283                        } else if (session.mode === 'short-break') {
3284                            shortBreakDuration += duration;
3285                        } else if (session.mode === 'long-break') {
3286                            longBreakDuration += duration;
3287                        }
3288                    }
3289                }
3290            });
3291        }
3292    } catch (e) {
3293        console.error("[Pomo] Error parsing history for stats", e);
3294    }
3295    
3296    // 2. Priorities completed through today's execution flow.
3297    let completedCount = 0;
3298    try {
3299        const dayPlan = syncDayProgressPlan(getTodayPriorities());
3300        completedCount = dayPlan.tasks.filter(task => task?.completed === true).length;
3301    } catch (e) {
3302        console.error("[Pomo] Error counting completed priorities", e);
3303    }
3304    
3305    // 3. Goal reached check
3306    const focusMinutes = Math.round(focusDuration / 60);
3307    const dailyGoalMin = state.settings.dailyGoalMin || 120;
3308    const goalReached = focusMinutes >= dailyGoalMin;
3309    const productivityScore = Math.min(100, Math.round((focusMinutes / Math.max(1, dailyGoalMin)) * 100));
3310    
3311    return {
3312        focusDuration,
3313        focusCount,
3314        shortBreakDuration,
3315        longBreakDuration,
3316        completedCount,
3317        goalReached,
3318        productivityScore
3319    };
3320}
3321
3322function formatDurationMinutes(seconds) {
3323    const mins = Math.round(seconds / 60);
3324    if (mins < 60) {
3325        return `${mins} min`;
3326    }
3327    const hrs = Math.floor(mins / 60);
3328    const remMins = mins % 60;
3329    return remMins > 0 ? `${hrs}h ${remMins}m` : `${hrs}h`;
3330}
3331
3332function getDayClosureFlameMarkup(className = '') {
3333    return `
3334        <span class="${className}" aria-hidden="true">
3335            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
3336                <path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.07-2.14-.22-4.05 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.1.2-2.2.5-3.3.3 1.4 1 2.2 2 2.8z"></path>
3337            </svg>
3338        </span>
3339    `;
3340}
3341
3342function getDayClosureCoinMarkup(className = '') {
3343    return `
3344        <span class="${className}" aria-hidden="true">
3345            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
3346                <circle cx="12" cy="12" r="9"></circle>
3347                <path d="M15.5 8.5h-5a2 2 0 1 0 0 4h3a2 2 0 1 1 0 4H8.5"></path>
3348                <path d="M12 6.5v11"></path>
3349            </svg>
3350        </span>
3351    `;
3352}
3353
3354function translateSummary(key, values = {}) {
3355    return Object.entries(values).reduce((text, [name, value]) => (
3356        text.replaceAll(`{${name}}`, String(value))
3357    ), i18n.t(key));
3358}
3359
3360function formatRewardNumber(value) {
3361    return Math.max(0, Math.round(Number(value) || 0))
3362        .toLocaleString(i18n.locale())
3363        .replace(/\s/g, ' ');
3364}
3365
3366function updateStreakBadgeValue(streak) {
3367    const value = document.getElementById('streak-val');
3368    if (!value || !Number.isFinite(Number(streak))) return;
3369    const dayKey = Number(streak) > 1 ? 'topbar_days_plural' : 'topbar_days_singular';
3370    value.textContent = `${streak} ${i18n.t(dayKey)} `;
3371}
3372
3373function updateCreditsBadgeValue(credits) {
3374    const value = document.getElementById('coins-val');
3375    if (!value || !Number.isFinite(Number(credits))) return;
3376    value.textContent = formatRewardNumber(credits);
3377}
3378
3379function animateDaySummaryNumber(element, from, to, duration = 900) {
3380    if (!element) return;
3381    const startValue = Math.max(0, Math.round(Number(from) || 0));
3382    const endValue = Math.max(0, Math.round(Number(to) || 0));
3383    const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
3384    if (reduceMotion || startValue === endValue) {
3385        element.textContent = formatRewardNumber(endValue);
3386        return;
3387    }
3388
3389    const startedAt = performance.now();
3390    const renderFrame = (now) => {
3391        const progress = Math.min(1, (now - startedAt) / duration);
3392        const eased = 1 - Math.pow(1 - progress, 3);
3393        element.textContent = formatRewardNumber(startValue + ((endValue - startValue) * eased));
3394        if (progress < 1) requestAnimationFrame(renderFrame);
3395    };
3396    requestAnimationFrame(renderFrame);
3397}
3398
3399function animateDayClosureRewardToTopbar({
3400    sourceSelector,
3401    targetId,
3402    value,
3403    delay,
3404    flyingClass,
3405    awardedClass,
3406    updateValue
3407}) {
3408    const source = document.querySelector(sourceSelector);
3409    const target = document.getElementById(targetId);
3410    const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
3411    if (!source || !target || reduceMotion || typeof source.animate !== 'function') {
3412        updateValue(value);
3413        return;
3414    }
3415
3416    const sourceRect = source.getBoundingClientRect();
3417    const targetRect = target.getBoundingClientRect();
3418    if (!sourceRect.width || !targetRect.width) {
3419        updateValue(value);
3420        return;
3421    }
3422
3423    const flyingReward = source.cloneNode(true);
3424    flyingReward.classList.add(flyingClass);
3425    flyingReward.setAttribute('aria-hidden', 'true');
3426    Object.assign(flyingReward.style, {
3427        left: `${sourceRect.left}px`,
3428        top: `${sourceRect.top}px`,
3429        width: `${sourceRect.width}px`,
3430        height: `${sourceRect.height}px`
3431    });
3432    document.body.appendChild(flyingReward);
3433
3434    const translateX = targetRect.left + (targetRect.width / 2) - (sourceRect.left + sourceRect.width / 2);
3435    const translateY = targetRect.top + (targetRect.height / 2) - (sourceRect.top + sourceRect.height / 2);
3436    const animation = flyingReward.animate([
3437        { transform: 'translate3d(0, 0, 0) scale(1)', opacity: 1 },
3438        { offset: 0.72, transform: `translate3d(${translateX * 0.78}px, ${translateY * 0.78}px, 0) scale(0.72)`, opacity: 1 },
3439        { transform: `translate3d(${translateX}px, ${translateY}px, 0) scale(0.38)`, opacity: 0.15 }
3440    ], {
3441        duration: 980,
3442        delay,
3443        easing: 'cubic-bezier(0.3, 0.8, 0.2, 1)',
3444        fill: 'forwards'
3445    });
3446
3447    const finishReward = () => {
3448        flyingReward.remove();
3449        updateValue(value);
3450        target.classList.add(awardedClass);
3451        window.setTimeout(() => target.classList.remove(awardedClass), 850);
3452    };
3453    animation.finished.then(finishReward, finishReward);
3454}
3455
3456function getDayClosureRewardSnapshot(stats, closureResult) {
3457    const now = new Date();
3458    const sessions = pomodoroStore.getHistory();
3459    const factoryStats = buildFactoryStats(
3460        sessions,
3461        now,
3462        loadSettledFactoryState(sessions, now)
3463    );
3464    const rankState = calculateProductivityRank(sessions, now);
3465    const streakCurrent = Number(closureResult.closure?.protectedStreak) || pomodoroStore.getProtectedStreak();
3466    const streakGain = closureResult.streakAdded ? 1 : 0;
3467    const creditsCurrent = Math.max(0, Math.round(Number(factoryStats.availableCredits) || 0));
3468    const creditsGain = Math.max(0, Math.round(Number(factoryStats.creditsToday) || 0));
3469
3470    return {
3471        streakBefore: Math.max(0, streakCurrent - streakGain),
3472        streakGain,
3473        streakCurrent,
3474        score: Math.max(0, Math.min(100, Number(stats.productivityScore) || 0)),
3475        rankState,
3476        creditsBefore: Math.max(0, creditsCurrent - creditsGain),
3477        creditsGain,
3478        creditsCurrent
3479    };
3480}
3481
3482function getNextRankMarkup(rankState) {
3483    if (!rankState.nextRank) {
3484        return `<p class="pomo-day-summary__rank-next">${i18n.t('pomo_summary_rank_max')}</p>`;
3485    }
3486
3487    const nextRankName = i18n.t(rankState.nextRank.labelKey);
3488    return `
3489        <p class="pomo-day-summary__rank-next">
3490            <strong>${translateSummary('pomo_summary_rank_next', { rank: nextRankName })}</strong>
3491            <span>${translateSummary('pomo_summary_rank_conditions', {
3492                minutes: rankState.nextRank.minAverage,
3493                active: rankState.nextRank.minActiveDays,
3494                history: rankState.nextRank.minHistoryDays
3495            })}</span>
3496        </p>
3497    `;
3498}
3499
3500function animateDaySummaryRewards(rewardSnapshot) {
3501    const summary = document.querySelector('.pomo-day-summary');
3502    if (!summary) return;
3503
3504    updateStreakBadgeValue(rewardSnapshot.streakBefore);
3505    updateCreditsBadgeValue(rewardSnapshot.creditsBefore);
3506    requestAnimationFrame(() => summary.classList.add('is-rewarding'));
3507
3508    animateDaySummaryNumber(
3509        summary.querySelector('.pomo-day-summary__score-value'),
3510        0,
3511        rewardSnapshot.score,
3512        950
3513    );
3514    animateDaySummaryNumber(
3515        summary.querySelector('.pomo-day-summary__credits-value'),
3516        rewardSnapshot.creditsBefore,
3517        rewardSnapshot.creditsCurrent,
3518        1150
3519    );
3520
3521    animateDayClosureRewardToTopbar({
3522        sourceSelector: '.pomo-day-summary__streak-symbol svg',
3523        targetId: 'user-streak',
3524        value: rewardSnapshot.streakCurrent,
3525        delay: 520,
3526        flyingClass: 'pomo-day-summary__flying-flame',
3527        awardedClass: 'is-streak-awarded',
3528        updateValue: updateStreakBadgeValue
3529    });
3530    animateDayClosureRewardToTopbar({
3531        sourceSelector: '.pomo-day-summary__coin-icon svg',
3532        targetId: 'user-coins',
3533        value: rewardSnapshot.creditsCurrent,
3534        delay: 880,
3535        flyingClass: 'pomo-day-summary__flying-coin',
3536        awardedClass: 'is-credits-awarded',
3537        updateValue: updateCreditsBadgeValue
3538    });
3539
3540    const scoreDisplay = document.getElementById('score-display');
3541    window.setTimeout(() => {
3542        scoreDisplay?.classList.add('is-score-awarded');
3543        window.setTimeout(() => scoreDisplay?.classList.remove('is-score-awarded'), 850);
3544    }, 880);
3545}
3546
3547function openDayClosureModal() {
3548    const stats = getTodayStats();
3549
3550    const closureData = {
3551        closedAt: new Date().toISOString(),
3552        date: getLocalDayString(),
3553        shutdownNote: '',
3554        statsSnapshot: stats
3555    };
3556
3557    const closureResult = pomodoroStore.setDayClosure(closureData);
3558    const rewardSnapshot = getDayClosureRewardSnapshot(stats, closureResult);
3559
3560    if (state.isActive) {
3561        pauseTimer();
3562    }
3563
3564    renderLayout();
3565    bindEvents(globalCtx);
3566
3567    const focusTimeFormatted = formatDurationMinutes(stats.focusDuration);
3568    const focusSessionCount = stats.focusCount;
3569    const completedTasksCount = stats.completedCount;
3570    const currentRankName = i18n.t(rewardSnapshot.rankState.labelKey);
3571    const rankAsset = RANK_BADGE_ASSETS[rewardSnapshot.rankState.id] || RANK_BADGE_ASSETS.seed;
3572
3573    const html = `
3574        <div class="pomo-day-summary" role="status" aria-labelledby="pomo-day-summary-title">
3575            <header class="pomo-day-summary__header">
3576                <span class="pomo-day-summary__eyebrow">${i18n.t('pomo_summary_eyebrow')}</span>
3577                <h2 id="pomo-day-summary-title">${i18n.t('pomo_summary_title')}</h2>
3578            </header>
3579
3580            <section class="pomo-day-summary__streak" aria-label="${i18n.t('pomo_summary_streak_label')}">
3581                <div class="pomo-day-summary__streak-heading">
3582                    ${getDayClosureFlameMarkup('pomo-day-summary__streak-symbol')}
3583                    <span>${i18n.t('pomo_summary_streak_label')}</span>
3584                </div>
3585                <div class="pomo-day-summary__streak-flow">
3586                    <span class="pomo-day-summary__streak-value pomo-day-summary__streak-before">
3587                        <small>${i18n.t('pomo_summary_before')}</small>
3588                        <strong>${rewardSnapshot.streakBefore}</strong>
3589                    </span>
3590                    <span class="pomo-day-summary__streak-gain">+${rewardSnapshot.streakGain}</span>
3591                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path><path d="m14 7 5 5-5 5"></path></svg>
3592                    <span class="pomo-day-summary__streak-value pomo-day-summary__streak-current">
3593                        <small>${i18n.t('pomo_summary_now')}</small>
3594                        <strong>${rewardSnapshot.streakCurrent}</strong>
3595                    </span>
3596                </div>
3597            </section>
3598
3599            <div class="pomo-day-summary__rewards">
3600                <section class="pomo-day-summary__reward-card pomo-day-summary__score-card">
3601                    <span class="pomo-day-summary__reward-label">${i18n.t('pomo_summary_score')}</span>
3602                    <div class="pomo-day-summary__score">
3603                        <strong class="pomo-day-summary__score-value">0</strong><span>/100</span>
3604                    </div>
3605                    <div class="pomo-day-summary__rank">
3606                        <img src="${rankAsset}" alt="" aria-hidden="true">
3607                        <span><small>${i18n.t('pomo_summary_rank')}</small><strong>${currentRankName}</strong></span>
3608                    </div>
3609                    ${getNextRankMarkup(rewardSnapshot.rankState)}
3610                </section>
3611
3612                <section class="pomo-day-summary__reward-card pomo-day-summary__credits-card">
3613                    <div class="pomo-day-summary__credits-heading">
3614                        ${getDayClosureCoinMarkup('pomo-day-summary__coin-icon')}
3615                        <span class="pomo-day-summary__reward-label">${i18n.t('pomo_summary_credits')}</span>
3616                    </div>
3617                    <strong class="pomo-day-summary__credits-value">${formatRewardNumber(rewardSnapshot.creditsBefore)}</strong>
3618                    <span class="pomo-day-summary__credits-gain">${translateSummary('pomo_summary_credits_gain', { value: formatRewardNumber(rewardSnapshot.creditsGain) })}</span>
3619                </section>
3620            </div>
3621
3622            <dl class="pomo-day-summary__stats">
3623                <div><dt>${i18n.t('pomo_summary_sessions')}</dt><dd>${focusSessionCount}</dd></div>
3624                <div><dt>${i18n.t('pomo_summary_focus_time')}</dt><dd>${focusTimeFormatted}</dd></div>
3625                <div><dt>${i18n.t('pomo_summary_priorities')}</dt><dd>${completedTasksCount}</dd></div>
3626            </dl>
3627
3628            <button id="pomo-day-summary-prepare" class="pomo-day-summary__button" type="button">${i18n.t('pomo_summary_prepare_tomorrow')}</button>
3629        </div>
3630    `;
3631    
3632    globalCtx.bus.emit('ui:modal:open', {
3633        titleKey: 'pomo_modal_closure_title',
3634        className: 'pomo-day-summary-modal',
3635        content: html
3636    });
3637    
3638    setTimeout(() => {
3639        animateDaySummaryRewards(rewardSnapshot);
3640        const prepareTomorrow = document.getElementById('pomo-day-summary-prepare');
3641        if (prepareTomorrow) {
3642            prepareTomorrow.addEventListener('click', () => {
3643                document.getElementById('modal-close-btn')?.click();
3644                globalCtx.bus?.emit('planning-session:start', {
3645                    ownerId: String(globalCtx.currentUser?.id || 'guest'),
3646                    requestedAt: new Date().toISOString()
3647                });
3648                const organisationButton = document.querySelector('.dock-btn[data-target="v2-future"]');
3649                if (organisationButton instanceof HTMLElement) {
3650                    organisationButton.click();
3651                    return;
3652                }
3653                document.getElementById('v2-future')?.scrollIntoView({ behavior: 'smooth' });
3654            });
3655            prepareTomorrow.focus({ preventScroll: true });
3656        }
3657    }, 0);
3658    window.setTimeout(() => globalCtx.bus?.emit('history:changed'), 1900);
3659}
3660
3661
3662/* --- Timer Logic --- */
3663
3664function toggleTimer() {
3665    stopAlarm(); // Stop ringing if ringing
3666    if (isDayClosed()) {
3667        globalCtx.bus.emit('ui:toast', i18n.t('pomo_alert_closed_desc'));
3668        return;
3669    }
3670    if (state.isActive) {
3671        pauseTimer();
3672    } else {
3673        startTimer();
3674    }
3675}
3676
3677function selectNextTaskForFocus() {
3678    const nextTask = getRecommendedTask();
3679
3680    if (nextTask) {
3681        globalCtx.bus.emit('app:push-task', { blockId: nextTask.blockId || nextTask.id, blockText: nextTask.text });
3682        return true;
3683    }
3684
3685    globalCtx.bus.emit('ui:quick-capture:open');
3686    return true;
3687}
3688
3689function finishFollowerTimer() {
3690    state.isActive = false;
3691    state.timeLeft = 0;
3692    state.sessionEndMs = null;
3693    clearInterval(state.interval);
3694    updateUI();
3695    updateControls();
3696    globalCtx?.bus?.emit('timer:stopped');
3697}
3698
3699function claimPomodoroTimerOwnership() {
3700    state.timerOwnerTabId = timerTabId;
3701    state.timerOwnerHeartbeatAt = Date.now();
3702    state.timerHeartbeatTicks = 0;
3703    state.isTimerFollower = false;
3704    saveTimerState();
3705    syncBrowserFocusProtection(true, 'active');
3706}
3707
3708function startCountdownInterval() {
3709    if (state.interval) clearInterval(state.interval);
3710
3711    const delay = state.isDemoSession ? 40 : 1000;
3712    state.interval = setInterval(() => {
3713        if (
3714            state.isTimerFollower
3715            && shouldClaimPomodoroTimerOwnership({
3716                isActive: state.isActive,
3717                ownerTabId: state.timerOwnerTabId,
3718                ownerHeartbeatAt: state.timerOwnerHeartbeatAt
3719            }, timerTabId)
3720        ) {
3721            claimPomodoroTimerOwnership();
3722        }
3723
3724        if (state.timeLeft > 0) {
3725            if (state.isDemoSession) {
3726                state.timeLeft = Math.max(0, state.timeLeft - 60);
3727            } else {
3728                state.timeLeft--;
3729            }
3730            updateUI();
3731
3732            if (!state.isDemoSession && !state.isTimerFollower) {
3733                state.timerHeartbeatTicks++;
3734                if (state.timerHeartbeatTicks >= 5) {
3735                    state.timerHeartbeatTicks = 0;
3736                    saveTimerState();
3737                }
3738            }
3739
3740            // Broadcast Tick for Real-time Analytics
3741            if (globalCtx && globalCtx.bus) {
3742                const total = getDuration();
3743                const elapsed = total - state.timeLeft;
3744                globalCtx.bus.emit('timer:tick', {
3745                    mode: state.mode,
3746                    timeLeft: state.timeLeft,
3747                    duration: total,
3748                    elapsed: elapsed
3749                });
3750            }
3751        } else if (state.isTimerFollower) {
3752            finishFollowerTimer();
3753        } else {
3754            completeTimer();
3755        }
3756    }, delay);
3757}
3758
3759function startTimer({ preserveAlarm = false } = {}) {
3760    if (!preserveAlarm) stopAlarm(); // Ensure ring resets
3761    if (isDayClosed()) {
3762        globalCtx.bus.emit('ui:toast', i18n.t('pomo_alert_closed_desc'));
3763        return;
3764    }
3765    if (isFocusMode(state.mode) && getCurrentDailyPriority()?.missing) {
3766        globalCtx?.bus?.emit('ui:toast', 'Cette priorité doit être resynchronisée ou retirée avant de lancer un focus.');
3767        return;
3768    }
3769    if (isFocusMode(state.mode) && state.pendingReview) {
3770        savePendingReviewOutcome();
3771        state.pendingReview = null;
3772        syncFocusGuide();
3773    }
3774    const isDemo = !!document.getElementById('onboarding-layer')?.classList.contains('active');
3775    state.isDemoSession = isDemo;
3776    state.isActive = true;
3777    state.isTimerFollower = false;
3778    state.timerOwnerTabId = timerTabId;
3779    state.timerOwnerHeartbeatAt = Date.now();
3780    state.timerHeartbeatTicks = 0;
3781    state.sessionEndMs = Date.now() + state.timeLeft * 1000;
3782    if (!state.isDemoSession && isFocusMode(state.mode)) {
3783        state.browserFocusSessionId ||= generateUUID();
3784        syncBrowserFocusProtection(true, 'active');
3785    }
3786    updateControls();
3787    saveTimerState();
3788
3789    startCountdownInterval();
3790
3791    // Analytics (Activation Moment): only a real Focus session counts here.
3792    if (!state.isDemoSession && isFocusMode(state.mode) && typeof window.gtag === 'function') {
3793        window.gtag('event', 'focus_session_start', {
3794            'event_category': 'usage',
3795            'event_label': state.mode,
3796            'duration_minutes': Math.round(getDuration() / 60)
3797        });
3798    }
3799}
3800
3801function pauseTimer({ preserveAlarm = false, browserStatus = 'paused' } = {}) {
3802    if (!preserveAlarm) stopAlarm(); // Can also stop if pause
3803    state.timerOwnerTabId = timerTabId;
3804    state.timerOwnerHeartbeatAt = Date.now();
3805    state.timerHeartbeatTicks = 0;
3806    state.isTimerFollower = false;
3807    syncBrowserFocusProtection(false, browserStatus);
3808    state.isActive = false;
3809    state.sessionEndMs = null;
3810    clearInterval(state.interval);
3811    updateControls();
3812    saveTimerState();
3813    // Notify analytics that timer stopped (clear active session)
3814    if (globalCtx && globalCtx.bus) {
3815        globalCtx.bus.emit('timer:stopped');
3816    }
3817}
3818
3819function resetTimer({ preserveAlarm = false, browserStatus = 'cancelled' } = {}) {
3820    if (!preserveAlarm) stopAlarm(); // Key: Stop alarm if ringing
3821    pauseTimer({ preserveAlarm: true, browserStatus });
3822    state.browserFocusSessionId = null;
3823
3824    // Get Time based on mode and settings
3825    state.timeLeft = getDuration();
3826    updateUI();
3827    updateControls();
3828    saveTimerState();
3829}
3830
3831function setMode(mode, manual = false, { preserveAlarm = false } = {}) {
3832    const nextMode = (MODES[mode] || getCustomTimer(mode)) ? mode : 'focus';
3833    const modeChanged = state.mode !== nextMode;
3834    if (nextMode !== 'focus' || modeChanged || manual) {
3835        state.focusDurationOverrideSeconds = null;
3836    }
3837    state.mode = nextMode;
3838    resetTimer({ preserveAlarm });
3839
3840    // Update Badge (card header - main window)
3841    const badge = containerEl.querySelector('#pomo-mode-badge');
3842    syncModeBadge(badge, nextMode);
3843
3844    // Update mode buttons active state (works in both main & PiP)
3845    updateModeButtons();
3846    syncFocusGuide();
3847}
3848
3849function updateModeButtons() {
3850    // The .pomo-modes may be in main window or PiP window
3851    // Search in both containerEl and any active PiP
3852    const roots = [containerEl];
3853    if ('documentPictureInPicture' in window && window.documentPictureInPicture.window) {
3854        roots.push(window.documentPictureInPicture.window.document);
3855    }
3856
3857    // Calculate current durations in minutes
3858    const override = Number(state.focusDurationOverrideSeconds);
3859    const focusMins = Math.round((Number.isFinite(override) && override > 0 ? override : getTaskFocusMinutes() * 60) / 60);
3860    const shortMins = state.settings.shortBreakTime;
3861    const longMins = state.settings.longBreakTime;
3862
3863    roots.forEach(root => {
3864        const buttons = root.querySelectorAll('.badge-pomo[data-mode]');
3865        buttons.forEach(btn => {
3866            const m = btn.dataset.mode;
3867            btn.classList.toggle('active', m === state.mode);
3868
3869            const customTimer = getCustomTimer(m);
3870            const mins = customTimer?.minutes
3871                ?? (m === 'focus' ? focusMins : m === 'short-break' ? shortMins : longMins);
3872            const label = getModeLabel(m);
3873            const editLabel = i18n.t('pomo_custom_timer_edit_aria').replace('{name}', label);
3874
3875            btn.innerHTML = `<span class="badge-pomo-label">${escapeHtml(label)}</span><button class="mode-duration-badge" type="button" data-timer-edit="${escapeHtml(m)}" aria-label="${escapeHtml(editLabel)}" title="${escapeHtml(editLabel)}">
3876                <span class="mode-duration-normal">
3877                    <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right:2px"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
3878                    <span class="mode-duration-text">${mins}m</span>
3879                </span>
3880                <span class="mode-duration-pencil" aria-hidden="true">
3881                    <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg>
3882                </span>
3883            </button>`;
3884        });
3885    });
3886}
3887
3888async function completeTimer() {
3889    pauseTimer({ browserStatus: 'completed' });
3890    const completedMode = state.mode;
3891    const completedTaskText = String(state.activeTask?.text || '').trim();
3892    const completedTaskId = String(
3893        state.activeTask?.taskId
3894        || getCurrentDailyPriority()?.taskId
3895        || ''
3896    ).trim() || null;
3897
3898    if (state.isDemoSession) {
3899        // The accelerated onboarding demo must advance without polluting
3900        // the user's history or the real Focus analytics.
3901        globalCtx?.bus?.emit('pomo:completed');
3902        state.isDemoSession = false;
3903        state.isActive = false;
3904        resetTimer();
3905        return;
3906    }
3907
3908    playAlarm();
3909
3910    // A pause can be persisted in history, but it is not a completed Focus.
3911    if (isFocusMode(completedMode)) {
3912        sendGAEvent('focus_session_complete', 'productivity', completedMode);
3913    }
3914
3915    // Notification
3916    if ('Notification' in window) {
3917        if (Notification.permission === "granted") {
3918            new Notification(i18n.t('pomo_timer_done'), { body: getModeLabel(state.mode) });
3919        } else if (Notification.permission !== "denied") {
3920            Notification.requestPermission();
3921        }
3922    }
3923
3924    // Auto-switch Logic
3925    const completedSession = saveSession(); // Save real session (Focus or Break)
3926    if (isFocusMode(completedMode)) state.browserFocusSessionId = null;
3927
3928    if (isFocusMode(completedMode) && completedSession && globalCtx?.currentUser?.id) {
3929        // J1/J2 only advances after this Focus has been persisted and read back
3930        // from the authenticated server history. Browser history is never the
3931        // retention source of truth.
3932        const completedLocalDate = getPilotLocalDate(completedSession.timestamp);
3933        const pilotResult = await refreshTwoDayPilotFromServer({
3934            flushHistory: true,
3935            resumeTaskId: completedTaskId
3936        });
3937        const pilotAfter = pilotResult.state;
3938        const evidence = pilotResult.evidence;
3939        const advancedThisCompletion = Boolean(
3940            completedLocalDate
3941            && pilotAfter?.serverConfirmed === true
3942            && pilotResult.snapshot?.historyPersisted === true
3943            && pilotResult.advancedDates?.includes(completedLocalDate)
3944        );
3945        const completedDayCount = completedLocalDate === evidence?.j1Date
3946            ? 1
3947            : completedLocalDate === evidence?.j2Date
3948                ? 2
3949                : 0;
3950
3951        if (advancedThisCompletion && completedDayCount > 0) {
3952            const campaign = pilotAfter?.campaign || 'two_day_pilot';
3953            if (completedDayCount === 1) {
3954                sendGAEvent('activation_first_focus_completed', 'activation', 'server_confirmed_j1');
3955            }
3956            sendGAEvent('pilot_day_completed', 'activation', `${campaign}:day_${completedDayCount}`);
3957
3958            if (completedDayCount === 2) {
3959                const classification = evidence?.j2Classification || 'UNVERIFIED_J2';
3960                sendGAEvent(
3961                    'pilot_two_day_completed',
3962                    'retention',
3963                    `${campaign}:${classification.toLowerCase()}`
3964                );
3965                if (classification === 'NATURAL_J2') {
3966                    sendGAEvent('retention_second_day_focus_completed', 'retention', 'natural_two_day_return');
3967                    sendGAEvent('activation_two_day_completed', 'activation', 'natural_two_day_return');
3968                } else if (classification === 'ASSISTED_J2') {
3969                    const assistanceType = evidence?.assistance?.type || 'assisted_return';
3970                    sendGAEvent('retention_assisted_second_day_focus_completed', 'retention', assistanceType);
3971                    sendGAEvent('activation_assisted_two_day_completed', 'activation', assistanceType);
3972                } else {
3973                    sendGAEvent(
3974                        'retention_unverified_second_day_focus_completed',
3975                        'retention',
3976                        'server_assistance_unavailable'
3977                    );
3978                }
3979            }
3980
3981            const isOrganicReturn = campaign === ORGANIC_TWO_DAY_PILOT_CAMPAIGN;
3982            const progressTitleKey = completedDayCount >= 2
3983                ? (isOrganicReturn ? 'organic_progress_complete_title' : 'pilot_progress_complete_title')
3984                : (isOrganicReturn ? 'organic_progress_wait_title' : 'pilot_progress_wait_title');
3985            globalCtx?.bus?.emit('ui:toast', i18n.t(progressTitleKey));
3986            globalCtx?.bus?.emit('two-day-pilot:changed');
3987        }
3988    }
3989
3990    if (isFocusMode(completedMode)) {
3991        // Post-session review card deactivated as requested
3992        state.pomoCount++;
3993        // Check for long break
3994        if (state.pomoCount % state.settings.longBreakInterval === 0) {
3995            setMode('long-break', false, { preserveAlarm: true });
3996        } else {
3997            setMode('short-break', false, { preserveAlarm: true });
3998        }
3999
4000        if (state.settings.autoStartBreaks) {
4001            startTimer({ preserveAlarm: true });
4002        }
4003
4004    } else {
4005        // Break is over, back to focus
4006        setMode('focus', false, { preserveAlarm: true });
4007        if (state.settings.autoStartPomo) {
4008            startTimer({ preserveAlarm: true });
4009        }
4010    }
4011}
4012
4013function saveSession() {
4014    try {
4015        const duration = getDuration();
4016        const activeProj = getSelectedProject();
4017        const customTimer = getCustomTimer(state.mode);
4018        const timerCategory = customTimer ? getTimerCategoryMeta(customTimer.category) : null;
4019        const browserFocusSessionId = isFocusMode(state.mode)
4020            ? String(state.browserFocusSessionId || '')
4021            : '';
4022        const browserFeedback = browserFocusSessionId
4023            ? pendingBrowserFeedback.get(browserFocusSessionId)
4024            : null;
4025        const newSession = {
4026            id: generateUUID(),
4027            timestamp: Date.now() - (duration * 1000), // Save Start Time, not End Time
4028            duration: duration,
4029            mode: getModeCategory(state.mode),
4030            ...(customTimer ? {
4031                customTimerId: customTimer.id,
4032                customTimerName: customTimer.name,
4033                customTimerCategory: customTimer.category,
4034                ...(timerCategory?.custom ? {
4035                    customTimerCategoryName: timerCategory.name,
4036                    customTimerCategoryEmoji: timerCategory.emoji,
4037                    customTimerCategoryColor: timerCategory.color
4038                } : {})
4039            } : {}),
4040            ...(isFocusMode(state.mode) ? {
4041                projectId: activeProj ? activeProj.id : null,
4042                projectName: activeProj ? activeProj.title : '',
4043                projectIcon: activeProj ? activeProj.icon : '',
4044                outcome: '',
4045                ...(browserFocusSessionId ? {
4046                    browserFocusSessionId,
4047                    ...(browserFeedback ? { browserFeedback } : {})
4048                } : {})
4049            } : {})
4050        };
4051        pomodoroStore.addSession(newSession);
4052        if (browserFeedback) pendingBrowserFeedback.delete(browserFocusSessionId);
4053
4054        // Notify Analytics & Sync
4055        if (globalCtx && globalCtx.bus) {
4056            globalCtx.bus.emit('pomo:completed');
4057        }
4058        return newSession;
4059    } catch (e) {
4060        console.error("Failed to save session", e);
4061        return null;
4062    }
4063}
4064
4065function savePendingReviewOutcome() {
4066    if (!state.pendingReview) return false;
4067    if (state.pendingReview.saved && !state.pendingReview.saveError) return true;
4068
4069    try {
4070        const history = pomodoroStore.getHistory();
4071        const outcome = String(state.pendingReview.outcome || '').trim();
4072        let sessionFound = false;
4073        const updatedHistory = history.map((session) => {
4074            if (session?.id !== state.pendingReview.sessionId) return session;
4075
4076            sessionFound = true;
4077            return { ...session, outcome };
4078        });
4079
4080        if (!sessionFound) throw new Error('Completed session not found');
4081
4082        pomodoroStore.updateHistory(updatedHistory);
4083        state.pendingReview.outcome = outcome;
4084        state.pendingReview.saved = true;
4085        state.pendingReview.saveError = false;
4086        syncSessionReviewStatus();
4087
4088        if (globalCtx?.bus) {
4089            globalCtx.bus.emit('history:changed');
4090        }
4091        return true;
4092    } catch (e) {
4093        console.error("Failed to save review outcome", e);
4094        state.pendingReview.saved = false;
4095        state.pendingReview.saveError = true;
4096        syncSessionReviewStatus();
4097        return false;
4098    }
4099}
4100
4101function formatTimerText(seconds) {
4102    const minutes = Math.floor(seconds / 60).toString().padStart(2, '0');
4103    const remainingSeconds = (seconds % 60).toString().padStart(2, '0');
4104    return `${minutes}:${remainingSeconds}`;
4105}
4106
4107function updateUI() {
4108    const totalSeconds = getDuration();
4109    const pct = 1 - (state.timeLeft / totalSeconds);
4110
4111    // Time
4112    const timeText = formatTimerText(state.timeLeft);
4113    if (timeEl) timeEl.textContent = timeText;
4114    document.title = state.isActive || state.timeLeft < totalSeconds
4115        ? `${timeText} - Pomodoro`
4116        : i18n.t('app_document_title');
4117
4118    // Ring
4119    if (ringEl) {
4120        const circle = ringEl.querySelector('#pomo-progress-circle');
4121        if (circle) {
4122            if (state.isDemoSession) {
4123                circle.style.transition = 'none';
4124            } else {
4125                circle.style.transition = 'stroke-dashoffset 1s linear, stroke 0.3s ease';
4126            }
4127            const color = getModeColor(state.mode);
4128            circle.style.stroke = color;
4129            // Circumference is 157.08 for r=25 (filled Pac-Man style)
4130            const circumference = 157.08;
4131            const offset = circumference * (1 - pct);
4132            circle.style.strokeDashoffset = offset;
4133            
4134            // Clean up old conic gradient style to prevent any interaction/background bleeding
4135            if (ringEl.style.background) {
4136                ringEl.style.background = '';
4137            }
4138        } else {
4139            const deg = pct * 360;
4140            const color = getModeColor(state.mode);
4141            ringEl.style.background = `conic-gradient(${color} ${deg}deg, #E5E5EA ${deg}deg)`;
4142        }
4143    }
4144
4145    // ETA Finish Time
4146    if (ringEl) {
4147        const etaEl = ringEl.querySelector('#pomo-eta');
4148        const etaTimeEl = ringEl.querySelector('#pomo-eta-time');
4149        if (etaEl && etaTimeEl) {
4150            const endMs = (state.isActive && state.sessionEndMs) ? state.sessionEndMs : (Date.now() + state.timeLeft * 1000);
4151            const endStr = new Date(endMs).toLocaleTimeString(i18n.getLang() === 'fr' ? 'fr-FR' : 'en-US', { hour: '2-digit', minute: '2-digit' });
4152            etaTimeEl.textContent = endStr;
4153            etaEl.style.display = 'flex';
4154        }
4155    }
4156
4157    updateMobileMiniTimer();
4158    updateDayProgressGauge();
4159    focusAmbience?.sync();
4160}
4161
4162function updateControls() {
4163    if (!playBtn) return;
4164    const guideCopy = getFocusGuideCopy();
4165    const hasProgress = state.timeLeft < getDuration();
4166    let actionLabel = guideCopy.pauseAction;
4167    let actionIcon = '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16" rx="2"></rect><rect x="14" y="4" width="4" height="16" rx="2"></rect></svg>';
4168
4169    if (!state.isActive) {
4170        actionIcon = '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path d="M8 5v14l11-7z" /></svg>';
4171
4172        if (hasProgress) {
4173            actionLabel = guideCopy.resumeAction;
4174        } else if (isFocusMode(state.mode)) {
4175            actionLabel = guideCopy.startFocusAction.replace('{minutes}', Math.round(getDuration() / 60));
4176        } else {
4177            actionLabel = guideCopy.startBreakAction;
4178        }
4179    }
4180
4181    playBtn.removeAttribute('data-i18n-aria-label');
4182    playBtn.setAttribute('aria-label', actionLabel);
4183    const startShortcut = getPomodoroStartShortcut(state.settings.startShortcut);
4184    const shortcutLabelKey = {
4185        space: 'pomo_shortcut_space',
4186        keyP: 'pomo_shortcut_key_p',
4187        keyF: 'pomo_shortcut_key_f'
4188    }[normalizePomodoroStartShortcut(state.settings.startShortcut)];
4189    if (startShortcut.ariaKey && shortcutLabelKey) {
4190        const shortcutLabel = i18n.t(shortcutLabelKey);
4191        playBtn.setAttribute('aria-keyshortcuts', startShortcut.ariaKey);
4192        playBtn.title = translatePomodoro('pomo_start_shortcut_action_hint', {
4193            action: actionLabel,
4194            shortcut: shortcutLabel
4195        });
4196    } else {
4197        playBtn.removeAttribute('aria-keyshortcuts');
4198        playBtn.title = actionLabel;
4199    }
4200    playBtn.innerHTML = actionIcon;
4201
4202    if (state.isActive) {
4203        playBtn.classList.add('secondary');
4204        playBtn.classList.remove('filled');
4205    } else {
4206        playBtn.classList.add('filled');
4207        playBtn.classList.remove('secondary');
4208    }
4209
4210    syncFocusGuide();
4211    renderDailyPriorityCard();
4212    renderTwoDayPilotCard();
4213    updateMobileMiniTimer();
4214    focusAmbience?.sync();
4215}
4216
4217function getDuration() {
4218    if (state.mode === 'focus') {
4219        const override = Number(state.focusDurationOverrideSeconds);
4220        return Number.isFinite(override) && override > 0
4221            ? override
4222            : getTaskFocusMinutes() * 60;
4223    }
4224    const customTimer = getCustomTimer(state.mode);
4225    if (customTimer) return customTimer.minutes * 60;
4226    if (state.mode === 'short-break') return state.settings.shortBreakTime * 60;
4227    if (state.mode === 'long-break') return state.settings.longBreakTime * 60;
4228    return 25 * 60;
4229}
4230
4231/* --- Audio System --- */
4232
4233let currentAudio = null;
4234let currentAudioUrl = null;
4235let activeOscillators = [];
4236let alarmRepeatTimer = null;
4237let alarmStopTimer = null;
4238let alarmPlaybackToken = 0;
4239
4240function stopAlarm() {
4241    alarmPlaybackToken += 1;
4242    if (alarmRepeatTimer) {
4243        clearInterval(alarmRepeatTimer);
4244        alarmRepeatTimer = null;
4245    }
4246    if (alarmStopTimer) {
4247        clearTimeout(alarmStopTimer);
4248        alarmStopTimer = null;
4249    }
4250    // 1. Stop Audio Element
4251    if (currentAudio) {
4252        currentAudio.pause();
4253        currentAudio.currentTime = 0;
4254        currentAudio = null;
4255    }
4256    if (currentAudioUrl) {
4257        URL.revokeObjectURL(currentAudioUrl);
4258        currentAudioUrl = null;
4259    }
4260    // 2. Stop Oscillators
4261    activeOscillators.forEach(node => {
4262        try { node.stop(); } catch (e) { }
4263        try { node.disconnect(); } catch (e) { }
4264    });
4265    activeOscillators = [];
4266}
4267
4268function scheduleAlarmStop(durationMs = ALARM_RING_DURATION_MS) {
4269    alarmStopTimer = setTimeout(() => stopAlarm(), durationMs);
4270}
4271
4272// Melodies for Oscillator Presets
4273const MELODIES = {
4274    'bell': [
4275        { freq: 523.25, type: 'triangle', duration: 0.8 }, // C5
4276        { freq: 0, duration: 0.1 },
4277    ],
4278    'chime': [
4279        { freq: 659.25, type: 'sine', duration: 0.2 }, // E5
4280        { freq: 880.00, type: 'sine', duration: 0.4 }, // A5
4281        { freq: 659.25, type: 'sine', duration: 0.2 }, // E5
4282        { freq: 1046.50, type: 'sine', duration: 0.6 } // C6
4283    ],
4284    'digital': [
4285        { freq: 880, type: 'square', duration: 0.1 },
4286        { freq: 880, type: 'square', duration: 0.1 },
4287        { freq: 880, type: 'square', duration: 0.1 },
4288        { freq: 0, duration: 0.2 },
4289        { freq: 1760, type: 'square', duration: 0.4 }
4290    ]
4291};
4292
4293async function playAlarm(preview = false, forceCustom = false) {
4294    stopAlarm(); // Stop previous sound
4295    const playbackToken = alarmPlaybackToken;
4296
4297    if (state.settings.alarmSound === 'none' && !forceCustom) {
4298        if (preview) alert(i18n.t('pomo_no_sound'));
4299        return;
4300    }
4301
4302    // 1. Custom Sound (File)
4303    if ((state.settings.alarmSound === 'custom' || forceCustom) && state.settings.customSoundId) {
4304        try {
4305            const saved = await audioDB.getCustomAlarm(state.settings.customSoundId);
4306            if (playbackToken !== alarmPlaybackToken) return;
4307            if (saved?.blob) {
4308                const policy = getAlarmPlaybackPolicy({ preview, custom: true });
4309                currentAudioUrl = URL.createObjectURL(saved.blob);
4310                currentAudio = new Audio(currentAudioUrl);
4311                currentAudio.volume = state.settings.volume / 100;
4312                currentAudio.loop = policy.loop;
4313                currentAudio.play().catch((error) => {
4314                    console.warn('Pomodoro custom alarm playback was blocked', error);
4315                });
4316                if (policy.stopAfterMs) scheduleAlarmStop(policy.stopAfterMs);
4317                return;
4318            }
4319        } catch (e) {
4320            console.error(e);
4321        }
4322    }
4323
4324    // 2. Presets (Oscillators)
4325    const melody = MELODIES[state.settings.alarmSound] || MELODIES.bell;
4326    if (melody) {
4327        playMelody(melody, state.settings.volume);
4328        const policy = getAlarmPlaybackPolicy({ preview, custom: false });
4329        if (policy.repeat) {
4330            alarmRepeatTimer = setInterval(
4331                () => playMelody(melody, state.settings.volume),
4332                getAlarmRepeatIntervalMs(melody)
4333            );
4334        }
4335        if (policy.stopAfterMs) scheduleAlarmStop(policy.stopAfterMs);
4336    }
4337}
4338
4339let audioCtx = null;
4340
4341function getAudioContext() {
4342    const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
4343    if (!AudioContextCtor) return null;
4344    if (!audioCtx) audioCtx = new AudioContextCtor();
4345    return audioCtx;
4346}
4347
4348function playMelody(notes, volume) {
4349    const ctx = getAudioContext();
4350    if (!ctx) return;
4351    if (ctx.state === 'suspended') ctx.resume();
4352
4353    let startTime = ctx.currentTime;
4354
4355    notes.forEach(note => {
4356        if (note.freq > 0) {
4357            const osc = ctx.createOscillator();
4358            const gain = ctx.createGain();
4359
4360            osc.type = note.type || 'sine';
4361            osc.frequency.value = note.freq;
4362
4363            // Volume logic
4364            gain.gain.setValueAtTime((volume / 100) * 0.2, startTime);
4365            gain.gain.exponentialRampToValueAtTime(0.0001, startTime + note.duration);
4366
4367            osc.connect(gain);
4368            gain.connect(ctx.destination);
4369
4370            osc.start(startTime);
4371            osc.stop(startTime + note.duration);
4372
4373            activeOscillators.push(osc);
4374            osc.addEventListener('ended', () => {
4375                const index = activeOscillators.indexOf(osc);
4376                if (index >= 0) activeOscillators.splice(index, 1);
4377                try { osc.disconnect(); } catch (e) {}
4378                try { gain.disconnect(); } catch (e) {}
4379            }, { once: true });
4380        }
4381        startTime += note.duration;
4382    });
4383}
4384
4385function playTone(freq, type, duration, volume) {
4386    playMelody([{ freq, type, duration }], volume);
4387}
4388
4389window.playPomoAlarm = playAlarm;
4390window.stopPomoAlarm = stopAlarm; // Expose for debugging logic
4391
4392/* --- PiP --- */
4393async function requestPiP() {
4394    if (window.Roxima?.isTouchLayout?.() || !('documentPictureInPicture' in window)) {
4395        showMobileMiniTimer();
4396        return;
4397    }
4398
4399    // Re-implement if needed, straightforward copy from previous version
4400    if (!('documentPictureInPicture' in window)) return alert(i18n.t('pomo_not_supported'));
4401    try {
4402        const pipWindow = await window.documentPictureInPicture.requestWindow({ width: 250, height: 96 });
4403        // Style copy...
4404        [...document.styleSheets].forEach((styleSheet) => {
4405            try {
4406                const style = document.createElement('style');
4407                style.textContent = [...styleSheet.cssRules].map(r => r.cssText).join('');
4408                pipWindow.document.head.appendChild(style);
4409            } catch (e) { }
4410        });
4411
4412        const source = containerEl.querySelector('.pomo-container');
4413        source.classList.add('is-pip'); // Add dedicated class for CSS
4414
4415        // Temporarily remove Day Closure button from DOM for PiP mode
4416        const closureBtn = source.querySelector('.pomo-day-closure-container');
4417        if (closureBtn) {
4418            closureBtn.remove();
4419        }
4420
4421        // We need to keep the ORIGINAL in place or settings might break interaction?
4422        // Actually move it is better for state sync.
4423        const placeholder = document.createElement('div');
4424        placeholder.innerText = i18n.t('pomo_mobile_mini_active');
4425        placeholder.style.cssText = 'height:100%; display:flex; align-items:center; justify-content:center; color:#999;';
4426
4427        const parent = source.parentNode;
4428        parent.insertBefore(placeholder, source);
4429        pipWindow.document.body.appendChild(source);
4430        // Attempt transparent background and full width/height
4431        pipWindow.document.body.style.cssText = "display:flex; width:100%; height:100%; margin:0; padding:0; background:transparent; overflow:hidden;";
4432
4433
4434        pipWindow.addEventListener('pagehide', () => {
4435            source.classList.remove('is-pip'); // Remove class on exit
4436
4437            // Restore Day Closure button when returning to main document
4438            if (closureBtn) {
4439                const pipProjectSelector = source.querySelector('#pomo-project-selector-pip');
4440                if (pipProjectSelector) {
4441                    source.insertBefore(closureBtn, pipProjectSelector);
4442                } else {
4443                    source.appendChild(closureBtn);
4444                }
4445            }
4446
4447            placeholder.replaceWith(source);
4448            // Re-bind mode and project buttons back in main window
4449            bindModeButtons(containerEl);
4450            bindProjectNavigation(containerEl);
4451        });
4452
4453        // Bind mode and project buttons in PiP window context
4454        bindModeButtons(source);
4455        bindProjectNavigation(source);
4456
4457        // Bind double Shift shortcut inside PiP window to close it
4458        let pipLastShiftTime = 0;
4459        pipWindow.document.addEventListener('keydown', (e) => {
4460            if (e.key === 'Shift') {
4461                const currentTime = Date.now();
4462                if (currentTime - pipLastShiftTime < 500) {
4463                    e.preventDefault();
4464                    pipWindow.close();
4465                } else {
4466                    pipLastShiftTime = currentTime;
4467                }
4468            } else {
4469                pipLastShiftTime = 0;
4470            }
4471        });
4472
4473        setTimeout(() => {
4474            fetch('/debug-pip', { method: 'POST', body: pipWindow.document.documentElement.outerHTML }).catch(e=>console.log(e));
4475            // Or just write to localstorage
4476            localStorage.setItem('pip_debug_html', pipWindow.document.documentElement.outerHTML);
4477        }, 1000);
4478
4479    } catch (err) { console.error(err); }
4480}
4481
4482function showMobileMiniTimer() {
4483    if (mobileMiniEl) {
4484        mobileMiniEl.classList.toggle('open');
4485        if (mobileMiniEl.classList.contains('open')) updateMobileMiniTimer();
4486        return;
4487    }
4488
4489    mobileMiniEl = document.createElement('div');
4490    mobileMiniEl.className = 'pomo-mobile-mini open';
4491    mobileMiniEl.innerHTML = `
4492        <button class="pomo-mini-close" type="button" data-i18n-aria-label="common_close" aria-label="${i18n.t('common_close')}">x</button>
4493        <div class="pomo-mini-copy">
4494            <span class="pomo-mini-label" data-i18n="pomo_mobile_mini">${i18n.t('pomo_mobile_mini')}</span>
4495            <strong class="pomo-mini-time">25:00</strong>
4496        </div>
4497        <button class="pomo-mini-play" type="button" data-i18n-aria-label="pomo_start" aria-label="${i18n.t('pomo_start')}"></button>
4498    `;
4499
4500    mobileMiniEl.querySelector('.pomo-mini-close')?.addEventListener('click', () => {
4501        mobileMiniEl.classList.remove('open');
4502    });
4503    mobileMiniEl.querySelector('.pomo-mini-play')?.addEventListener('click', toggleTimer);
4504    document.body.appendChild(mobileMiniEl);
4505    updateMobileMiniTimer();
4506    globalCtx.bus.emit('ui:toast', i18n.t('pomo_mobile_mini_active'));
4507}
4508
4509function updateMobileMiniTimer() {
4510    if (!mobileMiniEl) return;
4511
4512    const timeText = formatTimerText(state.timeLeft);
4513    const time = mobileMiniEl.querySelector('.pomo-mini-time');
4514    const play = mobileMiniEl.querySelector('.pomo-mini-play');
4515
4516    if (time) time.textContent = timeText;
4517    if (play) {
4518        const playLabelKey = state.isActive ? 'pomo_pause' : 'pomo_start';
4519        play.setAttribute('data-i18n-aria-label', playLabelKey);
4520        play.setAttribute('aria-label', i18n.t(playLabelKey));
4521        play.innerHTML = state.isActive
4522            ? '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16" rx="2"></rect><rect x="14" y="4" width="4" height="16" rx="2"></rect></svg>'
4523            : '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"></path></svg>';
4524    }
4525}
4526
4527/** Bind click handlers on .badge-pomo mode buttons within a container */
4528function bindModeButtons(root) {
4529    const modes = root.matches?.('.pomo-modes') ? root : root.querySelector?.('.pomo-modes');
4530    if (!modes) return;
4531
4532    if (modes.dataset.pomoModesBound === 'true') return;
4533    modes.dataset.pomoModesBound = 'true';
4534
4535    modes.addEventListener('click', (e) => {
4536        const editBtn = e.target.closest('[data-timer-edit]');
4537        if (editBtn) {
4538            e.stopPropagation();
4539            e.preventDefault();
4540            editBtn.blur?.();
4541            const mode = editBtn.dataset.timerEdit;
4542            const customTimer = getCustomTimer(mode);
4543            if (customTimer) {
4544                openCustomTimerEditor(customTimer.id, editBtn);
4545            } else {
4546                state.settingsOpen = true;
4547                syncSettingsPanelVisibility();
4548
4549                const inputId = mode === 'focus'
4550                    ? '#set-focus-time'
4551                    : mode === 'short-break'
4552                    ? '#set-short-break-time'
4553                    : '#set-long-break-time';
4554
4555                const field = containerEl?.querySelector(inputId);
4556                if (field) {
4557                    window.setTimeout(() => {
4558                        field.focus();
4559                        field.select?.();
4560                    }, 120);
4561                }
4562            }
4563            return;
4564        }
4565
4566        const modeBtn = e.target.closest('.pomo-mode-trigger');
4567        if (modeBtn) {
4568            modeBtn.blur?.();
4569            if (modeBtn.hasAttribute('data-custom-timer-add')) {
4570                openCustomTimerEditor(null, modeBtn);
4571                return;
4572            }
4573            if (modeBtn.dataset.mode) {
4574                setMode(modeBtn.dataset.mode, true);
4575            }
4576        }
4577    });
4578}
4579
4580/** Bind project navigation handlers within a container (essential for PiP context switching) */
4581function bindProjectNavigation(root) {
4582    const prevBtn = root.querySelector('#pomo-project-prev');
4583    const nextBtn = root.querySelector('#pomo-project-next');
4584    const prevPipBtn = root.querySelector('#pomo-project-prev-pip');
4585    const nextPipBtn = root.querySelector('#pomo-project-next-pip');
4586    const iconPipBtn = root.querySelector('#pomo-project-icon-pip');
4587
4588    if (prevBtn) {
4589        const newBtn = prevBtn.cloneNode(true);
4590        prevBtn.parentNode.replaceChild(newBtn, prevBtn);
4591        newBtn.addEventListener('click', (e) => cycleProject(-1, e));
4592    }
4593    if (nextBtn) {
4594        const newBtn = nextBtn.cloneNode(true);
4595        nextBtn.parentNode.replaceChild(newBtn, nextBtn);
4596        newBtn.addEventListener('click', (e) => cycleProject(1, e));
4597    }
4598    if (prevPipBtn) {
4599        const newBtn = prevPipBtn.cloneNode(true);
4600        prevPipBtn.parentNode.replaceChild(newBtn, prevPipBtn);
4601        newBtn.addEventListener('click', (e) => cycleProject(-1, e));
4602    }
4603    if (nextPipBtn) {
4604        const newBtn = nextPipBtn.cloneNode(true);
4605        nextPipBtn.parentNode.replaceChild(newBtn, nextPipBtn);
4606        newBtn.addEventListener('click', (e) => cycleProject(1, e));
4607    }
4608    if (iconPipBtn) {
4609        const newIcon = iconPipBtn.cloneNode(true);
4610        iconPipBtn.parentNode.replaceChild(newIcon, iconPipBtn);
4611        newIcon.addEventListener('click', (e) => cycleProject(1, e));
4612    }
4613}
4614
4615function saveTimerState() {
4616    try {
4617        const now = Date.now();
4618        const lastUpdated = Math.max(now, lastTimerStateUpdatedAt + 1);
4619        if (state.isActive && state.timerOwnerTabId === timerTabId) {
4620            state.timerOwnerHeartbeatAt = now;
4621        }
4622        const endTime = state.isActive
4623            ? (state.sessionEndMs || now + state.timeLeft * 1000)
4624            : null;
4625        const timerState = {
4626            mode: state.mode,
4627            isActive: state.isActive,
4628            timeLeft: state.timeLeft,
4629            endTime: endTime,
4630            activeTask: state.activeTask,
4631            browserFocusSessionId: state.browserFocusSessionId,
4632            focusDurationOverrideSeconds: state.focusDurationOverrideSeconds,
4633            pomoCount: state.pomoCount,
4634            ownerTabId: state.timerOwnerTabId,
4635            ownerHeartbeatAt: state.timerOwnerHeartbeatAt,
4636            lastUpdated
4637        };
4638        lastTimerStateUpdatedAt = lastUpdated;
4639        localStorage.setItem(POMODORO_TIMER_STORAGE_KEY, JSON.stringify(timerState));
4640    } catch (e) {
4641        console.error("Failed to save Pomodoro timer state", e);
4642    }
4643}
4644
4645function applySharedPomodoroTimerState(saved, { restore = false } = {}) {
4646    if (!saved) return false;
4647
4648    state.mode = (MODES[saved.mode] || getCustomTimer(saved.mode)) ? saved.mode : 'focus';
4649    state.pomoCount = saved.pomoCount;
4650    state.activeTask = saved.activeTask;
4651    state.browserFocusSessionId = saved.browserFocusSessionId;
4652    state.focusDurationOverrideSeconds = state.mode === 'focus'
4653        && Number.isFinite(saved.focusDurationOverrideSeconds)
4654        && saved.focusDurationOverrideSeconds > 0
4655        ? saved.focusDurationOverrideSeconds
4656        : null;
4657    state.timerOwnerTabId = saved.ownerTabId || null;
4658    state.timerOwnerHeartbeatAt = saved.ownerHeartbeatAt;
4659
4660    const badge = containerEl.querySelector('#pomo-mode-badge');
4661    syncModeBadge(badge, state.mode);
4662    updateModeButtons();
4663    syncFocusGuide();
4664
4665    if (saved.isActive) {
4666        const claimsOwnership = shouldClaimPomodoroTimerOwnership(saved, timerTabId);
4667        state.isActive = true;
4668        state.isDemoSession = false;
4669        state.timeLeft = saved.timeLeft;
4670        state.sessionEndMs = saved.endTime;
4671        state.isTimerFollower = !claimsOwnership;
4672        state.timerHeartbeatTicks = 0;
4673
4674        if (claimsOwnership) {
4675            state.timerOwnerTabId = timerTabId;
4676            state.timerOwnerHeartbeatAt = Date.now();
4677            saveTimerState();
4678            syncBrowserFocusProtection(true, 'active');
4679        }
4680
4681        updateUI();
4682        updateControls();
4683        startCountdownInterval();
4684        return true;
4685    }
4686
4687    const expiredOwnerCanComplete = saved.expired && restore && (
4688        !saved.ownerTabId
4689        || saved.ownerTabId === timerTabId
4690        || Date.now() - saved.ownerHeartbeatAt > POMODORO_TIMER_OWNER_TIMEOUT_MS
4691    );
4692    if (expiredOwnerCanComplete) {
4693        state.isActive = true;
4694        state.isTimerFollower = false;
4695        state.timerOwnerTabId = timerTabId;
4696        state.timeLeft = 0;
4697        void completeTimer();
4698        return true;
4699    }
4700
4701    state.isActive = false;
4702    state.isTimerFollower = false;
4703    state.timeLeft = saved.timeLeft || getDuration();
4704    state.sessionEndMs = null;
4705    clearInterval(state.interval);
4706    updateUI();
4707    updateControls();
4708    globalCtx?.bus?.emit('timer:stopped');
4709    return true;
4710}
4711
4712function handlePomodoroTimerStorageEvent(event) {
4713    if (event?.key !== POMODORO_TIMER_STORAGE_KEY || !event.newValue) return;
4714    const saved = readSharedPomodoroTimerState(event.newValue);
4715    if (!saved || saved.lastUpdated <= lastTimerStateUpdatedAt) return;
4716
4717    lastTimerStateUpdatedAt = saved.lastUpdated;
4718    if (saved.ownerTabId === timerTabId && state.timerOwnerTabId === timerTabId) return;
4719    applySharedPomodoroTimerState(saved);
4720}
4721
4722function restoreTimerState() {
4723    try {
4724        const raw = localStorage.getItem(POMODORO_TIMER_STORAGE_KEY);
4725        if (!raw) return false;
4726        const saved = readSharedPomodoroTimerState(raw);
4727        if (!saved) return false;
4728        lastTimerStateUpdatedAt = saved.lastUpdated;
4729        return applySharedPomodoroTimerState(saved, { restore: true });
4730    } catch (e) {
4731        console.error("Failed to restore Pomodoro timer state", e);
4732        return false;
4733    }
4734}
4735
4736function releasePomodoroTimerOwnership() {
4737    if (!state.isActive || state.timerOwnerTabId !== timerTabId) return;
4738    state.timerOwnerTabId = null;
4739    state.timerOwnerHeartbeatAt = 0;
4740    state.isTimerFollower = true;
4741    saveTimerState();
4742}
4743
4744window.addEventListener('beforeunload', () => {
4745    releasePomodoroTimerOwnership();
4746});
4747
4748document.addEventListener('visibilitychange', () => {
4749    if (document.visibilityState === 'visible') {
4750        if (refreshDayClosureForCurrentDate()) return;
4751        refreshTwoDayPilotCardForCurrentDay();
4752    }
4753});
4754
4755// --- Z-Order Refocus Fallback for PiP Window (Deactivated to prevent Chrome focus protection/gray screen) ---
4756/*
4757let isRefocusingPiP = false;
4758function handleRefocusPiP() {
4759    if (isRefocusingPiP) return;
4760    if (window.documentPictureInPicture && window.documentPictureInPicture.window) {
4761        const pipWindow = window.documentPictureInPicture.window;
4762        if (!pipWindow.closed) {
4763            isRefocusingPiP = true;
4764            try {
4765                // Focus PiP to bring it on top of other desktop windows, 
4766                // then focus main window to keep keyboard input active
4767                pipWindow.focus();
4768                window.focus();
4769            } catch (e) {
4770                console.error('[PiP Refocus] Failed to bring PiP to front:', e);
4771            } finally {
4772                setTimeout(() => {
4773                    isRefocusingPiP = false;
4774                }, 50);
4775            }
4776        }
4777    }
4778}
4779
4780window.addEventListener('focus', handleRefocusPiP);
4781document.addEventListener('visibilitychange', () => {
4782    if (document.visibilityState === 'visible') {
4783        handleRefocusPiP();
4784    }
4785});
4786*/

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.