diff --git a/assets/app.js b/assets/app.js
index 2fcbda8..d944139 100644
--- a/assets/app.js
+++ b/assets/app.js
@@ -1,5 +1,7 @@
// --- Data Init & Colors ---
let projects = JSON.parse(localStorage.getItem('crochetCounters')) || [];
+let patterns = JSON.parse(localStorage.getItem('crochetPatterns')) || [];
+if (!Array.isArray(patterns)) patterns = [];
// New Earthy/Woodland Palette extracted from image vibes
const colors = [
'#a17d63', // Soft oak
@@ -41,6 +43,9 @@ const colorGrid = document.getElementById('colorGrid');
const customColorInput = document.getElementById('customColorInput');
const saveOverlay = document.getElementById('saveOverlay');
const saveList = document.getElementById('saveList');
+const patternPicker = document.getElementById('patternPicker');
+const patternSelect = document.getElementById('patternSelect');
+if (patternPicker && patternSelect) populatePatternSelect();
let pendingSaveSelection = [];
let lastCountPulse = null;
let lastFinishedId = null;
@@ -230,11 +235,23 @@ function closeColorPicker() {
colorOverlay.dataset.partId = '';
}
+function savePatterns() {
+ localStorage.setItem('crochetPatterns', JSON.stringify(patterns));
+}
+
+function populatePatternSelect() {
+ if (!patternPicker || !patternSelect) return;
+ const hasPatterns = patterns.length > 0;
+ patternPicker.style.display = hasPatterns ? 'block' : 'none';
+ patternSelect.innerHTML = '' + patterns.map(p => ``).join('');
+}
+
function exportData(selectedProjects = projects) {
const payload = {
projects: selectedProjects,
isDarkMode,
- animationsEnabled
+ animationsEnabled,
+ patterns
};
const names = selectedProjects.map(p => p.name || 'Project').join('_').replace(/\s+/g, '-').slice(0, 50) || 'projects';
const filename = `toadstool_${names}.json`;
@@ -271,6 +288,10 @@ async function handleImport(event) {
animationsEnabled = data.animationsEnabled;
localStorage.setItem('crochetAnimations', animationsEnabled);
}
+ if (Array.isArray(data.patterns)) {
+ patterns = data.patterns;
+ localStorage.setItem('crochetPatterns', JSON.stringify(patterns));
+ }
localStorage.setItem('crochetCounters', JSON.stringify(projects));
applyTheme();
render();
@@ -618,16 +639,22 @@ document.addEventListener('visibilitychange', async () => {
lastCountPulse = { partId, dir: change > 0 ? 'up' : 'down' };
save();
}
- async function resetCount(pId, partId) {
- const project = projects.find(p => p.id === pId);
- const part = project.parts.find(pt => pt.id === partId);
- if (part.locked || part.finished) return;
- const ok = await showConfirm({ title: 'Reset count?', text: 'Set this count back to zero.', confirmText: 'Reset', danger: true });
- if(ok) {
- part.count = 0;
- save();
- }
+async function resetCount(pId, partId) {
+ const project = projects.find(p => p.id === pId);
+ const part = project.parts.find(pt => pt.id === partId);
+ if (part.locked || part.finished) return;
+ const ok = await showConfirm({ title: 'Reset count?', text: 'Set this count back to zero.', confirmText: 'Reset', danger: true });
+ if(ok) {
+ part.count = 0;
+ save();
}
+}
+
+function saveProjectAsPattern(pId) {
+ const project = projects.find(p => p.id === pId);
+ if (!project) return;
+ openModal('savePattern', pId);
+}
// --- Modal Logic ---
function openModal(type, pId = null, partId = null) {
@@ -637,6 +664,10 @@ function openModal(type, pId = null, partId = null) {
if (type === 'addProject') {
modalTitle.innerText = "New Project Name";
modalInput.type = "text"; modalInput.placeholder = "e.g., Amigurumi Bear";
+ if (patternPicker && patternSelect) {
+ populatePatternSelect();
+ patternSelect.value = '';
+ }
} else if (type === 'addPart') {
modalTitle.innerText = "New Part Name";
modalInput.type = "text"; modalInput.placeholder = "e.g., Head";
@@ -651,6 +682,11 @@ function openModal(type, pId = null, partId = null) {
if(part.locked || part.finished) return;
modalTitle.innerText = "Rename Part";
modalInput.value = part.name; modalInput.type = "text";
+ } else if (type === 'savePattern') {
+ modalTitle.innerText = "Save as Pattern";
+ const project = projects.find(p => p.id === pId);
+ modalInput.value = project ? `${project.name} pattern` : '';
+ modalInput.type = "text"; modalInput.placeholder = "Pattern name";
} else if (type === 'manualCount') {
const part = projects.find(p => p.id === pId).parts.find(pt => pt.id === partId);
if(part.locked || part.finished) return;
@@ -661,6 +697,9 @@ function openModal(type, pId = null, partId = null) {
const part = projects.find(p => p.id === pId).parts.find(pt => pt.id === partId);
if (part.locked) return;
}
+ if (type !== 'addProject' && patternPicker) {
+ patternPicker.style.display = 'none';
+ }
modal.classList.add('active');
setTimeout(() => modalInput.focus(), 100);
}
@@ -674,12 +713,31 @@ function closeModal() {
if (modalState.type === 'addProject') {
const nextColor = colors[projects.length % colors.length];
- projects.push({ id: Date.now(), name: val, color: nextColor, collapsed: false, parts: [] });
- projects[projects.length-1].parts.push({ id: Date.now() + 1, name: 'Part 1', count: 0, locked: false, finished: false, minimized: false, max: null, color: nextColor });
+ const newProject = { id: Date.now(), name: val, color: nextColor, collapsed: false, note: '', parts: [] };
+ const selectedPatternId = patternSelect ? patternSelect.value : '';
+ const chosenPattern = selectedPatternId ? patterns.find(p => String(p.id) === selectedPatternId) : null;
+ if (chosenPattern && Array.isArray(chosenPattern.parts) && chosenPattern.parts.length) {
+ chosenPattern.parts.forEach((pt, idx) => {
+ newProject.parts.push({
+ id: Date.now() + idx + 1,
+ name: pt.name || `Part ${idx + 1}`,
+ count: 0,
+ locked: false,
+ finished: false,
+ minimized: false,
+ max: pt.max ?? null,
+ color: pt.color || newProject.color,
+ note: ''
+ });
+ });
+ } else {
+ newProject.parts.push({ id: Date.now() + 1, name: 'Part 1', count: 0, locked: false, finished: false, minimized: false, max: null, color: nextColor, note: '' });
+ }
+ projects.push(newProject);
}
else if (modalState.type === 'addPart') {
const project = projects.find(p => p.id === modalState.pId);
- project.parts.push({ id: Date.now(), name: val, count: 0, locked: false, finished: false, minimized: false, max: null, color: project.color });
+ project.parts.push({ id: Date.now(), name: val, count: 0, locked: false, finished: false, minimized: false, max: null, color: project.color, note: '' });
project.collapsed = false;
}
else if (modalState.type === 'renamePart') {
@@ -687,9 +745,9 @@ function closeModal() {
part.name = val;
}
else if (modalState.type === 'renameProject') {
- const project = projects.find(p => p.id === modalState.pId);
- project.name = val;
- }
+ const project = projects.find(p => p.id === modalState.pId);
+ project.name = val;
+ }
else if (modalState.type === 'manualCount') {
const num = parseInt(val);
if (!isNaN(num) && num >= 0) {
@@ -710,6 +768,15 @@ function closeModal() {
}
}
}
+ else if (modalState.type === 'savePattern') {
+ const project = projects.find(p => p.id === modalState.pId);
+ if (project) {
+ const template = project.parts.map(pt => ({ name: pt.name, color: pt.color, max: pt.max }));
+ patterns.push({ id: Date.now(), name: val || project.name, color: project.color, parts: template });
+ savePatterns();
+ populatePatternSelect();
+ }
+ }
save();
closeModal();
}
@@ -824,6 +891,7 @@ function render() {
+
diff --git a/assets/app.min.js b/assets/app.min.js
new file mode 100644
index 0000000..2823c13
--- /dev/null
+++ b/assets/app.min.js
@@ -0,0 +1 @@
+let projects=JSON.parse(localStorage.getItem("crochetCounters"))||[];let patterns=JSON.parse(localStorage.getItem("crochetPatterns"))||[];if(!Array.isArray(patterns))patterns=[];const colors=["#a17d63","#7a8c6a","#c7a272","#b88b8a","#7b9189","#aa9a7a","#5f6d57","#b07d6f","#6d5947","#c4b08a","#7c7565","#8ca58c"];const oldColors=["#b56b54","#7a8b4f","#cba052","#5f8a8b","#8c6246","#a87b8c","#4a5d43","#9c7e63"];let modalState={type:null,projectId:null,partId:null};const app=document.getElementById("app");const modal=document.getElementById("modalOverlay");const modalInput=document.getElementById("modalInput");const modalTitle=document.getElementById("modalTitle");const hapticTick=()=>{if("vibrate"in navigator)navigator.vibrate(12)};const installBtn=document.getElementById("installBtn");const importInput=document.getElementById("importFile");const motionBtn=document.getElementById("motionBtn");const colorOverlay=document.getElementById("colorOverlay");const colorGrid=document.getElementById("colorGrid");const customColorInput=document.getElementById("customColorInput");const saveOverlay=document.getElementById("saveOverlay");const saveList=document.getElementById("saveList");const patternPicker=document.getElementById("patternPicker");const patternSelect=document.getElementById("patternSelect");if(patternPicker&&patternSelect)populatePatternSelect();let pendingSaveSelection=[];let lastCountPulse=null;let lastFinishedId=null;let fireflyTimer=null;let fireflyActive=false;let titleClicks=[];let easterEggCooling=false;if("serviceWorker"in navigator){window.addEventListener("load",(()=>{navigator.serviceWorker.register("/sw.js").catch((()=>{}))}))}let deferredInstallPrompt=null;const isStandalone=()=>window.matchMedia("(display-mode: standalone)").matches||window.navigator.standalone===true;function hideInstall(){if(installBtn)installBtn.classList.add("hidden")}function showInstall(){if(installBtn)installBtn.classList.remove("hidden")}window.addEventListener("beforeinstallprompt",(e=>{e.preventDefault();deferredInstallPrompt=e;if(!isStandalone())showInstall()}));window.addEventListener("appinstalled",(()=>{deferredInstallPrompt=null;hideInstall()}));if(installBtn){installBtn.addEventListener("click",(async()=>{if(!deferredInstallPrompt)return;deferredInstallPrompt.prompt();const choice=await deferredInstallPrompt.userChoice;if(choice.outcome==="accepted")hideInstall();deferredInstallPrompt=null}))}if(isStandalone())hideInstall();function removeSwal(){const existing=document.querySelector(".swal-overlay");if(existing)existing.remove()}function showConfirm({title:title="Are you sure?",text:text="",confirmText:confirmText="Yes",cancelText:cancelText="Cancel",danger:danger=false}={}){return new Promise((resolve=>{removeSwal();const overlay=document.createElement("div");overlay.className="swal-overlay";overlay.innerHTML=`\n \n
${title}
\n
${text}
\n
\n \n \n
\n
\n `;const cancelBtn=overlay.querySelector(".swal-cancel");const confirmBtn=overlay.querySelector(".swal-confirm, .swal-danger");cancelBtn.onclick=()=>{removeSwal();resolve(false)};confirmBtn.onclick=()=>{removeSwal();resolve(true)};overlay.addEventListener("click",(e=>{if(e.target===overlay){removeSwal();resolve(false)}}));document.addEventListener("keydown",(function onKey(e){if(e.key==="Escape"){removeSwal();resolve(false);document.removeEventListener("keydown",onKey)}}));document.body.appendChild(overlay)}))}function showAlert({title:title="Notice",text:text=""}={}){return new Promise((resolve=>{removeSwal();const overlay=document.createElement("div");overlay.className="swal-overlay";overlay.innerHTML=`\n \n
${title}
\n
${text}
\n
\n \n
\n
\n `;const okBtn=overlay.querySelector(".swal-confirm");okBtn.onclick=()=>{removeSwal();resolve()};overlay.addEventListener("click",(e=>{if(e.target===overlay){removeSwal();resolve()}}));document.addEventListener("keydown",(function onKey(e){if(e.key==="Escape"){removeSwal();resolve();document.removeEventListener("keydown",onKey)}}));document.body.appendChild(overlay)}))}let isDarkMode=JSON.parse(localStorage.getItem("crochetDarkMode"));if(isDarkMode===null){if(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches){isDarkMode=true}else{isDarkMode=false}}let animationsEnabled=JSON.parse(localStorage.getItem("crochetAnimations"));if(animationsEnabled===null)animationsEnabled=true;function applyTheme(){if(isDarkMode){document.body.classList.add("dark-mode");document.getElementById("themeBtn").innerHTML=''}else{document.body.classList.remove("dark-mode");document.getElementById("themeBtn").innerHTML=''}handleAmbientDrift();updateMotionBtn()}function toggleTheme(){isDarkMode=!isDarkMode;localStorage.setItem("crochetDarkMode",isDarkMode);applyTheme();if(animationsEnabled){document.body.classList.add("theme-animating");setTimeout((()=>document.body.classList.remove("theme-animating")),750)}}applyTheme();function updateMotionBtn(){if(!motionBtn)return;motionBtn.innerHTML=animationsEnabled?'':'';motionBtn.title=animationsEnabled?"Toggle Animations":"Animations disabled"}function toggleAnimations(){animationsEnabled=!animationsEnabled;localStorage.setItem("crochetAnimations",animationsEnabled);updateMotionBtn();if(!animationsEnabled){stopAmbientDrift();document.body.classList.remove("theme-animating")}else{handleAmbientDrift()}}function openColorPicker(pId,partId){if(!colorOverlay||!colorGrid)return;const project=projects.find((p=>p.id===pId));const part=project.parts.find((pt=>pt.id===partId));colorGrid.innerHTML=colors.map((c=>`\n \n `)).join("");if(customColorInput){customColorInput.value=part.color||project.color||colors[0];customColorInput.oninput=e=>{setPartColor(pId,partId,e.target.value)}}colorOverlay.classList.add("active");colorOverlay.dataset.projectId=pId;colorOverlay.dataset.partId=partId}function closeColorPicker(){if(!colorOverlay)return;colorOverlay.classList.remove("active");colorGrid.innerHTML="";colorOverlay.dataset.projectId="";colorOverlay.dataset.partId=""}function savePatterns(){localStorage.setItem("crochetPatterns",JSON.stringify(patterns))}function populatePatternSelect(){if(!patternPicker||!patternSelect)return;const hasPatterns=patterns.length>0;patternPicker.style.display=hasPatterns?"block":"none";patternSelect.innerHTML=''+patterns.map((p=>``)).join("")}function exportData(selectedProjects=projects){const payload={projects:selectedProjects,isDarkMode:isDarkMode,animationsEnabled:animationsEnabled,patterns:patterns};const names=selectedProjects.map((p=>p.name||"Project")).join("_").replace(/\s+/g,"-").slice(0,50)||"projects";const filename=`toadstool_${names}.json`;const blob=new Blob([JSON.stringify(payload,null,2)],{type:"application/json"});const url=URL.createObjectURL(blob);const a=document.createElement("a");a.href=url;a.download=filename;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url)}function triggerImport(){if(!importInput)return;importInput.value="";importInput.click()}async function handleImport(event){const file=event.target.files[0];if(!file)return;try{const text=await file.text();const data=JSON.parse(text);if(!data.projects||!Array.isArray(data.projects))throw new Error("Invalid file");projects=data.projects;if(typeof data.isDarkMode==="boolean"){isDarkMode=data.isDarkMode;localStorage.setItem("crochetDarkMode",isDarkMode)}if(typeof data.animationsEnabled==="boolean"){animationsEnabled=data.animationsEnabled;localStorage.setItem("crochetAnimations",animationsEnabled)}if(Array.isArray(data.patterns)){patterns=data.patterns;localStorage.setItem("crochetPatterns",JSON.stringify(patterns))}localStorage.setItem("crochetCounters",JSON.stringify(projects));applyTheme();render()}catch(err){showAlert({title:"Import failed",text:err.message})}event.target.value=""}if(importInput){importInput.addEventListener("change",handleImport)}function openSaveModal(){if(!saveOverlay||!saveList)return;saveList.innerHTML="";pendingSaveSelection=projects.map((p=>p.id));projects.forEach((p=>{const item=document.createElement("label");item.className="save-item";item.innerHTML=`\n \n ${p.name}\n `;saveList.appendChild(item)}));saveOverlay.classList.add("active")}function closeSaveModal(){if(!saveOverlay)return;saveOverlay.classList.remove("active");saveList.innerHTML=""}function exportSelected(){if(!saveOverlay)return;const inputs=saveList.querySelectorAll('input[type="checkbox"]');const selectedIds=Array.from(inputs).filter((i=>i.checked)).map((i=>Number(i.dataset.id)));if(selectedIds.length===0){closeSaveModal();return}const selectedProjects=projects.filter((p=>selectedIds.includes(p.id)));exportData(selectedProjects);closeSaveModal()}function spawnFirefly({markActive:markActive=false,source:source="ambient",side:side="any"}={}){const wrap=document.createElement("div");wrap.className="firefly-wrap";const el=document.createElement("div");el.className="firefly";const top=Math.random()*55+5;const scale=.9+Math.random()*.4;const duration=12+Math.random()*8;const chosenSide=side==="any"?["left","right","top","bottom"][Math.floor(Math.random()*4)]:side;let startX="-10vw",endX="110vw",startY=`${top}vh`,endY=`${top+(Math.random()*12-6)}vh`;let midX="25vw",midY=`${top-6}vh`,mid2X="65vw",mid2Y=`${top+6}vh`;if(chosenSide==="right"){startX="110vw";endX="-10vw";midX="-25vw";mid2X="-65vw"}else if(chosenSide==="top"){const x=Math.random()*80+10;startX=`${x}vw`;endX=`${x+(Math.random()*20-10)}vw`;startY="-12vh";endY="110vh";midX=`${x+8}vw`;mid2X=`${x-8}vw`;midY="25vh";mid2Y="65vh"}else if(chosenSide==="bottom"){const x=Math.random()*80+10;startX=`${x}vw`;endX=`${x+(Math.random()*20-10)}vw`;startY="110vh";endY="-12vh";midX=`${x-8}vw`;mid2X=`${x+8}vw`;midY="75vh";mid2Y="35vh"}wrap.style.setProperty("--fly-scale",scale);wrap.style.setProperty("--fly-duration",`${duration}s`);wrap.style.setProperty("--fly-start-x",startX);wrap.style.setProperty("--fly-start-y",startY);wrap.style.setProperty("--fly-mid-x",midX);wrap.style.setProperty("--fly-mid-y",midY);wrap.style.setProperty("--fly-mid2-x",mid2X);wrap.style.setProperty("--fly-mid2-y",mid2Y);wrap.style.setProperty("--fly-end-x",endX);wrap.style.setProperty("--fly-end-y",endY);if(markActive)fireflyActive=true;wrap.addEventListener("animationend",(e=>{if(e.animationName!=="fireflyGlide")return;wrap.remove();if(markActive)fireflyActive=false}));wrap.appendChild(el);document.body.appendChild(wrap)}function spawnSeed({markActive:markActive=false,source:source="ambient"}={}){const wrap=document.createElement("div");wrap.className="seed-wrap";const el=document.createElement("div");el.className="seed";const top=Math.random()*55+5;const scale=.85+Math.random()*.4;const duration=14+Math.random()*8;const tilt=(Math.random()*16+8)*(Math.random()<.5?-1:1);const sway=4+Math.random()*6;const flipDur=5+Math.random()*4;const dir=["left","right","top"][Math.floor(Math.random()*3)];const fromLeft=dir==="left";let start=fromLeft?"-12vw":"112vw";let mid=fromLeft?"30vw":"-30vw";let end=fromLeft?"112vw":"-12vw";if(dir==="top"){const x=Math.random()*80+10;start=`${x}vw`;mid=`${x+(Math.random()*10-5)}vw`;end=`${x+(Math.random()*20-10)}vw`;wrap.style.top="-12vh"}else{wrap.style.top=`${top}vh`}wrap.style.setProperty("--seed-scale",scale);wrap.style.setProperty("--seed-duration",`${duration}s`);wrap.style.setProperty("--seed-tilt",`${tilt}deg`);wrap.style.setProperty("--seed-sway",`${sway}px`);wrap.style.setProperty("--seed-flip-duration",`${flipDur}s`);wrap.style.setProperty("--seed-start",start);wrap.style.setProperty("--seed-mid",mid);wrap.style.setProperty("--seed-end",end);if(markActive)fireflyActive=true;wrap.addEventListener("animationend",(e=>{if(e.animationName!=="seedGlide")return;wrap.remove();if(markActive)fireflyActive=false}));wrap.appendChild(el);document.body.appendChild(wrap)}function stopAmbientDrift(){if(fireflyTimer){clearTimeout(fireflyTimer);fireflyTimer=null}document.querySelectorAll(".firefly-wrap").forEach((el=>el.remove()));document.querySelectorAll(".seed-wrap").forEach((el=>el.remove()));fireflyActive=false}function scheduleAmbientDrift(){const delay=1e4+Math.random()*1e4;fireflyTimer=setTimeout((()=>{if(!animationsEnabled){stopAmbientDrift();return}const selector=isDarkMode?".firefly-wrap":".seed-wrap";let existing=document.querySelectorAll(selector).length;if(existing===0){isDarkMode?spawnFirefly():spawnSeed();existing++}else if(existing<5){isDarkMode?spawnFirefly():spawnSeed()}scheduleAmbientDrift()}),delay)}function handleAmbientDrift(){stopAmbientDrift();if(!animationsEnabled)return;if(isDarkMode){spawnFirefly()}else{spawnSeed()}scheduleAmbientDrift()}handleAmbientDrift();const logoIcon=document.querySelector(".brand-icon");if(logoIcon){logoIcon.addEventListener("click",(()=>{if(!animationsEnabled||fireflyActive)return;if(isDarkMode){spawnFirefly({markActive:true,source:"logo",side:"any"})}else{spawnSeed({markActive:true,source:"logo"})}}))}if(colorOverlay){colorOverlay.addEventListener("click",(e=>{if(e.target===colorOverlay)closeColorPicker()}))}document.addEventListener("keydown",(e=>{if(e.key==="Escape"&&colorOverlay&&colorOverlay.classList.contains("active")){closeColorPicker()}}));const importBtn=document.getElementById("importBtn");if(importBtn){importBtn.addEventListener("click",triggerImport)}const titleEl=document.getElementById("appTitle");if(titleEl){titleEl.addEventListener("click",(()=>{const now=Date.now();titleClicks=titleClicks.filter((ts=>now-ts<7e3));titleClicks.push(now);if(titleClicks.length>=5&&!easterEggCooling){easterEggCooling=true;triggerBurst();setTimeout((()=>{easterEggCooling=false;titleClicks=[]}),8e3)}}))}function triggerBurst(){if(!animationsEnabled)return;const burstCount=isDarkMode?24:18;const spawner=isDarkMode?opts=>spawnFirefly({...opts,side:"any"}):spawnSeed;for(let i=0;ispawner({source:"burst"})),i*140+jitter)}}let wakeLock=null;let isFocusMode=false;const focusBtn=document.getElementById("focusBtn");if(projects.length>0){let changed=false;projects.forEach(((p,index)=>{if(!p.parts){p.parts=[];changed=true}if(!p.color){p.color=colors[index%colors.length];changed=true}const oldIdx=oldColors.indexOf(p.color);if(oldIdx!==-1){p.color=colors[oldIdx%colors.length];changed=true}if(p.note===undefined){p.note="";changed=true}p.parts.forEach((pt=>{if(pt.max===undefined){pt.max=null;changed=true}if(pt.note===undefined){pt.note="";changed=true}if(!pt.color){pt.color=p.color;changed=true}const oldPartIdx=oldColors.indexOf(pt.color);if(oldPartIdx!==-1){pt.color=colors[oldPartIdx%colors.length];changed=true}}))}));if(changed){localStorage.setItem("crochetCounters",JSON.stringify(projects))}}function save(){localStorage.setItem("crochetCounters",JSON.stringify(projects));render()}async function toggleFocusMode(){if(!isFocusMode){try{if(document.documentElement.requestFullscreen)await document.documentElement.requestFullscreen();if("wakeLock"in navigator){wakeLock=await navigator.wakeLock.request("screen")}isFocusMode=true;focusBtn.classList.add("is-active")}catch(err){showAlert({title:"Focus Mode failed",text:err.message})}}else{if(document.fullscreenElement)document.exitFullscreen();if(wakeLock!==null){wakeLock.release();wakeLock=null}isFocusMode=false;focusBtn.classList.remove("is-active")}}document.addEventListener("visibilitychange",(async()=>{if(wakeLock!==null&&document.visibilityState==="visible"){wakeLock=await navigator.wakeLock.request("screen")}}));async function deleteProject(pId){const ok=await showConfirm({title:"Delete project?",text:"This will remove the entire project.",confirmText:"Delete",danger:true});if(ok){projects=projects.filter((p=>p.id!==pId));save()}}function toggleProjectCollapse(pId){const project=projects.find((p=>p.id===pId));project.collapsed=!project.collapsed;save()}function renameProject(pId){modalState={type:"renameProject",pId:pId,partId:null};const project=projects.find((p=>p.id===pId));modalTitle.innerText="Rename Project";modalInput.value=project.name;modalInput.type="text";modalInput.placeholder="Project name";modal.classList.add("active");setTimeout((()=>modalInput.focus()),100)}async function deletePart(pId,partId){const project=projects.find((p=>p.id===pId));const part=project.parts.find((pt=>pt.id===partId));if(part.locked)return;const ok=await showConfirm({title:"Delete part?",text:"This part will be removed.",confirmText:"Delete",danger:true});if(ok){project.parts=project.parts.filter((pt=>pt.id!==partId));save()}}function togglePartMinimize(pId,partId){const project=projects.find((p=>p.id===pId));const part=project.parts.find((pt=>pt.id===partId));part.minimized=!part.minimized;save()}function togglePartLock(pId,partId){const project=projects.find((p=>p.id===pId));const part=project.parts.find((pt=>pt.id===partId));part.locked=!part.locked;save()}function setPartColor(pId,partId,color){const project=projects.find((p=>p.id===pId));const part=project.parts.find((pt=>pt.id===partId));part.color=color;save()}function togglePartFinish(pId,partId){const project=projects.find((p=>p.id===pId));const part=project.parts.find((pt=>pt.id===partId));part.finished=!part.finished;if(part.finished){part.locked=false;lastFinishedId=part.id}else{lastFinishedId=null}save()}function updateCount(pId,partId,change){const project=projects.find((p=>p.id===pId));const part=project.parts.find((pt=>pt.id===partId));if(part.locked||part.finished)return;part.count+=change;if(part.max!==null&&part.count>part.max)part.count=part.max;if(part.count<0)part.count=0;hapticTick();lastCountPulse={partId:partId,dir:change>0?"up":"down"};save()}async function resetCount(pId,partId){const project=projects.find((p=>p.id===pId));const part=project.parts.find((pt=>pt.id===partId));if(part.locked||part.finished)return;const ok=await showConfirm({title:"Reset count?",text:"Set this count back to zero.",confirmText:"Reset",danger:true});if(ok){part.count=0;save()}}function saveProjectAsPattern(pId){const project=projects.find((p=>p.id===pId));if(!project)return;openModal("savePattern",pId)}function openModal(type,pId=null,partId=null){modalState={type:type,pId:pId,partId:partId};modalInput.value="";if(type==="addProject"){modalTitle.innerText="New Project Name";modalInput.type="text";modalInput.placeholder="e.g., Amigurumi Bear";if(patternPicker&&patternSelect){populatePatternSelect();patternSelect.value=""}}else if(type==="addPart"){modalTitle.innerText="New Part Name";modalInput.type="text";modalInput.placeholder="e.g., Head"}else if(type==="setMax"){const part=projects.find((p=>p.id===pId)).parts.find((pt=>pt.id===partId));modalTitle.innerText="Set Max Stitches";modalInput.value=part.max??"";modalInput.type="number";modalInput.placeholder="Leave blank to clear"}else if(type==="renamePart"){const part=projects.find((p=>p.id===pId)).parts.find((pt=>pt.id===partId));if(part.locked||part.finished)return;modalTitle.innerText="Rename Part";modalInput.value=part.name;modalInput.type="text"}else if(type==="savePattern"){modalTitle.innerText="Save as Pattern";const project=projects.find((p=>p.id===pId));modalInput.value=project?`${project.name} pattern`:"";modalInput.type="text";modalInput.placeholder="Pattern name"}else if(type==="manualCount"){const part=projects.find((p=>p.id===pId)).parts.find((pt=>pt.id===partId));if(part.locked||part.finished)return;modalTitle.innerText="Set Row Count";modalInput.value=part.count;modalInput.type="number"}else if(type==="setMax"){const part=projects.find((p=>p.id===pId)).parts.find((pt=>pt.id===partId));if(part.locked)return}if(type!=="addProject"&&patternPicker){patternPicker.style.display="none"}modal.classList.add("active");setTimeout((()=>modalInput.focus()),100)}function closeModal(){modal.classList.remove("active");modalInput.blur()}function saveModal(){const val=modalInput.value.trim();if(!val&&modalState.type!=="manualCount"&&modalState.type!=="setMax")return closeModal();if(modalState.type==="addProject"){const nextColor=colors[projects.length%colors.length];const newProject={id:Date.now(),name:val,color:nextColor,collapsed:false,note:"",parts:[]};const selectedPatternId=patternSelect?patternSelect.value:"";const chosenPattern=selectedPatternId?patterns.find((p=>String(p.id)===selectedPatternId)):null;if(chosenPattern&&Array.isArray(chosenPattern.parts)&&chosenPattern.parts.length){chosenPattern.parts.forEach(((pt,idx)=>{newProject.parts.push({id:Date.now()+idx+1,name:pt.name||`Part ${idx+1}`,count:0,locked:false,finished:false,minimized:false,max:pt.max??null,color:pt.color||newProject.color,note:""})}))}else{newProject.parts.push({id:Date.now()+1,name:"Part 1",count:0,locked:false,finished:false,minimized:false,max:null,color:nextColor,note:""})}projects.push(newProject)}else if(modalState.type==="addPart"){const project=projects.find((p=>p.id===modalState.pId));project.parts.push({id:Date.now(),name:val,count:0,locked:false,finished:false,minimized:false,max:null,color:project.color,note:""});project.collapsed=false}else if(modalState.type==="renamePart"){const part=projects.find((p=>p.id===modalState.pId)).parts.find((pt=>pt.id===modalState.partId));part.name=val}else if(modalState.type==="renameProject"){const project=projects.find((p=>p.id===modalState.pId));project.name=val}else if(modalState.type==="manualCount"){const num=parseInt(val);if(!isNaN(num)&&num>=0){const part=projects.find((p=>p.id===modalState.pId)).parts.find((pt=>pt.id===modalState.partId));part.count=num;if(part.max!==null&&part.count>part.max)part.count=part.max}}else if(modalState.type==="setMax"){const part=projects.find((p=>p.id===modalState.pId)).parts.find((pt=>pt.id===modalState.partId));if(val===""){part.max=null}else{const num=parseInt(val);if(!isNaN(num)&&num>0){part.max=num;if(part.count>part.max)part.count=part.max}}}else if(modalState.type==="savePattern"){const project=projects.find((p=>p.id===modalState.pId));if(project){const template=project.parts.map((pt=>({name:pt.name,color:pt.color,max:pt.max})));patterns.push({id:Date.now(),name:val||project.name,color:project.color,parts:template});savePatterns();populatePatternSelect()}}save();closeModal()}modalInput.addEventListener("keyup",(e=>{if(e.key==="Enter")saveModal()}));function toggleNote(id){const el=document.getElementById(id);if(!el)return;el.classList.toggle("show")}function updateProjectNote(e,pId){const project=projects.find((p=>p.id===pId));project.note=e.target.value;localStorage.setItem("crochetCounters",JSON.stringify(projects))}function updatePartNote(e,pId,partId){const project=projects.find((p=>p.id===pId));const part=project.parts.find((pt=>pt.id===partId));part.note=e.target.value;localStorage.setItem("crochetCounters",JSON.stringify(projects))}function render(){app.innerHTML="";if(projects.length===0){app.innerHTML='Toadstools & twine await...
Tap + to begin a new project.
';return}const grid=document.createElement("div");grid.className="projects-grid";projects.forEach((project=>{const sortedParts=[...project.parts].sort(((a,b)=>a.finished-b.finished));const projectCollapsedClass=project.collapsed?"project-collapsed":"";let partsHtml="";sortedParts.forEach((part=>{const accent=part.color||project.color;const isLocked=part.locked?"is-locked":"";const isFinished=part.finished?"is-finished":"";const isMinimized=part.minimized?"is-minimized":"";const lockIcon=part.locked?'':'';const lockBtnClass=part.locked?"btn-lock locked-active":"btn-lock";const controlsDimmed=part.locked||part.finished?"dimmed":"";const hideControls=part.finished||part.minimized?"hidden-controls":"";const showSetMax=part.minimized?"hidden":"";const partNoteId=`part-note-${project.id}-${part.id}`;const countId=`count-${part.id}`;const pulseClass=lastCountPulse&&lastCountPulse.partId===part.id?lastCountPulse.dir==="up"?"count-bump-up":"count-bump-down":"";const finishPulseClass=part.finished&&lastFinishedId===part.id?"finish-shimmer":"";const partCardId=`part-${part.id}`;const partCardFullClass=`${isLocked} ${isFinished} ${isMinimized} ${finishPulseClass}`;const lockDisabled=part.locked?"disabled":"";const actionsHtml=part.minimized?``:`\n \n \n \n \n
`;const countSubtext=part.minimized?"":`\n \n ${part.max!==null?`${part.count} / ${part.max}`:"No max set"}\n \n
\n `;partsHtml+=`\n \n \n
${part.count}
\n ${countSubtext}\n
\n \n \n \n
\n
\n \n
\n
\n
`}));const projectContainer=document.createElement("div");projectContainer.className=`project-container ${projectCollapsedClass}`;projectContainer.style=`--project-color: ${project.color}`;const projectNoteId=`project-note-${project.id}`;projectContainer.innerHTML=`\n \n \n \n
\n \n ${partsHtml}
\n `;grid.appendChild(projectContainer)}));lastCountPulse=null;lastFinishedId=null;app.appendChild(grid)}render();
\ No newline at end of file
diff --git a/assets/style.css b/assets/style.css
index bc0d4fa..966a10c 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -297,6 +297,11 @@ h1 {
cursor: pointer; box-shadow: 0 3px 8px rgba(0,0,0,0.1); transition: transform 0.1s;
}
.btn-add-part:active { transform: scale(0.95); }
+.btn-save-pattern {
+ background: none; border: 1px dashed var(--border); color: var(--text-muted);
+ border-radius: 14px; padding: 6px 10px; font-size: 0.9rem; cursor: pointer;
+}
+.btn-save-pattern:hover { color: var(--project-color); border-color: var(--project-color); }
.btn-delete-project {
background: none; border: none; color: var(--text-muted); font-size: 1.2rem; cursor: pointer; padding: 5px;
@@ -520,6 +525,22 @@ button:active { transform: scale(0.97); box-shadow: none; }
.modal-btn { padding: 12px 24px; border: none; border-radius: 10px; font-size: 1rem; font-weight: 600; cursor: pointer; }
.btn-cancel { background: var(--lock-btn-bg); color: var(--text-muted); }
.btn-save { background: var(--text); color: var(--bg); }
+.pattern-picker { display: none; margin: 12px 0 6px; }
+.pattern-picker label {
+ display: block;
+ font-size: 0.9rem;
+ color: var(--text-muted);
+ margin-bottom: 4px;
+}
+.pattern-picker select {
+ width: 100%;
+ padding: 10px 12px;
+ border-radius: 10px;
+ border: 1px solid var(--border);
+ background: var(--input-bg);
+ color: var(--text);
+ font-family: inherit;
+}
.empty-state { text-align: center; color: var(--text-muted); margin-top: 80px; font-size: 1.2rem; font-style: italic;}
diff --git a/assets/style.min.css b/assets/style.min.css
new file mode 100644
index 0000000..2947a09
--- /dev/null
+++ b/assets/style.min.css
@@ -0,0 +1 @@
+root--bg#e6e0d0--card-bg#f9f4e6--header-bg#8a6b52--header-text#fdf8f0--text#433628--text-muted#7a6d5c--border#d6cabc--shadow0 4px 10px rgba(6754400.12)--seed-opacity0.06--modal-bg#f8f5e6--input-bg#fffdf5--input-border#d1c7b7--bg-finished#dbe4ce--lock-btn-bg#ebe7da--lock-btn-text#9c8e7e--btn-secondary-bg#fdfcf5--btn-secondary-text#4a3b2a--danger#b56b54--success#7a8b4f--project-color#b56b54body.dark-mode--bg#2c3327--card-bg#3b342c--header-bg#4b3829--header-text#e9e4d7--text#e9e4d7--text-muted#b5aa9b--border#5f5245--shadow0 4px 10px rgba(0000.45)--modal-bg#3d362d--input-bg#2c2720--input-border#594e3f--bg-finished#2a382a--lock-btn-bg#4a4238--lock-btn-text#8c8276--btn-secondary-bg#4a4238--btn-secondary-text#d1c7b7*box-sizingborder-box-webkit-tap-highlight-colortransparentbodyfont-family'Quicksand'"Segoe UI"RobotoHelveticaArialsans-serifbackground-colorvar(--bg)min-height100vhcolorvar(--text)margin0padding0displayflexflex-directioncolumnoverflow-xhiddenscrollbar-gutterstabletransitionbackground-color 0.3scolor 0.3sbody.theme-animatinganimationthemeSwap 0.7s ease-in-out@keyframes themeSwap0%filterbrightness(1.08) saturate(1.06)opacity0.8545%filterbrightness(1.03) saturate(1.12) contrast(1.04)opacity0.9275%filterbrightness(0.98) saturate(1.04)opacity0.97100%filterbrightness(1) saturate(1)opacity1headerbackground-colorvar(--header-bg)colorvar(--header-text)padding0.8rem 1remdisplayflexjustify-contentspace-betweenalign-itemscenterbox-shadow0 2px 10px rgba(0000.1)positionstickytop0z-index100border-bottom2px solid rgba(0000.05)transitionbackground-color 0.3sh1margin0font-size1.3remfont-weight800letter-spacing0.5pxcolorvar(--header-text)text-shadow1px 1px 2px rgba(0000.1)font-family'Cormorant Garamond'Georgiaserifcursorpointeruser-selectnone.branddisplayflexalign-itemscentergap10px.brand-iconwidth52pxheight52pxborder-radius8pxobject-fitcontaindisplayblockbackgroundrgba(2552552550.12)padding4pxbox-shadowinset 0 1px 2px rgba(0000.08).header-controlsdisplayflexgap10px.header-btnbackgroundrgba(2552552550.2)bordernoneborder-radius50%width40pxheight40pxdisplayflexalign-itemscenterjustify-contentcenterfont-size1.2remcursorpointercolorvar(--header-text)transitiontransform 0.18s easebox-shadow 0.18s easebackground-color 0.2stransformtranslateY(0)font-familyinherit.header-btnhovertransformtranslateY(-1px) scale(1.03)box-shadow0 6px 14px rgba(0000.12).header-btnactivetransformtranslateY(0) scale(0.96)box-shadownone.header-btn.is-activebackgroundvar(--header-text)colorvar(--header-bg).hiddendisplaynone !important.hidden-inputdisplaynone.color-overlaypositionfixedinset0backgroundrgba(4435250.55)displaynonealign-itemscenterjustify-contentcenterz-index210padding20pxbackdrop-filterblur(2px).color-overlay.activedisplayflex.color-modalbackgroundvar(--card-bg)colorvar(--text)border-radius16pxpadding18px 18px 14pxwidthmin(420px90vw)box-shadow0 10px 30px rgba(0000.25)border1px solid var(--border).color-titlemargin0 0 12pxfont-family'Cormorant Garamond'Georgiaserif.color-griddisplaygridgrid-template-columnsrepeat(auto-fitminmax(60px1fr))gap10pxmargin-bottom12px.color-customdisplayflexalign-itemscentergap10pxmargin-bottom12px.color-custom input[type="color"]width42pxheight32pxborder1px solid var(--border)border-radius8pxbackgroundvar(--card-bg)padding0.color-swatchheight44pxborder-radius12pxborder2px solid var(--border)cursorpointerbox-shadowinset 0 0 0 2px rgba(2552552550.55)transitiontransform 0.15s easebox-shadow 0.15s ease.color-swatchhovertransformtranslateY(-1px) scale(1.02)box-shadowinset 0 0 0 2px rgba(2552552550.8).color-swatchactivetransformscale(0.98).color-swatch.customdisplayflexalign-itemscenterjustify-contentcenterfont-weight700colorvar(--text)backgroundlinear-gradient(45deg#f4ead8#d6cabc)box-shadowinset 0 0 0 2px rgba(0000.08).save-overlaypositionfixedinset0backgroundrgba(4435250.55)displaynonealign-itemscenterjustify-contentcenterz-index210padding20pxbackdrop-filterblur(2px).save-overlay.activedisplayflex.save-modalbackgroundvar(--card-bg)colorvar(--text)border-radius16pxpadding18px 18px 14pxwidthmin(420px90vw)box-shadow0 10px 30px rgba(0000.25)border1px solid var(--border).save-subtextmargin0 0 8pxcolorvar(--text-muted).save-listdisplaygridgap8pxmax-height220pxoverflow-yautomargin-bottom10px.save-itemdisplayflexalign-itemscentergap10pxpadding8px 10pxborder-radius10pxbackgroundvar(--input-bg)border1px solid var(--border).save-item inputwidth18pxheight18px.save-actionsdisplayflexjustify-contentflex-endgap8px.icon-woodlandwidth22pxheight22px.containermax-width1200pxmargin0 autopadding1.5rem 1.25rem calc(120px + env(safe-area-inset-bottom0px))positionrelativez-index1flex1.projects-griddisplaygridgrid-template-columnsrepeat(auto-fitminmax(320px1fr))gap20pxalign-itemsstart@media (min-width1280px).projects-gridgrid-template-columnsrepeat(3minmax(320px1fr)).project-containerbackgroundvar(--card-bg)border-radius18pxpadding5px 15px 15px 15pxmargin-bottom2rembox-shadowvar(--shadow)transitionbackground-color 0.3sborder1px solid var(--border)z-index10.project-headerdisplayflexjustify-contentspace-betweenalign-itemscenterpadding15px 5pxmargin-bottom10pxgap10px.project-title-groupdisplayflexalign-itemscentergap10px.project-titlefont-size1.4remfont-weight800colorvar(--project-color)text-transformuppercaseletter-spacing1pxfont-family'Cormorant Garamond'Georgiaserif.btn-toggle-projectbackgroundnonebordernonecolorvar(--project-color)font-size1.2remcursorpointertransitiontransform 0.2spadding5px.part-listmax-height2000pxopacity1transitionmax-height 0.35s easeopacity 0.3s easeoverflowhidden.project-collapsed .btn-toggle-projecttransformrotate(-90deg).project-collapsed .part-listmax-height0opacity0pointer-eventsnone.project-collapsedmargin-bottom1remopacity0.8box-shadownone.project-actionsdisplayflexgap8pxalign-itemscenter.btn-add-partbackgroundvar(--project-color)colorvar(--card-bg)bordernoneborder-radius20pxpadding6px 16pxfont-size0.9remfont-weightboldcursorpointerbox-shadow0 3px 8px rgba(0000.1)transitiontransform 0.1s.btn-add-partactivetransformscale(0.95).btn-save-patternbackgroundnoneborder1px dashed var(--border)colorvar(--text-muted)border-radius14pxpadding6px 10pxfont-size0.9remcursorpointer.btn-save-patternhovercolorvar(--project-color)border-colorvar(--project-color).btn-delete-projectbackgroundnonebordernonecolorvar(--text-muted)font-size1.2remcursorpointerpadding5px.btn-delete-projecthovercolorvar(--danger).btn-rename-projectbackgroundnonebordernonecolorvar(--text-muted)padding5px 8pxborder-radius8pxfont-size1remcursorpointer.btn-rename-projecthovercolorvar(--project-color).icon-pencildisplayinline-blocktransformscaleX(-1).btn-colorwidth28pxheight28pxborder-radius50%border2px solid var(--border)backgroundvar(--project-color)cursorpointerpadding0displayinline-flexalign-itemscenterjustify-contentcenterbox-shadowinset 0 0 0 2px var(--card-bg).btn-colorhoverbox-shadowinset 0 0 0 2px var(--project-color).part-cardbackgroundvar(--card-bg)border-radius14pxpadding1remmargin-bottom0.8rembox-shadowvar(--shadow)positionrelativetransitionbackground-color 0.3soverflowhiddenborder-left7px solid var(--project-color)border-top1px solid var(--border)border-right1px solid var(--border)border-bottom1px solid var(--border).part-card.is-lockedbackground-colorvar(--bg)opacity0.9.part-card.is-finishedbackground-colorvar(--bg-finished)border-left-colorvar(--success)opacity0.9.finish-shimmeranimationfinishShimmer 0.8s ease@keyframes finishShimmer0%box-shadowvar(--shadow)0 0 0 0 rgba(122139790.32)60%box-shadowvar(--shadow)0 0 0 16px rgba(122139790)100%box-shadowvar(--shadow)0 0 0 0 rgba(122139790).part-card.is-minimizedpadding0.8rem 1rem.part-card.is-minimized .count-display.part-card.is-minimized .controlsdisplaynone.part-card.is-minimized .part-mini-countdisplayinline-block.part-card.is-minimized .btn-toggle-parttransformrotate(-90deg).part-card.is-minimized .count-subtext.part-card.is-minimized .btn-reset-part.part-card.is-minimized .btn-delete-partdisplaynone.part-headerdisplayflexjustify-contentspace-betweenalign-itemscenter.part-cardnot(.is-minimized) .part-headermargin-bottom15px.part-name-groupdisplayflexalign-itemscentergap12pxflex-grow1.check-containerpositionrelativecursorpointerwidth26pxheight26pxflex-shrink0.check-container inputopacity0cursorpointerheight0width0.checkmarkpositionabsolutetop0left0height26pxwidth26pxbackground-colorvar(--card-bg)border-radius50%border2px solid var(--text-muted)transitionall 0.2s.check-container inputchecked ~ .checkmarkbackground-colorvar(--success)border-colorvar(--success).checkmarkaftercontent""positionabsolutedisplaynone.check-container inputchecked ~ .checkmarkafterdisplayblock.check-container .checkmarkafterleft9pxtop5pxwidth6pxheight12pxbordersolid var(--card-bg)border-width0 2px 2px 0transformrotate(45deg).part-namefont-size1.1remfont-weight700colorvar(--text)border-bottom2px dashed var(--project-color)cursorpointerwhite-spacenowrapoverflowhiddentext-overflowellipsismax-width160px.part-mini-countdisplaynonefont-weight800colorvar(--project-color)margin-left8pxfont-size1.2rem.is-finished .part-nametext-decorationline-throughcolorvar(--success)border-bottomnone.is-finished .part-mini-countcolorvar(--success).part-actionsdisplayflexgap8pxalign-itemscenter.part-actionsjustify-contentflex-endmin-width120px.icon-btnbackgroundnonebordernonefont-size1.3rempadding5pxcolorvar(--text-muted)cursorpointertransitioncolor 0.2sdisplayinline-flexalign-itemscenterjustify-contentcenterwidth32pxheight32px.icon-btndisabledopacity0.4pointer-eventsnone.btn-delete-parthovercolorvar(--danger).btn-toggle-parttransitiontransform 0.2s.count-displayfont-size3.5remfont-weight800text-aligncentercolorvar(--project-color)margin0.5rem 0 0.4rem 0touch-actionmanipulationtext-shadow1px 1px 0px var(--card-bg).is-locked .count-displaycolorvar(--lock-btn-text)pointer-eventsnone.is-finished .count-displaycolorvar(--success)pointer-eventsnone.count-subtexttext-aligncenterfont-size0.9remcolorvar(--text-muted)margin0 0 0.6rem 0.count-subtext strongcolorvar(--project-color).count-bump-upanimationcountUp 0.26s ease.count-bump-downanimationcountDown 0.26s ease@keyframes countUp0%transformscale(0.95)colorvar(--btn-secondary-text)60%transformscale(1.08)colorvar(--project-color)100%transformscale(1)colorvar(--project-color)@keyframes countDown0%transformscale(1.05)colorvar(--project-color)60%transformscale(0.9)colorvar(--text-muted)100%transformscale(1)colorvar(--project-color).note-togglebackgroundnoneborder1px dashed var(--border)colorvar(--text-muted)padding4px 10pxborder-radius10pxcursorpointerfont-size0.9remmargin12px auto 6pxdisplayblock.note-togglehovercolorvar(--project-color)border-colorvar(--project-color).note-areadisplaynonemargin-top10px.note-area.showdisplayblock.note-area textareawidth100%min-height90pxborder-radius10pxborder1px solid var(--border)backgroundvar(--input-bg)colorvar(--text)padding10pxfont-size0.95remresizevertical.controlsdisplaygridgrid-template-columns1fr 1fr 1frgap10pxpadding0 5pxbutton.action-btnbordernoneborder-radius12pxpadding12px 0font-size1.5remcursorpointerdisplayflexalign-itemscenterjustify-contentcentertransitionall 0.1sbox-shadow0 3px 6px rgba(0000.05).btn-minusbackground-colorvar(--btn-secondary-bg)colorvar(--btn-secondary-text)border1px solid var(--border).btn-plusbackground-colorvar(--project-color)colorvar(--card-bg).btn-lockbackground-colorvar(--lock-btn-bg)colorvar(--lock-btn-text)font-size1.1rem.btn-lock.locked-activebackground-colorvar(--border)colorvar(--text-muted)border2px solid var(--text-muted)box-shadownone.hidden-controlsvisibilityhiddenpointer-eventsnone.dimmedopacity0.4pointer-eventsnonebuttonactivetransformscale(0.97)box-shadownone.fabpositionfixedbottom30pxright30pxbackground-colorvar(--text)colorvar(--bg)width65pxheight65pxborder-radius50%displayflexalign-itemscenterjustify-contentcenterfont-size2.2rembox-shadow0 6px 15px rgba(7459420.3)bordernonez-index3transitiontransform 0.2s easebox-shadow 0.2s ease.fabhovertransformtranslateY(-3px) scale(1.04) rotate(-2deg)box-shadow0 10px 24px rgba(0000.18).fabactivetransformtranslateY(0) scale(0.94)box-shadow0 3px 8px rgba(0000.2).fabz-index2.modal-overlaypositionfixedtop0left0width100%height100%background-colorrgba(4435250.6)z-index200align-itemscenterjustify-contentcenterbackdrop-filterblur(2px)displayflexopacity0visibilityhiddenpointer-eventsnonetransitionopacity 0.3s easevisibility 0.3s ease.modal-overlay.activeopacity1visibilityvisiblepointer-eventsauto.modal-contentbackgroundvar(--modal-bg)colorvar(--text)padding25pxborder-radius20pxwidth85%max-width400pxanimationnonebox-shadowvar(--shadow)border1px solid var(--border)transform-origincenter.modal-overlay.active .modal-contentanimationmodalIn 0.32s ease@keyframes modalIn0%transformtranslateY(10px) scale(0.96)opacity060%transformtranslateY(-4px) scale(1.02)opacity1100%transformtranslateY(0) scale(1)opacity1.modal-titlemargin0 0 15px 0font-size1.3remcolorvar(--text)font-weight800.modal-inputwidth100%padding15pxfont-size1.2rembackgroundvar(--input-bg)colorvar(--text)border2px solid var(--input-border)border-radius12pxmargin-bottom25pxoutlinenonetransitionborder-color 0.2s.modal-inputfocusborder-colorvar(--project-color).modal-actionsdisplayflexjustify-contentflex-endgap10px.modal-btnpadding12px 24pxbordernoneborder-radius10pxfont-size1remfont-weight600cursorpointer.btn-cancelbackgroundvar(--lock-btn-bg)colorvar(--text-muted).btn-savebackgroundvar(--text)colorvar(--bg).pattern-pickerdisplaynonemargin12px 0 6px.pattern-picker labeldisplayblockfont-size0.9remcolorvar(--text-muted)margin-bottom4px.pattern-picker selectwidth100%padding10px 12pxborder-radius10pxborder1px solid var(--border)backgroundvar(--input-bg)colorvar(--text)font-familyinherit.empty-statetext-aligncentercolorvar(--text-muted)margin-top80pxfont-size1.2remfont-styleitalic.footer-bgpositionfixedleft0right0bottom-180pxheightclamp(760px1200px1200px)backgroundurl('textures/mushroom.svg') no-repeat center bottombackground-sizecoveropacity1width125vwpointer-eventsnonez-index0.firefly-wrappositionfixedwidth28pxheight28pxanimationfireflyGlide var(--fly-duration14s) linear forwardsz-index0pointer-eventsnone.fireflywidth100%height100%border-radius50%backgroundradial-gradient(circlergba(2552452000.9) 0%rgba(2552452000.4) 45%rgba(2552452000.1) 70%rgba(2552452000) 85%)opacity0.9mix-blend-modescreenfilterblur(1px) drop-shadow(0 0 14px rgba(2552452000.8)) brightness(1.15)animationfireflyFlutter 1.6s ease-in-out infinite alternatefireflyFlicker 1.1s ease-in-out infinite alternate.seed-wrappositionfixedwidth36pxheight36pxanimationseedGlide var(--seed-duration16s) linear forwardsz-index0pointer-eventsnone.seedwidth200pxheight200pxbackgroundurl('textures/seed.svg') no-repeat center/containopacityvar(--seed-opacity0.07)mix-blend-modescreenfilterdrop-shadow(0 4px 10px rgba(2001901600.55))animationseedDrift 2.6s ease-in-out infinite alternateseedFlicker 1.4s ease-in-out infinite alternateseedFlip var(--seed-flip-duration6s) ease-in-out infinite@keyframes fireflyGlide0%transformtranslate3d(var(--fly-start-x-10vw)var(--fly-start-y0)0) scale(var(--fly-scale1))opacity012%opacity0.5535%transformtranslate3d(var(--fly-mid-x25vw)var(--fly-mid-y-6px)0) scale(var(--fly-scale1))opacity0.6565%transformtranslate3d(var(--fly-mid2-x65vw)var(--fly-mid2-y6px)0) scale(var(--fly-scale1))opacity0.5590%opacity0.35100%transformtranslate3d(var(--fly-end-x110vw)var(--fly-end-y0)0) scale(var(--fly-scale1))opacity0@keyframes fireflyFlutter0%transformtranslateY(-2px) translateX(-3px) scale(0.95)50%transformtranslateY(3px) translateX(2px) scale(1.05)100%transformtranslateY(-2px) translateX(1px) scale(0.97)@keyframes fireflyFlicker0%opacity0.65filterblur(1px) drop-shadow(0 0 10px rgba(2552452000.65))50%opacity1filterblur(0.5px) drop-shadow(0 0 16px rgba(2552452001))100%opacity0.75filterblur(1px) drop-shadow(0 0 12px rgba(2552452000.75))@keyframes seedGlide0%transformtranslate3d(var(--seed-start-12vw)00) scale(var(--seed-scale1))opacity010%opacity0.3540%transformtranslate3d(calc(var(--seed-mid30vw))-10px0) scale(var(--seed-scale1))opacity0.570%transformtranslate3d(calc(var(--seed-mid30vw) * 2)8px0) scale(var(--seed-scale1))opacity0.4100%transformtranslate3d(var(--seed-end110vw)-6px0) scale(var(--seed-scale1))opacity0@keyframes seedDrift0%transformtranslateY(-4px) translateX(calc(var(--seed-sway6px) * -1)) rotate(calc(var(--seed-tilt10deg) * -0.6)) scale(0.94)opacity0.6550%transformtranslateY(5px) translateX(calc(var(--seed-sway6px) * 0.8)) rotate(calc(var(--seed-tilt10deg))) scale(1.05)opacity0.9100%transformtranslateY(-3px) translateX(calc(var(--seed-sway6px) * 0.4)) rotate(calc(var(--seed-tilt10deg) * 0.2)) scale(0.97)opacity0.7@keyframes seedFlicker0%opacity0.65filterdrop-shadow(0 2px 4px rgba(2001901600.35))50%opacity0.9filterdrop-shadow(0 4px 8px rgba(2001901600.5))100%opacity0.7filterdrop-shadow(0 3px 6px rgba(2001901600.4))@keyframes seedFlip0%transformrotate(0deg) scale(1)20%transformrotate(8deg) scale(1.02)40%transformrotate(-10deg) scale(0.98)60%transformrotate(14deg) scale(1.03)80%transformrotate(-6deg) scale(0.99)100%transformrotate(0deg) scale(1)@media (max-width768px).footer-bgbottom-40pxheightclamp(260px24vh450px)@media (max-width1024px) and (min-width769px).containerpadding1.5rem 1.25rem calc(140px + env(safe-area-inset-bottom0px)).footer-bgheightclamp(300px28vh480px)bottom-180px.swal-overlaypositionfixedinset0backgroundrgba(4435250.55)displayflexalign-itemscenterjustify-contentcenterz-index300padding20pxanimationoverlayFade 0.22s ease.swal-dialogbackgroundvar(--card-bg)colorvar(--text)border-radius16pxpadding22px 20px 18pxwidthmin(420px90vw)box-shadow0 10px 30px rgba(0000.25)border1px solid var(--border)text-aligncenterpositionrelativeanimationdialogPop 0.28s ease.swal-titlefont-family'Cormorant Garamond'Georgiaseriffont-size1.4remmargin0 0 8px.swal-textmargin0 0 16pxcolorvar(--text-muted).swal-actionsdisplayflexjustify-contentcentergap10px.swal-btnbordernoneborder-radius12pxpadding12px 18pxfont-weight700cursorpointermin-width110pxtransitiontransform 0.08s.swal-btnactivetransformscale(0.97).swal-confirmbackgroundvar(--project-color)colorvar(--card-bg).swal-cancelbackgroundvar(--lock-btn-bg)colorvar(--text).swal-dangerbackgroundvar(--danger)colorvar(--card-bg)@keyframes overlayFadefromopacity0toopacity1@keyframes dialogPop0%transformtranslateY(8px) scale(0.96)opacity060%transformtranslateY(-3px) scale(1.02)opacity1100%transformtranslateY(0) scale(1)opacity1
\ No newline at end of file
diff --git a/index.html b/index.html
index 233619c..675f056 100644
--- a/index.html
+++ b/index.html
@@ -40,6 +40,12 @@
Title
+
+
+
+