Bread Timer

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>The Perfect Loaf Timeline Calculator</title>
    <style>
        body { 
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 
            padding: 20px; 
            max-width: 650px; 
            margin: 0 auto; 
            background: #fdfbf7; 
            color: #333; 
        }
        h1, h2 { color: #8b5a2b; text-align: center; }
        .card { 
            background: white; 
            padding: 20px; 
            border-radius: 12px; 
            box-shadow: 0 4px 6px rgba(0,0,0,0.05); 
            margin-bottom: 20px; 
            border: 1px solid #eee;
        }
        label { display: block; margin-top: 15px; font-weight: bold; color: #555; }
        select, input { 
            width: 100%; 
            padding: 12px; 
            margin-top: 5px; 
            border: 1px solid #ccc; 
            border-radius: 6px; 
            box-sizing: border-box; 
            font-size: 16px; 
        }
        .step-row { 
            display: flex; 
            justify-content: space-between; 
            align-items: center; 
            padding: 14px 0; 
            border-bottom: 1px solid #f0f0f0; 
            flex-wrap: wrap;
            gap: 10px;
        }
        .step-row.overlap {
            background-color: #f7f4eb;
            margin: 4px 0;
            padding: 14px 8px;
            border-radius: 6px;
            border-left: 4px solid #d4b28c;
        }
        .step-info { 
            flex: 1; 
            min-width: 180px; 
        }
        .step-time { 
            flex: 1; 
            min-width: 250px; 
            text-align: right; 
        }
        .step-time input { 
            width: 100%; 
            max-width: 260px; 
            font-size: 15px; 
            padding: 8px;
            text-align: center; 
            background-color: #fff;
        }
        button { 
            background: #8b5a2b; 
            color: white; 
            border: none; 
            padding: 14px; 
            width: 100%; 
            border-radius: 6px; 
            font-size: 16px; 
            font-weight: bold; 
            cursor: pointer; 
            margin-top: 20px; 
        }
        button:hover { background: #6f441f; }
        
        @media (max-width: 480px) {
            .step-time { text-align: left; }
            .step-time input { max-width: 100%; }
        }
    </style>
</head>
<body>

    <h1>Baking Timeline Calculator</h1>
    <h3 style="text-align:center; color:#666; font-weight:normal; margin-top:-10px;">Inspired by <em>The Perfect Loaf</em></h3>

    <div class="card">
        <label for="recipeSelect">Select Recipe:</label>
        <select id="recipeSelect">
            <option value="beginners">The Beginner's Sourdough</option>
            <option value="country">Pain de Campagne</option>
        </select>

        <label for="calcMode">Scheduling Mode:</label>
        <select id="calcMode" onchange="toggleModeInputs()">
            <option value="forward">Forward (Plan from Levain Start)</option>
            <option value="backward">Backward (Plan from Target End Time)</option>
        </select>

        <div id="forwardInputGroup">
            <label for="startTime">Start Date & Time (Mix Levain):</label>
            <input type="datetime-local" id="startTime">
        </div>

        <div id="backwardInputGroup" style="display:none;">
            <label for="endTime">Target End Date & Time (Out of Oven):</label>
            <input type="datetime-local" id="endTime">
        </div>

        <button onclick="calculateInitialTimeline()">Generate Timeline</button>
    </div>

    <div class="card" id="timelineCard" style="display:none;">
        <h2>Your Custom Schedule</h2>
        <p style="font-size: 0.85em; color: #666; text-align: center; margin-bottom: 15px;">
            💡 <strong>Parallel Steps:</strong> Levain and Autolyse run together. Adjusting <em>Begin Mix & Knead</em> shifts all future steps if your levain is slow. Adjusting <em>Divide & Pre-shape</em> updates your schedule if bulk fermentation speeds up or slows down.
        </p>
        <div id="timelineSteps"></div>
    </div>

    <script>
        // Durations define how long THAT specific step lasts before the next action occurs.
        const recipes = {
            beginners: [
                { name: "Start Levain Build", duration: 300, desc: "Mix starter, flour, and water", isOverlap: true }, // 5h
                { name: "Start Autolyse", duration: 60, desc: "Mix flour and water; rest until levain ready", isOverlap: true },    // 1h
                { name: "Begin Mix & Knead", duration: 30, desc: "Combine levain, autolyse, salt, and water" },
                { name: "Begin Bulk Fermentation", duration: 240, desc: "Perform stretch & folds throughout" },
                { name: "Divide, Pre-shape & Rest", duration: 50, desc: "Divide dough and let rest on bench" },
                { name: "Final Shape & Cold Proof", duration: 960, desc: "Shape into bannetons and retard in fridge" }, 
                { name: "Bake", duration: 50, desc: "Bake in preheated combo cooker / Dutch oven" },
                { name: "Out of Oven 🎉", duration: 0, desc: "Cool on a wire rack before slicing!" }
            ],
            country: [
                { name: "Start Levain Build", duration: 300, desc: "Mix build parameters", isOverlap: true },
                { name: "Start Autolyse", duration: 45, desc: "Mix flour and water ahead of mix", isOverlap: true },
                { name: "Begin Mix & Knead", duration: 20, desc: "Incorporate levain and salt" },
                { name: "Begin Bulk Fermentation", duration: 270, desc: "Ferment with ambient structural folds" },
                { name: "Divide & Shape", duration: 30, desc: "Divide and shape cleanly" },
                { name: "Cold Proof", duration: 840, desc: "Place in cold storage overnight" },
                { name: "Bake", duration: 50, desc: "Bake until deep mahogany crust forms" },
                { name: "Out of Oven 🎉", duration: 0, desc: "Loaf complete!" }
            ]
        };

        window.onload = function() {
            const now = new Date();
            now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
            document.getElementById('startTime').value = now.toISOString().slice(0, 16);
            
            const tomorrow = new Date();
            tomorrow.setDate(tomorrow.getDate() + 1);
            tomorrow.setMinutes(tomorrow.getMinutes() - tomorrow.getTimezoneOffset());
            document.getElementById('endTime').value = tomorrow.toISOString().slice(0, 16);
        };

        function toggleModeInputs() {
            const mode = document.getElementById('calcMode').value;
            document.getElementById('forwardInputGroup').style.display = mode === 'forward' ? 'block' : 'none';
            document.getElementById('backwardInputGroup').style.display = mode === 'backward' ? 'block' : 'none';
        }

        let currentTimelineData = [];

        function calculateInitialTimeline() {
            const recipeKey = document.getElementById('recipeSelect').value;
            const steps = recipes[recipeKey];
            const mode = document.getElementById('calcMode').value;
            
            currentTimelineData = [];
            steps.forEach(s => currentTimelineData.push({ name: s.name, desc: s.desc, duration: s.duration, isOverlap: s.isOverlap, time: null }));

            if (mode === 'forward') {
                const startVal = document.getElementById('startTime').value;
                if (!startVal) return alert("Please pick a start time.");
                
                let levainStart = new Date(startVal);
                let levainDuration = steps[0].duration;
                let autolyseDuration = steps[1].duration;
                
                // The magic meeting point where both parallel tracks finish
                let mixStart = new Date(levainStart.getTime() + levainDuration * 60000);
                let autolyseStart = new Date(mixStart.getTime() - autolyseDuration * 60000);
                
                currentTimelineData[0].time = levainStart;
                currentTimelineData[1].time = autolyseStart;
                currentTimelineData[2].time = mixStart;
                
                let runningTime = new Date(mixStart);
                for (let i = 3; i < steps.length; i++) {
                    runningTime = new Date(runningTime.getTime() + steps[i-1].duration * 60000);
                    currentTimelineData[i].time = new Date(runningTime);
                }
            } else {
                const endVal = document.getElementById('endTime').value;
                if (!endVal) return alert("Please pick a target end time.");
                
                let runningTime = new Date(endVal);
                currentTimelineData[steps.length - 1].time = new Date(runningTime);
                
                // Work backward from out-of-oven to mix step
                for (let i = steps.length - 2; i >= 2; i--) {
                    runningTime = new Date(runningTime.getTime() - steps[i].duration * 60000);
                    currentTimelineData[i].time = new Date(runningTime);
                }
                
                // Calculate parallel track starting points based on target mix time
                let mixStart = currentTimelineData[2].time;
                currentTimelineData[1].time = new Date(mixStart.getTime() - steps[1].duration * 60000);
                currentTimelineData[0].time = new Date(mixStart.getTime() - steps[0].duration * 60000);
            }

            renderTimeline();
        }

        function renderTimeline() {
            const container = document.getElementById('timelineSteps');
            container.innerHTML = '';
            
            currentTimelineData.forEach((step, index) => {
                const row = document.createElement('div');
                row.className = 'step-row' + (step.isOverlap ? ' overlap' : '');
                
                const info = document.createElement('div');
                info.className = 'step-info';
                
                let durationLabel = step.duration > 0 ? `Duration: ${formatDuration(step.duration)}` : 'Milestone';
                info.innerHTML = `<strong>${step.name}</strong><br><small style="color:#666;">${step.desc}</small><br><small style="color:#999; font-style:italic;">${durationLabel}</small>`;
                
                const timeDiv = document.createElement('div');
                timeDiv.className = 'step-time';
                
                const input = document.createElement('input');
                input.type = 'datetime-local';
                
                const localTime = new Date(step.time.getTime() - step.time.getTimezoneOffset() * 60000);
                input.value = localTime.toISOString().slice(0, 16);
                
                input.onchange = function() {
                    recalculateFromStep(index, new Date(this.value));
                };
                
                timeDiv.appendChild(input);
                row.appendChild(info);
                row.appendChild(timeDiv);
                container.appendChild(row);
            });
            
            document.getElementById('timelineCard').style.display = 'block';
        }

        function recalculateFromStep(changedIndex, newTime) {
            currentTimelineData[changedIndex].time = new Date(newTime);
            const recipeKey = document.getElementById('recipeSelect').value;
            const steps = recipes[recipeKey];

            if (changedIndex === 0) {
                // Changing Levain Start recalculates everything forward
                let levainStart = currentTimelineData[0].time;
                let mixStart = new Date(levainStart.getTime() + steps[0].duration * 60000);
                currentTimelineData[1].time = new Date(mixStart.getTime() - steps[1].duration * 60000);
                currentTimelineData[2].time = mixStart;
                
                let runningTime = new Date(mixStart);
                for (let i = 3; i < steps.length; i++) {
                    runningTime = new Date(runningTime.getTime() + steps[i-1].duration * 60000);
                    currentTimelineData[i].time = new Date(runningTime);
                }
            } 
            else if (changedIndex === 1) {
                // If you shift Autolyse Start, it forces a shift in the target Mix Time
                let autolyseStart = currentTimelineData[1].time;
                let mixStart = new Date(autolyseStart.getTime() + steps[1].duration * 60000);
                currentTimelineData[2].time = mixStart;
                
                let runningTime = new Date(mixStart);
                for (let i = 3; i < steps.length; i++) {
                    runningTime = new Date(runningTime.getTime() + steps[i-1].duration * 60000);
                    currentTimelineData[i].time = new Date(runningTime);
                }
            } 
            else if (changedIndex === 2) {
                // Crucial: Changing Mix Time (e.g. Levain took longer) shifts everything downstream
                let mixStart = currentTimelineData[2].time;
                currentTimelineData[1].time = new Date(mixStart.getTime() - steps[1].duration * 60000);
                
                let runningTime = new Date(mixStart);
                for (let i = 3; i < steps.length; i++) {
                    runningTime = new Date(runningTime.getTime() + steps[i-1].duration * 60000);
                    currentTimelineData[i].time = new Date(runningTime);
                }
            } 
            else {
                // Changing any step downstream (e.g., Bulk Fermentation ending late) ripples forward
                let runningTime = new Date(newTime);
                for (let i = changedIndex + 1; i < steps.length; i++) {
                    runningTime = new Date(runningTime.getTime() + steps[i-1].duration * 60000);
                    currentTimelineData[i].time = new Date(runningTime);
                }
            }

            renderTimeline();
        }

        function formatDuration(mins) {
            if (mins < 60) return `${mins} mins`;
            const hours = Math.floor(mins / 60);
            const remainingMins = mins % 60;
            return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
        }
    </script>
</body>
</html>