diff --git a/assets/app.js b/assets/app.js
index d944139..10db008 100644
--- a/assets/app.js
+++ b/assets/app.js
@@ -2,6 +2,282 @@
let projects = JSON.parse(localStorage.getItem('crochetCounters')) || [];
let patterns = JSON.parse(localStorage.getItem('crochetPatterns')) || [];
if (!Array.isArray(patterns)) patterns = [];
+let saveFlashTimer = null;
+const crochetAbbrev = [
+ { code: 'alt', desc: 'alternate' },
+ { code: 'approx', desc: 'approximately' },
+ { code: 'beg', desc: 'begin/beginning' },
+ { code: 'bet', desc: 'between' },
+ { code: 'bl/blo', desc: 'back loop/back loop only' },
+ { code: 'bo', desc: 'bobble' },
+ { code: 'bp', desc: 'back post' },
+ { code: 'bpdc', desc: 'back post double crochet' },
+ { code: 'bpdtr', desc: 'back post double treble crochet' },
+ { code: 'bphdc', desc: 'back post half double crochet' },
+ { code: 'bpsc', desc: 'back post single crochet' },
+ { code: 'bptr', desc: 'back post treble crochet' },
+ { code: 'cc', desc: 'contrasting color' },
+ { code: 'ch', desc: 'chain stitch' },
+ { code: 'ch-sp', desc: 'chain space' },
+ { code: 'cl', desc: 'cluster' },
+ { code: 'cont', desc: 'continue' },
+ { code: 'dc', desc: 'double crochet' },
+ { code: 'dc2tog', desc: 'double crochet 2 stitches together' },
+ { code: 'dec', desc: 'decrease' },
+ { code: 'dtr', desc: 'double treble crochet' },
+ { code: 'edc', desc: 'extended double crochet' },
+ { code: 'ehdc', desc: 'extended half double crochet' },
+ { code: 'esc', desc: 'extended single crochet' },
+ { code: 'etr', desc: 'extended treble crochet' },
+ { code: 'fl/flo', desc: 'front loop/front loop only' },
+ { code: 'foll', desc: 'following' },
+ { code: 'fp', desc: 'front post' },
+ { code: 'fpdc', desc: 'front post double crochet' },
+ { code: 'fpdtr', desc: 'front post double treble crochet' },
+ { code: 'fphdc', desc: 'front post half double crochet' },
+ { code: 'fpsc', desc: 'front post single crochet' },
+ { code: 'fptr', desc: 'front post treble crochet' },
+ { code: 'hdc', desc: 'half double crochet' },
+ { code: 'hdc2tog', desc: 'half double crochet 2 stitches together' },
+ { code: 'inc', desc: 'increase' },
+ { code: 'lp', desc: 'loop' },
+ { code: 'm', desc: 'marker' },
+ { code: 'mc', desc: 'main color' },
+ { code: 'pat', desc: 'pattern' },
+ { code: 'pc', desc: 'popcorn stitch' },
+ { code: 'pm', desc: 'place marker' },
+ { code: 'prev', desc: 'previous' },
+ { code: 'ps/puff', desc: 'puff stitch' },
+ { code: 'rem', desc: 'remaining' },
+ { code: 'rep', desc: 'repeat' },
+ { code: 'rnd', desc: 'round' },
+ { code: 'rs', desc: 'right side' },
+ { code: 'sc', desc: 'single crochet' },
+ { code: 'sc2tog', desc: 'single crochet 2 stitches together' },
+ { code: 'sh', desc: 'shell' },
+ { code: 'sk', desc: 'skip' },
+ { code: 'sl st', desc: 'slip stitch' },
+ { code: 'sm/sl m', desc: 'slip marker' },
+ { code: 'sp', desc: 'space' },
+ { code: 'st', desc: 'stitch' },
+ { code: 'tbl', desc: 'through back loop' },
+ { code: 'tch/t-ch', desc: 'turning chain' },
+ { code: 'tog', desc: 'together' },
+ { code: 'tr', desc: 'treble crochet' },
+ { code: 'tr2tog', desc: 'treble crochet 2 stitches together' },
+ { code: 'trtr', desc: 'triple treble crochet' },
+ { code: 'ws', desc: 'wrong side' },
+ { code: 'yo', desc: 'yarn over' },
+ { code: 'yoh', desc: 'yarn over hook' },
+ // Tunisian
+ { code: 'etss', desc: 'extended Tunisian simple stitch' },
+ { code: 'fwp', desc: 'forward pass' },
+ { code: 'retp', desc: 'return pass' },
+ { code: 'tdc', desc: 'Tunisian double crochet' },
+ { code: 'tfs', desc: 'Tunisian full stitch' },
+ { code: 'thdc', desc: 'Tunisian half double crochet' },
+ { code: 'tks', desc: 'Tunisian knit stitch' },
+ { code: 'tps', desc: 'Tunisian purl stitch' },
+ { code: 'trs', desc: 'Tunisian reverse stitch' },
+ { code: 'tsc', desc: 'Tunisian single crochet' },
+ { code: 'tss', desc: 'Tunisian simple stitch' },
+ { code: 'tslst', desc: 'Tunisian slip stitch' },
+ { code: 'ttr', desc: 'Tunisian treble crochet' },
+ { code: 'ttw', desc: 'Tunisian twisted' }
+];
+
+const knitAbbrev = [
+ { code: 'alt', desc: 'alternate' },
+ { code: 'approx', desc: 'approximately' },
+ { code: 'beg', desc: 'beginning' },
+ { code: 'bet', desc: 'between' },
+ { code: 'BO', desc: 'bind off' },
+ { code: 'byo', desc: 'backward yarn over' },
+ { code: 'CC', desc: 'contrasting color' },
+ { code: 'cn', desc: 'cable needle' },
+ { code: 'CO', desc: 'cast on' },
+ { code: 'cont', desc: 'continue' },
+ { code: 'dec', desc: 'decrease' },
+ { code: 'dpn', desc: 'double-pointed needles' },
+ { code: 'foll', desc: 'follow' },
+ { code: 'inc', desc: 'increase' },
+ { code: 'k', desc: 'knit' },
+ { code: 'k1B', desc: 'knit stitch in row below' },
+ { code: 'kfb', desc: 'knit 1 front and back (inc)' },
+ { code: 'ksp', desc: 'knit 1, slip back, pass over (dec)' },
+ { code: 'k2tog', desc: 'knit 2 together (dec)' },
+ { code: 'kwise', desc: 'knitwise' },
+ { code: 'LH', desc: 'left hand' },
+ { code: 'lp', desc: 'loop' },
+ { code: 'm', desc: 'marker' },
+ { code: 'M1', desc: 'make one knitwise (inc)' },
+ { code: 'M1R', desc: 'make one right (inc)' },
+ { code: 'M1L', desc: 'make one left (inc)' },
+ { code: 'M1p', desc: 'make one purlwise (inc)' },
+ { code: 'M1rp', desc: 'make one right purlwise (inc)' },
+ { code: 'M1lp', desc: 'make one left purlwise (inc)' },
+ { code: 'MC', desc: 'main color' },
+ { code: 'p', desc: 'purl' },
+ { code: 'pat', desc: 'pattern' },
+ { code: 'pfb', desc: 'purl front and back (inc)' },
+ { code: 'pm', desc: 'place marker' },
+ { code: 'p2tog', desc: 'purl 2 together (dec)' },
+ { code: 'prev', desc: 'previous' },
+ { code: 'psso', desc: 'pass slipped stitch over' },
+ { code: 'p2sso', desc: 'pass 2 slipped stitches over' },
+ { code: 'pwise', desc: 'purlwise' },
+ { code: 'rem', desc: 'remaining' },
+ { code: 'rep', desc: 'repeat' },
+ { code: 'rev St st', desc: 'reverse stockinette stitch' },
+ { code: 'RH', desc: 'right hand' },
+ { code: 'rnd', desc: 'round' },
+ { code: 'RS', desc: 'right side' },
+ { code: 'SKP', desc: 'slip 1 k-wise, k1, pass slipped over (dec)' },
+ { code: 'SK2P', desc: 'slip 1 k-wise, k2tog, pass slipped over (dec)' },
+ { code: 'sl', desc: 'slip' },
+ { code: 'sl1k', desc: 'slip 1 knitwise' },
+ { code: 'sl1p', desc: 'slip 1 purlwise' },
+ { code: 'sl st', desc: 'slip stitch' },
+ { code: 'sm', desc: 'slip marker' },
+ { code: 'ssk', desc: 'slip 2 k-wise, k through back loops (dec)' },
+ { code: 'ssp', desc: 'slip 2 k-wise, return, p through back loops (dec)' },
+ { code: 'sssk', desc: 'slip 3 k-wise, k through back loops (double dec)' },
+ { code: 'sssp', desc: 'slip 3 k-wise, return, p through back loops (double dec)' },
+ { code: 'S2KP2', desc: 'slip 2 as if to k2tog, k1, pass 2 over (double dec)' },
+ { code: 'SSPP2', desc: 'slip 2 k-wise, return, p2tog tbl, p1, pass 2 over (double dec)' },
+ { code: 'st', desc: 'stitch' },
+ { code: 'St st', desc: 'stockinette stitch' },
+ { code: 'tbl', desc: 'through back loop' },
+ { code: 'tfl', desc: 'through front loop' },
+ { code: 'tog', desc: 'together' },
+ { code: 'WS', desc: 'wrong side' },
+ { code: 'w&t', desc: 'wrap and turn' },
+ { code: 'wyib', desc: 'with yarn in back' },
+ { code: 'wyif', desc: 'with yarn in front' },
+ { code: 'yb', desc: 'yarn back' },
+ { code: 'yfwd/yf', desc: 'yarn forward' },
+ { code: 'yo', desc: 'yarn over' },
+ { code: 'yon', desc: 'yarn over needle' },
+ { code: 'yrn', desc: 'yarn round needle' }
+];
+
+const crochetBuckets = [
+ { label: 'Core stitches', codes: ['ch', 'sc', 'hdc', 'dc', 'tr', 'sl st'] },
+ { label: 'Shaping & repeats', codes: ['inc', 'dec', 'tog', 'rep', 'rem', 'rnd', 'rs', 'ws', 'tch/t-ch'] },
+ { label: 'Placement & loops', codes: ['fl/flo', 'bl/blo', 'tbl', 'lp', 'sp', 'ch-sp', 'prev'] },
+ { label: 'Post stitches', codes: ['fp', 'fpdc', 'fptr', 'fpsc', 'fphdc', 'fpdtr', 'bp', 'bpdc', 'bptr', 'bpsc', 'bphdc', 'bpdtr'] },
+ { label: 'Extended & special', codes: ['pat', 'pc', 'ps/puff', 'cl', 'sh', 'dtr', 'edc', 'ehdc', 'esc', 'etr', 'tr2tog', 'trtr', 'yo', 'yoh'] },
+ { label: 'Markers & color', codes: ['mc', 'cc', 'm', 'pm', 'sm/sl m'] },
+ { label: 'Tunisian', codes: ['etss', 'fwp', 'retp', 'tdc', 'tfs', 'thdc', 'tks', 'tps', 'trs', 'tsc', 'tss', 'tslst', 'ttr', 'ttw'] },
+ { label: 'General terms', codes: ['alt', 'approx', 'beg', 'bet', 'cont', 'foll', 'pat', 'prev'] }
+];
+
+const knitBuckets = [
+ { label: 'Core stitches', codes: ['k', 'p', 'yo', 'sl st', 'St st', 'rev St st'] },
+ { label: 'Increases', codes: ['inc', 'M1', 'M1R', 'M1L', 'M1p', 'M1rp', 'M1lp', 'kfb', 'pfb'] },
+ { label: 'Decreases', codes: ['dec', 'k2tog', 'ssk', 'SKP', 'SK2P', 'ssp', 'sssk', 'sssp', 'S2KP2', 'SSPP2', 'p2tog', 'psso', 'p2sso', 'ksp', 'tog'] },
+ { label: 'Slips & markers', codes: ['sl', 'sl1k', 'sl1p', 'm', 'pm', 'sm', 'cn', 'dpn'] },
+ { label: 'Yarn moves', codes: ['wyib', 'wyif', 'yfwd/yf', 'yb', 'yon', 'yrn', 'w&t'] },
+ { label: 'Cast on / bind off', codes: ['CO', 'BO'] },
+ { label: 'Color & pattern', codes: ['MC', 'CC', 'pat'] },
+ { label: 'Positioning', codes: ['k1B', 'tbl', 'tfl', 'lp', 'prev'] },
+ { label: 'Rows & sides', codes: ['rep', 'rem', 'RS', 'WS', 'rnd', 'foll'] },
+ { label: 'General terms', codes: ['alt', 'approx', 'beg', 'bet', 'cont'] }
+];
+
+const repeatTokens = ['*', 'rep from *', '[', ']', '(', ')', 'to end'];
+const nonMergeTokens = new Set(['*', '[', ']', '(', ')', 'rep from *', 'to end']);
+
+function getActiveAbbrevLibrary() {
+ return patternDraft.mode === 'knit' ? knitAbbrev : crochetAbbrev;
+}
+
+function getAbbrevByCode(code) {
+ return crochetAbbrev.find(a => a.code === code) || knitAbbrev.find(a => a.code === code);
+}
+
+function buildAbbrevGroups(library, buckets) {
+ const index = new Map(library.map(item => [item.code, item]));
+ const used = new Set();
+ const groups = [];
+ buckets.forEach(bucket => {
+ const items = [];
+ bucket.codes.forEach(code => {
+ const item = index.get(code);
+ if (item && !used.has(code)) {
+ items.push(item);
+ used.add(code);
+ }
+ });
+ if (items.length) groups.push({ label: bucket.label, items });
+ });
+ const leftover = library.filter(item => !used.has(item.code));
+ if (leftover.length) groups.push({ label: 'Other', items: leftover });
+ return groups;
+}
+
+function getAbbrevGroups() {
+ const library = getActiveAbbrevLibrary();
+ const buckets = patternDraft.mode === 'knit' ? knitBuckets : crochetBuckets;
+ return buildAbbrevGroups(library, buckets);
+}
+
+function getCoreCodes() {
+ const buckets = patternDraft.mode === 'knit' ? knitBuckets : crochetBuckets;
+ const core = buckets.find(b => b.label === 'Core stitches');
+ return core ? core.codes : [];
+}
+
+function getPatternButtonCodes() {
+ const base = (patternDraft.abbrevSelection && patternDraft.abbrevSelection.length)
+ ? patternDraft.abbrevSelection
+ : (patternDraft.mode === 'knit'
+ ? ['k', 'p', 'yo', 'k2tog', 'ssk', 'sl st']
+ : ['ch', 'sc', 'hdc', 'dc', 'tr', 'inc', 'dec', 'sl st', 'sk', 'rep']);
+ const merged = [...base, ...repeatTokens];
+ return Array.from(new Set(merged));
+}
+function normalizePatternDraft(d = {}) {
+ const baseStep = { title: '', rows: [], rowDraft: '', note: '', image: '' };
+ const base = {
+ mode: 'crochet',
+ currentRow: 1,
+ line: '',
+ output: '',
+ meta: { title: '', designer: '' },
+ materials: '',
+ gauge: '',
+ gaugeSts: '',
+ gaugeRows: '',
+ gaugeHook: '',
+ size: '',
+ abbrev: '',
+ abbrevSelection: [],
+ stitches: '',
+ notes: '',
+ steps: []
+ };
+ const merged = { ...base, ...d };
+ merged.meta = { ...base.meta, ...(d.meta || {}) };
+ merged.abbrevSelection = Array.isArray(merged.abbrevSelection) ? merged.abbrevSelection : [];
+ merged.steps = Array.isArray(merged.steps) ? merged.steps.map(s => ({ ...baseStep, ...s })) : [];
+ merged.materials = merged.materials || '';
+ merged.gauge = merged.gauge || '';
+ merged.gaugeSts = merged.gaugeSts || '';
+ merged.gaugeRows = merged.gaugeRows || '';
+ merged.gaugeHook = merged.gaugeHook || '';
+ merged.size = merged.size || '';
+ merged.abbrev = merged.abbrev || '';
+ merged.stitches = merged.stitches || '';
+ merged.notes = merged.notes || '';
+ if (!merged.mode) merged.mode = 'crochet';
+ if (!merged.currentRow) merged.currentRow = 1;
+ if (merged.line === undefined) merged.line = '';
+ if (merged.output === undefined) merged.output = '';
+ return merged;
+}
+
+let patternDraft = normalizePatternDraft(JSON.parse(localStorage.getItem('crochetPatternDraft')) || {});
// New Earthy/Woodland Palette extracted from image vibes
const colors = [
'#a17d63', // Soft oak
@@ -45,7 +321,570 @@ const saveOverlay = document.getElementById('saveOverlay');
const saveList = document.getElementById('saveList');
const patternPicker = document.getElementById('patternPicker');
const patternSelect = document.getElementById('patternSelect');
+const patternOverlay = document.getElementById('patternOverlay');
+const patternRowNumber = null;
+const patternLine = null;
+const patternOutput = document.getElementById('patternOutput');
+const patternButtonsWrap = null;
if (patternPicker && patternSelect) populatePatternSelect();
+if (patternOverlay) {
+ patternOverlay.addEventListener('click', (e) => {
+ if (e.target === patternOverlay) closePatternComposer();
+ });
+}
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && patternOverlay && patternOverlay.classList.contains('active')) {
+ closePatternComposer();
+ }
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && patternOverlay && patternOverlay.classList.contains('active')) {
+ e.preventDefault();
+ addStep();
+ }
+});
+
+function persistPatternDraft() {
+ localStorage.setItem('crochetPatternDraft', JSON.stringify(patternDraft));
+ flashSave();
+}
+
+function setPatternMode(mode) {
+ patternDraft.mode = mode;
+ persistPatternDraft();
+ if (!document.querySelector('.pattern-overlay')) return;
+ document.querySelectorAll('.pattern-mode').forEach(btn => {
+ btn.classList.toggle('is-active', btn.dataset.mode === mode);
+ });
+ renderAbbrevChecklist();
+ renderSteps();
+}
+
+function loadDefaultAbbrev() {
+ patternDraft.abbrevSelection = getCoreCodes();
+ updateAbbrevFromSelection();
+ renderAbbrevChecklist();
+}
+
+function bindPatternInputs() {
+ const titleEl = document.getElementById('patternTitle');
+ const designerEl = document.getElementById('patternDesigner');
+ const materialsEl = document.getElementById('patternMaterials');
+ const gaugeEl = document.getElementById('patternGauge');
+ const gaugeStsEl = document.getElementById('patternGaugeSts');
+ const gaugeRowsEl = document.getElementById('patternGaugeRows');
+ const gaugeHookEl = document.getElementById('patternGaugeHook');
+ const sizeEl = document.getElementById('patternSize');
+ const abbrevEl = document.getElementById('patternAbbrev');
+ const stitchesEl = document.getElementById('patternStitches');
+ const notesEl = document.getElementById('patternNotes');
+ const outputEl = document.getElementById('patternOutput');
+ if (titleEl) titleEl.addEventListener('input', e => { updateMetaField('title', e.target.value); });
+ if (designerEl) designerEl.addEventListener('input', e => { updateMetaField('designer', e.target.value); });
+ if (materialsEl) materialsEl.addEventListener('input', e => { updatePatternField('materials', e.target.value); });
+ if (gaugeEl) gaugeEl.addEventListener('input', e => { updatePatternField('gauge', e.target.value); });
+ if (gaugeStsEl) gaugeStsEl.addEventListener('input', e => { updatePatternField('gaugeSts', e.target.value); });
+ if (gaugeRowsEl) gaugeRowsEl.addEventListener('input', e => { updatePatternField('gaugeRows', e.target.value); });
+ if (gaugeHookEl) gaugeHookEl.addEventListener('input', e => { updatePatternField('gaugeHook', e.target.value); });
+ if (sizeEl) sizeEl.addEventListener('input', e => { updatePatternField('size', e.target.value); });
+ if (abbrevEl) abbrevEl.addEventListener('input', e => { updatePatternField('abbrev', e.target.value); });
+ if (stitchesEl) stitchesEl.addEventListener('input', e => { updatePatternField('stitches', e.target.value); });
+ if (notesEl) notesEl.addEventListener('input', e => { updatePatternField('notes', e.target.value); });
+ if (outputEl) outputEl.addEventListener('input', e => {
+ patternDraft.output = e.target.value;
+ const lines = patternDraft.output.split('\n').filter(l => l.trim() !== '');
+ patternDraft.currentRow = Math.max(1, lines.length + 1);
+ persistPatternDraft();
+ });
+}
+
+function syncPatternUI() {
+ setPatternMode(patternDraft.mode);
+ const titleEl = document.getElementById('patternTitle');
+ const designerEl = document.getElementById('patternDesigner');
+ const materialsEl = document.getElementById('patternMaterials');
+ const gaugeEl = document.getElementById('patternGauge');
+ const gaugeStsEl = document.getElementById('patternGaugeSts');
+ const gaugeRowsEl = document.getElementById('patternGaugeRows');
+ const gaugeHookEl = document.getElementById('patternGaugeHook');
+ const sizeEl = document.getElementById('patternSize');
+ const abbrevEl = document.getElementById('patternAbbrev');
+ const stitchesEl = document.getElementById('patternStitches');
+ const notesEl = document.getElementById('patternNotes');
+ if (titleEl) titleEl.value = patternDraft.meta.title;
+ if (designerEl) designerEl.value = patternDraft.meta.designer;
+ if (materialsEl) materialsEl.value = patternDraft.materials;
+ if (gaugeEl) gaugeEl.value = patternDraft.gauge;
+ if (gaugeStsEl) gaugeStsEl.value = patternDraft.gaugeSts;
+ if (gaugeRowsEl) gaugeRowsEl.value = patternDraft.gaugeRows;
+ if (gaugeHookEl) gaugeHookEl.value = patternDraft.gaugeHook;
+ if (sizeEl) sizeEl.value = patternDraft.size;
+ if (abbrevEl) abbrevEl.value = patternDraft.abbrev;
+ if (stitchesEl) stitchesEl.value = patternDraft.stitches;
+ if (notesEl) notesEl.value = patternDraft.notes;
+ renderSteps();
+ renderAbbrevChecklist();
+}
+
+function addPatternToken(tok) {
+ if (!patternLine) return;
+ patternDraft.line = patternDraft.line ? `${patternDraft.line} ${tok}` : tok;
+ patternLine.value = patternDraft.line;
+ persistPatternDraft();
+}
+
+function clearPatternLine() {
+ if (!patternLine) return;
+ patternDraft.line = '';
+ patternLine.value = '';
+ persistPatternDraft();
+}
+
+function addPatternRow() {
+ if (!patternLine || !patternOutput) return;
+ const line = patternDraft.line.trim();
+ if (!line) return;
+ const rowLabel = patternDraft.mode === 'knit' ? 'Row' : 'Rnd';
+ const rowText = `${rowLabel} ${patternDraft.currentRow}: ${line}`;
+ patternDraft.output = patternDraft.output ? `${patternDraft.output}\n${rowText}` : rowText;
+ patternDraft.currentRow += 1;
+ patternDraft.line = '';
+ persistPatternDraft();
+ syncPatternUI();
+}
+
+function removePatternRow() {
+ if (!patternOutput) return;
+ const lines = (patternDraft.output || '').split('\n').filter(l => l.trim() !== '');
+ if (lines.length === 0) return;
+ lines.pop();
+ patternDraft.output = lines.join('\n');
+ patternDraft.currentRow = Math.max(1, lines.length + 1);
+ persistPatternDraft();
+ syncPatternUI();
+}
+
+function clearPatternOutput() {
+ if (!patternOutput) return;
+ patternDraft.output = '';
+ patternDraft.currentRow = 1;
+ patternDraft.line = '';
+ patternDraft.meta = { title: '', designer: '' };
+ patternDraft.materials = '';
+ patternDraft.gauge = '';
+ patternDraft.gaugeSts = '';
+ patternDraft.gaugeRows = '';
+ patternDraft.gaugeHook = '';
+ patternDraft.size = '';
+ patternDraft.abbrev = '';
+ patternDraft.stitches = '';
+ patternDraft.notes = '';
+ patternDraft.steps = [];
+ if (patternOutput) patternOutput.value = '';
+ persistPatternDraft();
+ syncPatternUI();
+}
+
+function exportPatternJSON() {
+ const payload = { patternDraft };
+ const name = patternDraft.meta.title || 'pattern';
+ const filename = `${name.replace(/\s+/g,'_').toLowerCase()}_draft.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 importPatternJSON() {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = 'application/json';
+ input.onchange = async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+ try {
+ const text = await file.text();
+ const data = JSON.parse(text);
+ if (!data.patternDraft) throw new Error('Invalid file');
+ patternDraft = normalizePatternDraft(data.patternDraft);
+ persistPatternDraft();
+ syncPatternUI();
+ } catch (err) {
+ showAlert({ title: 'Import failed', text: err.message });
+ }
+ };
+ input.click();
+}
+
+function exportPatternPDF() {
+ const w = window.open('', '_blank');
+ if (!w) return;
+ const styles = getComputedStyle(document.body);
+ const themeBg = styles.getPropertyValue('--bg') || '#f4f0e8';
+ const themeText = styles.getPropertyValue('--text') || '#2f2b28';
+ const accent = styles.getPropertyValue('--project-color') || '#7a8c6a';
+ const gaugeBlock = `${patternDraft.gaugeSts || ''}${patternDraft.gaugeRows ? ' • ' + patternDraft.gaugeRows : ''}${patternDraft.gaugeHook ? ' • ' + patternDraft.gaugeHook : ''}`;
+ const fontLink = '';
+ const html = `
+
+
+ ${patternDraft.meta.title || 'Pattern'} - PDF
+ ${fontLink}
+
+
+
+ ${patternDraft.meta.title || 'Pattern'}
+ ${patternDraft.meta.designer || ''}
+
+
Materials
+
${patternDraft.materials || ''}
+
+
+
Gauge / Size
+
${gaugeBlock}\n${patternDraft.gauge || ''}\n${patternDraft.size || ''}
+
+
+
Abbreviations
+
${patternDraft.abbrev || ''}
+
+
+
Stitch Guide
+
${patternDraft.stitches || ''}
+
+
+
Steps
+ ${patternDraft.steps.map((s, i) => {
+ const rows = (s.rows || []).map((r, idx) => `Row ${idx + 1}: ${r}`).join('
');
+ const note = s.note ? `
${s.note}` : '';
+ return `
Step ${i + 1}${s.title ? ': ' + s.title : ''}
${rows}${note}
`;
+ }).join('
')}
+
+
+
Rows
+
${patternDraft.output || ''}
+
+
+
Notes
+
${patternDraft.notes || ''}
+
+
+
+ `;
+ w.document.write(html);
+ w.document.close();
+ w.focus();
+ w.print();
+}
+
+function showPatternTab(tab) {
+ document.querySelectorAll('.pattern-tab').forEach(btn => btn.classList.toggle('is-active', btn.dataset.tab === tab));
+ document.querySelectorAll('.pattern-section').forEach(sec => sec.classList.toggle('is-active', sec.dataset.section === tab));
+}
+
+function updateMetaField(field, value) {
+ patternDraft.meta[field] = value;
+ persistPatternDraft();
+}
+
+function updatePatternField(field, value) {
+ patternDraft[field] = value;
+ persistPatternDraft();
+}
+
+function renderSteps() {
+ const container = document.getElementById('patternSteps');
+ if (!container) return;
+ container.innerHTML = '';
+ patternDraft.steps.forEach((step, idx) => {
+ const card = document.createElement('div');
+ card.className = 'pattern-step-card card-pop';
+ const rows = step.rows || [];
+ const rowList = rows.map((r, i) => `
+
+
+
+
+
+ `).join('');
+ card.innerHTML = `
+ Step ${idx + 1}
+
+
+
+ ${getPatternButtonCodes().map(tok => ``).join('')}
+
+
+
+
+
+
+
+
+ ${rowList}
+
+
+
+
+
+
+
+
+
+ `;
+ card.querySelectorAll('input, textarea').forEach(el => {
+ el.addEventListener('input', (e) => {
+ const i = Number(e.target.dataset.idx);
+ const field = e.target.dataset.field;
+ if (!patternDraft.steps[i]) return;
+ patternDraft.steps[i][field] = e.target.value;
+ persistPatternDraft();
+ });
+ });
+ card.querySelectorAll('.pattern-buttons button').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const tok = btn.dataset.tok;
+ addPatternTokenToStep(idx, tok);
+ });
+ });
+ card.querySelectorAll('input[data-row]').forEach(inp => {
+ inp.addEventListener('input', (e) => {
+ const rowIdx = Number(e.target.dataset.row);
+ updateStepRow(idx, rowIdx, e.target.value);
+ });
+ });
+ const rowTextarea = card.querySelector('textarea[data-field="rowDraft"]');
+ if (rowTextarea) {
+ rowTextarea.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
+ e.preventDefault();
+ addStepRow(idx);
+ } else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
+ e.preventDefault();
+ addStep();
+ }
+ });
+ }
+ requestAnimationFrame(() => {
+ card.classList.remove('card-pop');
+ });
+ container.appendChild(card);
+ });
+ const addRow = document.createElement('div');
+ addRow.className = 'pattern-step-card add-step-row';
+ addRow.innerHTML = `
+
+ `;
+ container.appendChild(addRow);
+}
+
+function addStep() {
+ patternDraft.steps.push({ title: '', rows: [], rowDraft: '', note: '', image: '' });
+ persistPatternDraft();
+ renderSteps();
+}
+
+function addStepRow(idx) {
+ const step = patternDraft.steps[idx];
+ if (!step) return;
+ const line = (step.rowDraft || '').trim();
+ if (!line) return;
+ step.rows.push(line);
+ step.rowDraft = '';
+ persistPatternDraft();
+ renderSteps();
+}
+
+function removeStepLastRow(idx) {
+ // no-op: last-row removal handled per-row delete
+}
+
+function removeStepRow(idx, rowIdx) {
+ const step = patternDraft.steps[idx];
+ if (!step || !step.rows || rowIdx < 0 || rowIdx >= step.rows.length) return;
+ step.rows.splice(rowIdx, 1);
+ persistPatternDraft();
+ renderSteps();
+}
+
+function clearStepRow(idx) {
+ const step = patternDraft.steps[idx];
+ if (!step) return;
+ step.rowDraft = '';
+ persistPatternDraft();
+ renderSteps();
+}
+
+function updateStepRow(idx, rowIdx, value) {
+ const step = patternDraft.steps[idx];
+ if (!step || !step.rows || rowIdx < 0 || rowIdx >= step.rows.length) return;
+ step.rows[rowIdx] = value;
+ persistPatternDraft();
+}
+
+function renderAbbrevChecklist() {
+ const listEl = document.getElementById('patternAbbrevList');
+ if (!listEl) return;
+ listEl.innerHTML = '';
+ const selected = new Set(patternDraft.abbrevSelection);
+ getAbbrevGroups().forEach(bucket => {
+ const wrap = document.createElement('details');
+ wrap.className = 'abbrev-group';
+ wrap.open = true;
+ const summary = document.createElement('summary');
+ summary.textContent = `${bucket.label} (${bucket.items.length})`;
+ wrap.appendChild(summary);
+ const grid = document.createElement('div');
+ grid.className = 'abbrev-grid';
+ bucket.items.forEach(item => {
+ const pill = document.createElement('div');
+ pill.className = 'abbrev-pill';
+ if (selected.has(item.code)) pill.classList.add('is-selected');
+ pill.dataset.code = item.code;
+ pill.innerHTML = `
+ ${item.code}
+ ${item.desc}
+ `;
+ pill.addEventListener('click', () => {
+ const code = pill.dataset.code;
+ if (pill.classList.contains('is-selected')) {
+ pill.classList.remove('is-selected');
+ patternDraft.abbrevSelection = patternDraft.abbrevSelection.filter(c => c !== code);
+ } else {
+ pill.classList.add('is-selected');
+ if (!patternDraft.abbrevSelection.includes(code)) patternDraft.abbrevSelection.push(code);
+ }
+ updateAbbrevFromSelection();
+ });
+ grid.appendChild(pill);
+ });
+ wrap.appendChild(grid);
+ listEl.appendChild(wrap);
+ });
+ renderSelectedAbbrev();
+}
+
+function updateAbbrevFromSelection() {
+ const lines = patternDraft.abbrevSelection.map(code => {
+ const found = getAbbrevByCode(code);
+ return found ? `${found.code} – ${found.desc}` : code;
+ });
+ patternDraft.abbrev = lines.join('\n');
+ persistPatternDraft();
+ const abbrevEl = document.getElementById('patternAbbrev');
+ if (abbrevEl) abbrevEl.value = patternDraft.abbrev;
+ renderSelectedAbbrev();
+ renderSteps();
+}
+
+function renderSelectedAbbrev() {
+ renderPatternButtons();
+}
+
+function renderPatternButtons() {
+ // Buttons are rendered per-step; no global buttons.
+}
+
+function deleteStep(idx) {
+ patternDraft.steps.splice(idx, 1);
+ persistPatternDraft();
+ renderSteps();
+}
+
+function moveStep(idx, dir) {
+ const target = idx + dir;
+ if (target < 0 || target >= patternDraft.steps.length) return;
+ const tmp = patternDraft.steps[idx];
+ patternDraft.steps[idx] = patternDraft.steps[target];
+ patternDraft.steps[target] = tmp;
+ persistPatternDraft();
+ renderSteps();
+}
+
+function addPatternTokenToStep(idx, tok) {
+ const step = patternDraft.steps[idx];
+ if (!step) return;
+ // Append with comma/space
+ if (!step.rowDraft) {
+ step.rowDraft = tok;
+ } else {
+ // If last token matches, increment count
+ const parts = step.rowDraft.split(',').map(s => s.trim()).filter(Boolean);
+ if (parts.length && !nonMergeTokens.has(tok)) {
+ const last = parts[parts.length - 1];
+ const escapedTok = tok.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const match = last.match(new RegExp(`^${escapedTok}(\\d+)$`, 'i'));
+ if (last.toLowerCase() === tok.toLowerCase()) {
+ parts[parts.length - 1] = `${tok}2`;
+ } else if (match) {
+ const n = parseInt(match[1], 10) + 1;
+ parts[parts.length - 1] = `${tok}${n}`;
+ } else {
+ parts.push(tok);
+ }
+ step.rowDraft = parts.join(', ');
+ } else {
+ parts.push(tok);
+ step.rowDraft = parts.join(', ');
+ }
+ }
+ persistPatternDraft();
+ renderSteps();
+}
+
+function addStepRow(idx) {
+ const step = patternDraft.steps[idx];
+ if (!step) return;
+ const line = (step.rowDraft || '').trim();
+ if (!line) return;
+ step.rows.push(line);
+ step.rowDraft = '';
+ persistPatternDraft();
+ renderSteps();
+}
+
+function removeStepRow(idx) {
+ const step = patternDraft.steps[idx];
+ if (!step || !step.rows || step.rows.length === 0) return;
+ step.rows.pop();
+ persistPatternDraft();
+ renderSteps();
+}
+
+function clearStepRow(idx) {
+ const step = patternDraft.steps[idx];
+ if (!step) return;
+ step.rowDraft = '';
+ persistPatternDraft();
+ renderSteps();
+}
+
+function openPatternComposer() {
+ if (!patternOverlay) return;
+ patternOverlay.classList.add('active');
+ syncPatternUI();
+ showPatternTab('steps');
+}
+
+function closePatternComposer() {
+ if (!patternOverlay) return;
+ patternOverlay.classList.remove('active');
+}
+syncPatternUI();
+bindPatternInputs();
+renderAbbrevChecklist();
let pendingSaveSelection = [];
let lastCountPulse = null;
let lastFinishedId = null;
@@ -910,3 +1749,13 @@ function render() {
}
render();
+function flashSave() {
+ const el = document.getElementById('patternSaveIndicator');
+ if (!el) return;
+ el.textContent = 'Saved';
+ el.style.opacity = '1';
+ if (saveFlashTimer) clearTimeout(saveFlashTimer);
+ saveFlashTimer = setTimeout(() => {
+ el.style.opacity = '0.6';
+ }, 1200);
+}
diff --git a/assets/app.min.js b/assets/app.min.js
index 2823c13..10db008 100644
--- a/assets/app.min.js
+++ b/assets/app.min.js
@@ -1 +1,1761 @@
-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
+// --- Data Init & Colors ---
+let projects = JSON.parse(localStorage.getItem('crochetCounters')) || [];
+let patterns = JSON.parse(localStorage.getItem('crochetPatterns')) || [];
+if (!Array.isArray(patterns)) patterns = [];
+let saveFlashTimer = null;
+const crochetAbbrev = [
+ { code: 'alt', desc: 'alternate' },
+ { code: 'approx', desc: 'approximately' },
+ { code: 'beg', desc: 'begin/beginning' },
+ { code: 'bet', desc: 'between' },
+ { code: 'bl/blo', desc: 'back loop/back loop only' },
+ { code: 'bo', desc: 'bobble' },
+ { code: 'bp', desc: 'back post' },
+ { code: 'bpdc', desc: 'back post double crochet' },
+ { code: 'bpdtr', desc: 'back post double treble crochet' },
+ { code: 'bphdc', desc: 'back post half double crochet' },
+ { code: 'bpsc', desc: 'back post single crochet' },
+ { code: 'bptr', desc: 'back post treble crochet' },
+ { code: 'cc', desc: 'contrasting color' },
+ { code: 'ch', desc: 'chain stitch' },
+ { code: 'ch-sp', desc: 'chain space' },
+ { code: 'cl', desc: 'cluster' },
+ { code: 'cont', desc: 'continue' },
+ { code: 'dc', desc: 'double crochet' },
+ { code: 'dc2tog', desc: 'double crochet 2 stitches together' },
+ { code: 'dec', desc: 'decrease' },
+ { code: 'dtr', desc: 'double treble crochet' },
+ { code: 'edc', desc: 'extended double crochet' },
+ { code: 'ehdc', desc: 'extended half double crochet' },
+ { code: 'esc', desc: 'extended single crochet' },
+ { code: 'etr', desc: 'extended treble crochet' },
+ { code: 'fl/flo', desc: 'front loop/front loop only' },
+ { code: 'foll', desc: 'following' },
+ { code: 'fp', desc: 'front post' },
+ { code: 'fpdc', desc: 'front post double crochet' },
+ { code: 'fpdtr', desc: 'front post double treble crochet' },
+ { code: 'fphdc', desc: 'front post half double crochet' },
+ { code: 'fpsc', desc: 'front post single crochet' },
+ { code: 'fptr', desc: 'front post treble crochet' },
+ { code: 'hdc', desc: 'half double crochet' },
+ { code: 'hdc2tog', desc: 'half double crochet 2 stitches together' },
+ { code: 'inc', desc: 'increase' },
+ { code: 'lp', desc: 'loop' },
+ { code: 'm', desc: 'marker' },
+ { code: 'mc', desc: 'main color' },
+ { code: 'pat', desc: 'pattern' },
+ { code: 'pc', desc: 'popcorn stitch' },
+ { code: 'pm', desc: 'place marker' },
+ { code: 'prev', desc: 'previous' },
+ { code: 'ps/puff', desc: 'puff stitch' },
+ { code: 'rem', desc: 'remaining' },
+ { code: 'rep', desc: 'repeat' },
+ { code: 'rnd', desc: 'round' },
+ { code: 'rs', desc: 'right side' },
+ { code: 'sc', desc: 'single crochet' },
+ { code: 'sc2tog', desc: 'single crochet 2 stitches together' },
+ { code: 'sh', desc: 'shell' },
+ { code: 'sk', desc: 'skip' },
+ { code: 'sl st', desc: 'slip stitch' },
+ { code: 'sm/sl m', desc: 'slip marker' },
+ { code: 'sp', desc: 'space' },
+ { code: 'st', desc: 'stitch' },
+ { code: 'tbl', desc: 'through back loop' },
+ { code: 'tch/t-ch', desc: 'turning chain' },
+ { code: 'tog', desc: 'together' },
+ { code: 'tr', desc: 'treble crochet' },
+ { code: 'tr2tog', desc: 'treble crochet 2 stitches together' },
+ { code: 'trtr', desc: 'triple treble crochet' },
+ { code: 'ws', desc: 'wrong side' },
+ { code: 'yo', desc: 'yarn over' },
+ { code: 'yoh', desc: 'yarn over hook' },
+ // Tunisian
+ { code: 'etss', desc: 'extended Tunisian simple stitch' },
+ { code: 'fwp', desc: 'forward pass' },
+ { code: 'retp', desc: 'return pass' },
+ { code: 'tdc', desc: 'Tunisian double crochet' },
+ { code: 'tfs', desc: 'Tunisian full stitch' },
+ { code: 'thdc', desc: 'Tunisian half double crochet' },
+ { code: 'tks', desc: 'Tunisian knit stitch' },
+ { code: 'tps', desc: 'Tunisian purl stitch' },
+ { code: 'trs', desc: 'Tunisian reverse stitch' },
+ { code: 'tsc', desc: 'Tunisian single crochet' },
+ { code: 'tss', desc: 'Tunisian simple stitch' },
+ { code: 'tslst', desc: 'Tunisian slip stitch' },
+ { code: 'ttr', desc: 'Tunisian treble crochet' },
+ { code: 'ttw', desc: 'Tunisian twisted' }
+];
+
+const knitAbbrev = [
+ { code: 'alt', desc: 'alternate' },
+ { code: 'approx', desc: 'approximately' },
+ { code: 'beg', desc: 'beginning' },
+ { code: 'bet', desc: 'between' },
+ { code: 'BO', desc: 'bind off' },
+ { code: 'byo', desc: 'backward yarn over' },
+ { code: 'CC', desc: 'contrasting color' },
+ { code: 'cn', desc: 'cable needle' },
+ { code: 'CO', desc: 'cast on' },
+ { code: 'cont', desc: 'continue' },
+ { code: 'dec', desc: 'decrease' },
+ { code: 'dpn', desc: 'double-pointed needles' },
+ { code: 'foll', desc: 'follow' },
+ { code: 'inc', desc: 'increase' },
+ { code: 'k', desc: 'knit' },
+ { code: 'k1B', desc: 'knit stitch in row below' },
+ { code: 'kfb', desc: 'knit 1 front and back (inc)' },
+ { code: 'ksp', desc: 'knit 1, slip back, pass over (dec)' },
+ { code: 'k2tog', desc: 'knit 2 together (dec)' },
+ { code: 'kwise', desc: 'knitwise' },
+ { code: 'LH', desc: 'left hand' },
+ { code: 'lp', desc: 'loop' },
+ { code: 'm', desc: 'marker' },
+ { code: 'M1', desc: 'make one knitwise (inc)' },
+ { code: 'M1R', desc: 'make one right (inc)' },
+ { code: 'M1L', desc: 'make one left (inc)' },
+ { code: 'M1p', desc: 'make one purlwise (inc)' },
+ { code: 'M1rp', desc: 'make one right purlwise (inc)' },
+ { code: 'M1lp', desc: 'make one left purlwise (inc)' },
+ { code: 'MC', desc: 'main color' },
+ { code: 'p', desc: 'purl' },
+ { code: 'pat', desc: 'pattern' },
+ { code: 'pfb', desc: 'purl front and back (inc)' },
+ { code: 'pm', desc: 'place marker' },
+ { code: 'p2tog', desc: 'purl 2 together (dec)' },
+ { code: 'prev', desc: 'previous' },
+ { code: 'psso', desc: 'pass slipped stitch over' },
+ { code: 'p2sso', desc: 'pass 2 slipped stitches over' },
+ { code: 'pwise', desc: 'purlwise' },
+ { code: 'rem', desc: 'remaining' },
+ { code: 'rep', desc: 'repeat' },
+ { code: 'rev St st', desc: 'reverse stockinette stitch' },
+ { code: 'RH', desc: 'right hand' },
+ { code: 'rnd', desc: 'round' },
+ { code: 'RS', desc: 'right side' },
+ { code: 'SKP', desc: 'slip 1 k-wise, k1, pass slipped over (dec)' },
+ { code: 'SK2P', desc: 'slip 1 k-wise, k2tog, pass slipped over (dec)' },
+ { code: 'sl', desc: 'slip' },
+ { code: 'sl1k', desc: 'slip 1 knitwise' },
+ { code: 'sl1p', desc: 'slip 1 purlwise' },
+ { code: 'sl st', desc: 'slip stitch' },
+ { code: 'sm', desc: 'slip marker' },
+ { code: 'ssk', desc: 'slip 2 k-wise, k through back loops (dec)' },
+ { code: 'ssp', desc: 'slip 2 k-wise, return, p through back loops (dec)' },
+ { code: 'sssk', desc: 'slip 3 k-wise, k through back loops (double dec)' },
+ { code: 'sssp', desc: 'slip 3 k-wise, return, p through back loops (double dec)' },
+ { code: 'S2KP2', desc: 'slip 2 as if to k2tog, k1, pass 2 over (double dec)' },
+ { code: 'SSPP2', desc: 'slip 2 k-wise, return, p2tog tbl, p1, pass 2 over (double dec)' },
+ { code: 'st', desc: 'stitch' },
+ { code: 'St st', desc: 'stockinette stitch' },
+ { code: 'tbl', desc: 'through back loop' },
+ { code: 'tfl', desc: 'through front loop' },
+ { code: 'tog', desc: 'together' },
+ { code: 'WS', desc: 'wrong side' },
+ { code: 'w&t', desc: 'wrap and turn' },
+ { code: 'wyib', desc: 'with yarn in back' },
+ { code: 'wyif', desc: 'with yarn in front' },
+ { code: 'yb', desc: 'yarn back' },
+ { code: 'yfwd/yf', desc: 'yarn forward' },
+ { code: 'yo', desc: 'yarn over' },
+ { code: 'yon', desc: 'yarn over needle' },
+ { code: 'yrn', desc: 'yarn round needle' }
+];
+
+const crochetBuckets = [
+ { label: 'Core stitches', codes: ['ch', 'sc', 'hdc', 'dc', 'tr', 'sl st'] },
+ { label: 'Shaping & repeats', codes: ['inc', 'dec', 'tog', 'rep', 'rem', 'rnd', 'rs', 'ws', 'tch/t-ch'] },
+ { label: 'Placement & loops', codes: ['fl/flo', 'bl/blo', 'tbl', 'lp', 'sp', 'ch-sp', 'prev'] },
+ { label: 'Post stitches', codes: ['fp', 'fpdc', 'fptr', 'fpsc', 'fphdc', 'fpdtr', 'bp', 'bpdc', 'bptr', 'bpsc', 'bphdc', 'bpdtr'] },
+ { label: 'Extended & special', codes: ['pat', 'pc', 'ps/puff', 'cl', 'sh', 'dtr', 'edc', 'ehdc', 'esc', 'etr', 'tr2tog', 'trtr', 'yo', 'yoh'] },
+ { label: 'Markers & color', codes: ['mc', 'cc', 'm', 'pm', 'sm/sl m'] },
+ { label: 'Tunisian', codes: ['etss', 'fwp', 'retp', 'tdc', 'tfs', 'thdc', 'tks', 'tps', 'trs', 'tsc', 'tss', 'tslst', 'ttr', 'ttw'] },
+ { label: 'General terms', codes: ['alt', 'approx', 'beg', 'bet', 'cont', 'foll', 'pat', 'prev'] }
+];
+
+const knitBuckets = [
+ { label: 'Core stitches', codes: ['k', 'p', 'yo', 'sl st', 'St st', 'rev St st'] },
+ { label: 'Increases', codes: ['inc', 'M1', 'M1R', 'M1L', 'M1p', 'M1rp', 'M1lp', 'kfb', 'pfb'] },
+ { label: 'Decreases', codes: ['dec', 'k2tog', 'ssk', 'SKP', 'SK2P', 'ssp', 'sssk', 'sssp', 'S2KP2', 'SSPP2', 'p2tog', 'psso', 'p2sso', 'ksp', 'tog'] },
+ { label: 'Slips & markers', codes: ['sl', 'sl1k', 'sl1p', 'm', 'pm', 'sm', 'cn', 'dpn'] },
+ { label: 'Yarn moves', codes: ['wyib', 'wyif', 'yfwd/yf', 'yb', 'yon', 'yrn', 'w&t'] },
+ { label: 'Cast on / bind off', codes: ['CO', 'BO'] },
+ { label: 'Color & pattern', codes: ['MC', 'CC', 'pat'] },
+ { label: 'Positioning', codes: ['k1B', 'tbl', 'tfl', 'lp', 'prev'] },
+ { label: 'Rows & sides', codes: ['rep', 'rem', 'RS', 'WS', 'rnd', 'foll'] },
+ { label: 'General terms', codes: ['alt', 'approx', 'beg', 'bet', 'cont'] }
+];
+
+const repeatTokens = ['*', 'rep from *', '[', ']', '(', ')', 'to end'];
+const nonMergeTokens = new Set(['*', '[', ']', '(', ')', 'rep from *', 'to end']);
+
+function getActiveAbbrevLibrary() {
+ return patternDraft.mode === 'knit' ? knitAbbrev : crochetAbbrev;
+}
+
+function getAbbrevByCode(code) {
+ return crochetAbbrev.find(a => a.code === code) || knitAbbrev.find(a => a.code === code);
+}
+
+function buildAbbrevGroups(library, buckets) {
+ const index = new Map(library.map(item => [item.code, item]));
+ const used = new Set();
+ const groups = [];
+ buckets.forEach(bucket => {
+ const items = [];
+ bucket.codes.forEach(code => {
+ const item = index.get(code);
+ if (item && !used.has(code)) {
+ items.push(item);
+ used.add(code);
+ }
+ });
+ if (items.length) groups.push({ label: bucket.label, items });
+ });
+ const leftover = library.filter(item => !used.has(item.code));
+ if (leftover.length) groups.push({ label: 'Other', items: leftover });
+ return groups;
+}
+
+function getAbbrevGroups() {
+ const library = getActiveAbbrevLibrary();
+ const buckets = patternDraft.mode === 'knit' ? knitBuckets : crochetBuckets;
+ return buildAbbrevGroups(library, buckets);
+}
+
+function getCoreCodes() {
+ const buckets = patternDraft.mode === 'knit' ? knitBuckets : crochetBuckets;
+ const core = buckets.find(b => b.label === 'Core stitches');
+ return core ? core.codes : [];
+}
+
+function getPatternButtonCodes() {
+ const base = (patternDraft.abbrevSelection && patternDraft.abbrevSelection.length)
+ ? patternDraft.abbrevSelection
+ : (patternDraft.mode === 'knit'
+ ? ['k', 'p', 'yo', 'k2tog', 'ssk', 'sl st']
+ : ['ch', 'sc', 'hdc', 'dc', 'tr', 'inc', 'dec', 'sl st', 'sk', 'rep']);
+ const merged = [...base, ...repeatTokens];
+ return Array.from(new Set(merged));
+}
+function normalizePatternDraft(d = {}) {
+ const baseStep = { title: '', rows: [], rowDraft: '', note: '', image: '' };
+ const base = {
+ mode: 'crochet',
+ currentRow: 1,
+ line: '',
+ output: '',
+ meta: { title: '', designer: '' },
+ materials: '',
+ gauge: '',
+ gaugeSts: '',
+ gaugeRows: '',
+ gaugeHook: '',
+ size: '',
+ abbrev: '',
+ abbrevSelection: [],
+ stitches: '',
+ notes: '',
+ steps: []
+ };
+ const merged = { ...base, ...d };
+ merged.meta = { ...base.meta, ...(d.meta || {}) };
+ merged.abbrevSelection = Array.isArray(merged.abbrevSelection) ? merged.abbrevSelection : [];
+ merged.steps = Array.isArray(merged.steps) ? merged.steps.map(s => ({ ...baseStep, ...s })) : [];
+ merged.materials = merged.materials || '';
+ merged.gauge = merged.gauge || '';
+ merged.gaugeSts = merged.gaugeSts || '';
+ merged.gaugeRows = merged.gaugeRows || '';
+ merged.gaugeHook = merged.gaugeHook || '';
+ merged.size = merged.size || '';
+ merged.abbrev = merged.abbrev || '';
+ merged.stitches = merged.stitches || '';
+ merged.notes = merged.notes || '';
+ if (!merged.mode) merged.mode = 'crochet';
+ if (!merged.currentRow) merged.currentRow = 1;
+ if (merged.line === undefined) merged.line = '';
+ if (merged.output === undefined) merged.output = '';
+ return merged;
+}
+
+let patternDraft = normalizePatternDraft(JSON.parse(localStorage.getItem('crochetPatternDraft')) || {});
+// New Earthy/Woodland Palette extracted from image vibes
+const colors = [
+ '#a17d63', // Soft oak
+ '#7a8c6a', // Moss sage
+ '#c7a272', // Warm amber
+ '#b88b8a', // Rose clay
+ '#7b9189', // Sage teal
+ '#aa9a7a', // Wheat linen
+ '#5f6d57', // Forest dusk
+ '#b07d6f', // Terra blush
+ '#6d5947', // Walnut
+ '#c4b08a', // Oat
+ '#7c7565', // Stone
+ '#8ca58c' // Meadow
+];
+const oldColors = [
+ '#b56b54', // Rust/Mushroom
+ '#7a8b4f', // Olive Green
+ '#cba052', // Mustard/Daisy center
+ '#5f8a8b', // Muted Teal
+ '#8c6246', // Warm Wood tone
+ '#a87b8c', // Dusty Rose
+ '#4a5d43', // Deep Forest
+ '#9c7e63' // Taupe
+];
+
+// --- State Variables ---
+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');
+const patternOverlay = document.getElementById('patternOverlay');
+const patternRowNumber = null;
+const patternLine = null;
+const patternOutput = document.getElementById('patternOutput');
+const patternButtonsWrap = null;
+if (patternPicker && patternSelect) populatePatternSelect();
+if (patternOverlay) {
+ patternOverlay.addEventListener('click', (e) => {
+ if (e.target === patternOverlay) closePatternComposer();
+ });
+}
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && patternOverlay && patternOverlay.classList.contains('active')) {
+ closePatternComposer();
+ }
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && patternOverlay && patternOverlay.classList.contains('active')) {
+ e.preventDefault();
+ addStep();
+ }
+});
+
+function persistPatternDraft() {
+ localStorage.setItem('crochetPatternDraft', JSON.stringify(patternDraft));
+ flashSave();
+}
+
+function setPatternMode(mode) {
+ patternDraft.mode = mode;
+ persistPatternDraft();
+ if (!document.querySelector('.pattern-overlay')) return;
+ document.querySelectorAll('.pattern-mode').forEach(btn => {
+ btn.classList.toggle('is-active', btn.dataset.mode === mode);
+ });
+ renderAbbrevChecklist();
+ renderSteps();
+}
+
+function loadDefaultAbbrev() {
+ patternDraft.abbrevSelection = getCoreCodes();
+ updateAbbrevFromSelection();
+ renderAbbrevChecklist();
+}
+
+function bindPatternInputs() {
+ const titleEl = document.getElementById('patternTitle');
+ const designerEl = document.getElementById('patternDesigner');
+ const materialsEl = document.getElementById('patternMaterials');
+ const gaugeEl = document.getElementById('patternGauge');
+ const gaugeStsEl = document.getElementById('patternGaugeSts');
+ const gaugeRowsEl = document.getElementById('patternGaugeRows');
+ const gaugeHookEl = document.getElementById('patternGaugeHook');
+ const sizeEl = document.getElementById('patternSize');
+ const abbrevEl = document.getElementById('patternAbbrev');
+ const stitchesEl = document.getElementById('patternStitches');
+ const notesEl = document.getElementById('patternNotes');
+ const outputEl = document.getElementById('patternOutput');
+ if (titleEl) titleEl.addEventListener('input', e => { updateMetaField('title', e.target.value); });
+ if (designerEl) designerEl.addEventListener('input', e => { updateMetaField('designer', e.target.value); });
+ if (materialsEl) materialsEl.addEventListener('input', e => { updatePatternField('materials', e.target.value); });
+ if (gaugeEl) gaugeEl.addEventListener('input', e => { updatePatternField('gauge', e.target.value); });
+ if (gaugeStsEl) gaugeStsEl.addEventListener('input', e => { updatePatternField('gaugeSts', e.target.value); });
+ if (gaugeRowsEl) gaugeRowsEl.addEventListener('input', e => { updatePatternField('gaugeRows', e.target.value); });
+ if (gaugeHookEl) gaugeHookEl.addEventListener('input', e => { updatePatternField('gaugeHook', e.target.value); });
+ if (sizeEl) sizeEl.addEventListener('input', e => { updatePatternField('size', e.target.value); });
+ if (abbrevEl) abbrevEl.addEventListener('input', e => { updatePatternField('abbrev', e.target.value); });
+ if (stitchesEl) stitchesEl.addEventListener('input', e => { updatePatternField('stitches', e.target.value); });
+ if (notesEl) notesEl.addEventListener('input', e => { updatePatternField('notes', e.target.value); });
+ if (outputEl) outputEl.addEventListener('input', e => {
+ patternDraft.output = e.target.value;
+ const lines = patternDraft.output.split('\n').filter(l => l.trim() !== '');
+ patternDraft.currentRow = Math.max(1, lines.length + 1);
+ persistPatternDraft();
+ });
+}
+
+function syncPatternUI() {
+ setPatternMode(patternDraft.mode);
+ const titleEl = document.getElementById('patternTitle');
+ const designerEl = document.getElementById('patternDesigner');
+ const materialsEl = document.getElementById('patternMaterials');
+ const gaugeEl = document.getElementById('patternGauge');
+ const gaugeStsEl = document.getElementById('patternGaugeSts');
+ const gaugeRowsEl = document.getElementById('patternGaugeRows');
+ const gaugeHookEl = document.getElementById('patternGaugeHook');
+ const sizeEl = document.getElementById('patternSize');
+ const abbrevEl = document.getElementById('patternAbbrev');
+ const stitchesEl = document.getElementById('patternStitches');
+ const notesEl = document.getElementById('patternNotes');
+ if (titleEl) titleEl.value = patternDraft.meta.title;
+ if (designerEl) designerEl.value = patternDraft.meta.designer;
+ if (materialsEl) materialsEl.value = patternDraft.materials;
+ if (gaugeEl) gaugeEl.value = patternDraft.gauge;
+ if (gaugeStsEl) gaugeStsEl.value = patternDraft.gaugeSts;
+ if (gaugeRowsEl) gaugeRowsEl.value = patternDraft.gaugeRows;
+ if (gaugeHookEl) gaugeHookEl.value = patternDraft.gaugeHook;
+ if (sizeEl) sizeEl.value = patternDraft.size;
+ if (abbrevEl) abbrevEl.value = patternDraft.abbrev;
+ if (stitchesEl) stitchesEl.value = patternDraft.stitches;
+ if (notesEl) notesEl.value = patternDraft.notes;
+ renderSteps();
+ renderAbbrevChecklist();
+}
+
+function addPatternToken(tok) {
+ if (!patternLine) return;
+ patternDraft.line = patternDraft.line ? `${patternDraft.line} ${tok}` : tok;
+ patternLine.value = patternDraft.line;
+ persistPatternDraft();
+}
+
+function clearPatternLine() {
+ if (!patternLine) return;
+ patternDraft.line = '';
+ patternLine.value = '';
+ persistPatternDraft();
+}
+
+function addPatternRow() {
+ if (!patternLine || !patternOutput) return;
+ const line = patternDraft.line.trim();
+ if (!line) return;
+ const rowLabel = patternDraft.mode === 'knit' ? 'Row' : 'Rnd';
+ const rowText = `${rowLabel} ${patternDraft.currentRow}: ${line}`;
+ patternDraft.output = patternDraft.output ? `${patternDraft.output}\n${rowText}` : rowText;
+ patternDraft.currentRow += 1;
+ patternDraft.line = '';
+ persistPatternDraft();
+ syncPatternUI();
+}
+
+function removePatternRow() {
+ if (!patternOutput) return;
+ const lines = (patternDraft.output || '').split('\n').filter(l => l.trim() !== '');
+ if (lines.length === 0) return;
+ lines.pop();
+ patternDraft.output = lines.join('\n');
+ patternDraft.currentRow = Math.max(1, lines.length + 1);
+ persistPatternDraft();
+ syncPatternUI();
+}
+
+function clearPatternOutput() {
+ if (!patternOutput) return;
+ patternDraft.output = '';
+ patternDraft.currentRow = 1;
+ patternDraft.line = '';
+ patternDraft.meta = { title: '', designer: '' };
+ patternDraft.materials = '';
+ patternDraft.gauge = '';
+ patternDraft.gaugeSts = '';
+ patternDraft.gaugeRows = '';
+ patternDraft.gaugeHook = '';
+ patternDraft.size = '';
+ patternDraft.abbrev = '';
+ patternDraft.stitches = '';
+ patternDraft.notes = '';
+ patternDraft.steps = [];
+ if (patternOutput) patternOutput.value = '';
+ persistPatternDraft();
+ syncPatternUI();
+}
+
+function exportPatternJSON() {
+ const payload = { patternDraft };
+ const name = patternDraft.meta.title || 'pattern';
+ const filename = `${name.replace(/\s+/g,'_').toLowerCase()}_draft.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 importPatternJSON() {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = 'application/json';
+ input.onchange = async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+ try {
+ const text = await file.text();
+ const data = JSON.parse(text);
+ if (!data.patternDraft) throw new Error('Invalid file');
+ patternDraft = normalizePatternDraft(data.patternDraft);
+ persistPatternDraft();
+ syncPatternUI();
+ } catch (err) {
+ showAlert({ title: 'Import failed', text: err.message });
+ }
+ };
+ input.click();
+}
+
+function exportPatternPDF() {
+ const w = window.open('', '_blank');
+ if (!w) return;
+ const styles = getComputedStyle(document.body);
+ const themeBg = styles.getPropertyValue('--bg') || '#f4f0e8';
+ const themeText = styles.getPropertyValue('--text') || '#2f2b28';
+ const accent = styles.getPropertyValue('--project-color') || '#7a8c6a';
+ const gaugeBlock = `${patternDraft.gaugeSts || ''}${patternDraft.gaugeRows ? ' • ' + patternDraft.gaugeRows : ''}${patternDraft.gaugeHook ? ' • ' + patternDraft.gaugeHook : ''}`;
+ const fontLink = '';
+ const html = `
+
+
+ ${patternDraft.meta.title || 'Pattern'} - PDF
+ ${fontLink}
+
+
+
+ ${patternDraft.meta.title || 'Pattern'}
+ ${patternDraft.meta.designer || ''}
+
+
Materials
+
${patternDraft.materials || ''}
+
+
+
Gauge / Size
+
${gaugeBlock}\n${patternDraft.gauge || ''}\n${patternDraft.size || ''}
+
+
+
Abbreviations
+
${patternDraft.abbrev || ''}
+
+
+
Stitch Guide
+
${patternDraft.stitches || ''}
+
+
+
Steps
+ ${patternDraft.steps.map((s, i) => {
+ const rows = (s.rows || []).map((r, idx) => `Row ${idx + 1}: ${r}`).join('
');
+ const note = s.note ? `
${s.note}` : '';
+ return `
Step ${i + 1}${s.title ? ': ' + s.title : ''}
${rows}${note}
`;
+ }).join('
')}
+
+
+
Rows
+
${patternDraft.output || ''}
+
+
+
Notes
+
${patternDraft.notes || ''}
+
+
+
+ `;
+ w.document.write(html);
+ w.document.close();
+ w.focus();
+ w.print();
+}
+
+function showPatternTab(tab) {
+ document.querySelectorAll('.pattern-tab').forEach(btn => btn.classList.toggle('is-active', btn.dataset.tab === tab));
+ document.querySelectorAll('.pattern-section').forEach(sec => sec.classList.toggle('is-active', sec.dataset.section === tab));
+}
+
+function updateMetaField(field, value) {
+ patternDraft.meta[field] = value;
+ persistPatternDraft();
+}
+
+function updatePatternField(field, value) {
+ patternDraft[field] = value;
+ persistPatternDraft();
+}
+
+function renderSteps() {
+ const container = document.getElementById('patternSteps');
+ if (!container) return;
+ container.innerHTML = '';
+ patternDraft.steps.forEach((step, idx) => {
+ const card = document.createElement('div');
+ card.className = 'pattern-step-card card-pop';
+ const rows = step.rows || [];
+ const rowList = rows.map((r, i) => `
+
+
+
+
+
+ `).join('');
+ card.innerHTML = `
+ Step ${idx + 1}
+
+
+
+ ${getPatternButtonCodes().map(tok => ``).join('')}
+
+
+
+
+
+
+
+
+ ${rowList}
+
+
+
+
+
+
+
+
+
+ `;
+ card.querySelectorAll('input, textarea').forEach(el => {
+ el.addEventListener('input', (e) => {
+ const i = Number(e.target.dataset.idx);
+ const field = e.target.dataset.field;
+ if (!patternDraft.steps[i]) return;
+ patternDraft.steps[i][field] = e.target.value;
+ persistPatternDraft();
+ });
+ });
+ card.querySelectorAll('.pattern-buttons button').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const tok = btn.dataset.tok;
+ addPatternTokenToStep(idx, tok);
+ });
+ });
+ card.querySelectorAll('input[data-row]').forEach(inp => {
+ inp.addEventListener('input', (e) => {
+ const rowIdx = Number(e.target.dataset.row);
+ updateStepRow(idx, rowIdx, e.target.value);
+ });
+ });
+ const rowTextarea = card.querySelector('textarea[data-field="rowDraft"]');
+ if (rowTextarea) {
+ rowTextarea.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
+ e.preventDefault();
+ addStepRow(idx);
+ } else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
+ e.preventDefault();
+ addStep();
+ }
+ });
+ }
+ requestAnimationFrame(() => {
+ card.classList.remove('card-pop');
+ });
+ container.appendChild(card);
+ });
+ const addRow = document.createElement('div');
+ addRow.className = 'pattern-step-card add-step-row';
+ addRow.innerHTML = `
+
+ `;
+ container.appendChild(addRow);
+}
+
+function addStep() {
+ patternDraft.steps.push({ title: '', rows: [], rowDraft: '', note: '', image: '' });
+ persistPatternDraft();
+ renderSteps();
+}
+
+function addStepRow(idx) {
+ const step = patternDraft.steps[idx];
+ if (!step) return;
+ const line = (step.rowDraft || '').trim();
+ if (!line) return;
+ step.rows.push(line);
+ step.rowDraft = '';
+ persistPatternDraft();
+ renderSteps();
+}
+
+function removeStepLastRow(idx) {
+ // no-op: last-row removal handled per-row delete
+}
+
+function removeStepRow(idx, rowIdx) {
+ const step = patternDraft.steps[idx];
+ if (!step || !step.rows || rowIdx < 0 || rowIdx >= step.rows.length) return;
+ step.rows.splice(rowIdx, 1);
+ persistPatternDraft();
+ renderSteps();
+}
+
+function clearStepRow(idx) {
+ const step = patternDraft.steps[idx];
+ if (!step) return;
+ step.rowDraft = '';
+ persistPatternDraft();
+ renderSteps();
+}
+
+function updateStepRow(idx, rowIdx, value) {
+ const step = patternDraft.steps[idx];
+ if (!step || !step.rows || rowIdx < 0 || rowIdx >= step.rows.length) return;
+ step.rows[rowIdx] = value;
+ persistPatternDraft();
+}
+
+function renderAbbrevChecklist() {
+ const listEl = document.getElementById('patternAbbrevList');
+ if (!listEl) return;
+ listEl.innerHTML = '';
+ const selected = new Set(patternDraft.abbrevSelection);
+ getAbbrevGroups().forEach(bucket => {
+ const wrap = document.createElement('details');
+ wrap.className = 'abbrev-group';
+ wrap.open = true;
+ const summary = document.createElement('summary');
+ summary.textContent = `${bucket.label} (${bucket.items.length})`;
+ wrap.appendChild(summary);
+ const grid = document.createElement('div');
+ grid.className = 'abbrev-grid';
+ bucket.items.forEach(item => {
+ const pill = document.createElement('div');
+ pill.className = 'abbrev-pill';
+ if (selected.has(item.code)) pill.classList.add('is-selected');
+ pill.dataset.code = item.code;
+ pill.innerHTML = `
+ ${item.code}
+ ${item.desc}
+ `;
+ pill.addEventListener('click', () => {
+ const code = pill.dataset.code;
+ if (pill.classList.contains('is-selected')) {
+ pill.classList.remove('is-selected');
+ patternDraft.abbrevSelection = patternDraft.abbrevSelection.filter(c => c !== code);
+ } else {
+ pill.classList.add('is-selected');
+ if (!patternDraft.abbrevSelection.includes(code)) patternDraft.abbrevSelection.push(code);
+ }
+ updateAbbrevFromSelection();
+ });
+ grid.appendChild(pill);
+ });
+ wrap.appendChild(grid);
+ listEl.appendChild(wrap);
+ });
+ renderSelectedAbbrev();
+}
+
+function updateAbbrevFromSelection() {
+ const lines = patternDraft.abbrevSelection.map(code => {
+ const found = getAbbrevByCode(code);
+ return found ? `${found.code} – ${found.desc}` : code;
+ });
+ patternDraft.abbrev = lines.join('\n');
+ persistPatternDraft();
+ const abbrevEl = document.getElementById('patternAbbrev');
+ if (abbrevEl) abbrevEl.value = patternDraft.abbrev;
+ renderSelectedAbbrev();
+ renderSteps();
+}
+
+function renderSelectedAbbrev() {
+ renderPatternButtons();
+}
+
+function renderPatternButtons() {
+ // Buttons are rendered per-step; no global buttons.
+}
+
+function deleteStep(idx) {
+ patternDraft.steps.splice(idx, 1);
+ persistPatternDraft();
+ renderSteps();
+}
+
+function moveStep(idx, dir) {
+ const target = idx + dir;
+ if (target < 0 || target >= patternDraft.steps.length) return;
+ const tmp = patternDraft.steps[idx];
+ patternDraft.steps[idx] = patternDraft.steps[target];
+ patternDraft.steps[target] = tmp;
+ persistPatternDraft();
+ renderSteps();
+}
+
+function addPatternTokenToStep(idx, tok) {
+ const step = patternDraft.steps[idx];
+ if (!step) return;
+ // Append with comma/space
+ if (!step.rowDraft) {
+ step.rowDraft = tok;
+ } else {
+ // If last token matches, increment count
+ const parts = step.rowDraft.split(',').map(s => s.trim()).filter(Boolean);
+ if (parts.length && !nonMergeTokens.has(tok)) {
+ const last = parts[parts.length - 1];
+ const escapedTok = tok.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const match = last.match(new RegExp(`^${escapedTok}(\\d+)$`, 'i'));
+ if (last.toLowerCase() === tok.toLowerCase()) {
+ parts[parts.length - 1] = `${tok}2`;
+ } else if (match) {
+ const n = parseInt(match[1], 10) + 1;
+ parts[parts.length - 1] = `${tok}${n}`;
+ } else {
+ parts.push(tok);
+ }
+ step.rowDraft = parts.join(', ');
+ } else {
+ parts.push(tok);
+ step.rowDraft = parts.join(', ');
+ }
+ }
+ persistPatternDraft();
+ renderSteps();
+}
+
+function addStepRow(idx) {
+ const step = patternDraft.steps[idx];
+ if (!step) return;
+ const line = (step.rowDraft || '').trim();
+ if (!line) return;
+ step.rows.push(line);
+ step.rowDraft = '';
+ persistPatternDraft();
+ renderSteps();
+}
+
+function removeStepRow(idx) {
+ const step = patternDraft.steps[idx];
+ if (!step || !step.rows || step.rows.length === 0) return;
+ step.rows.pop();
+ persistPatternDraft();
+ renderSteps();
+}
+
+function clearStepRow(idx) {
+ const step = patternDraft.steps[idx];
+ if (!step) return;
+ step.rowDraft = '';
+ persistPatternDraft();
+ renderSteps();
+}
+
+function openPatternComposer() {
+ if (!patternOverlay) return;
+ patternOverlay.classList.add('active');
+ syncPatternUI();
+ showPatternTab('steps');
+}
+
+function closePatternComposer() {
+ if (!patternOverlay) return;
+ patternOverlay.classList.remove('active');
+}
+syncPatternUI();
+bindPatternInputs();
+renderAbbrevChecklist();
+let pendingSaveSelection = [];
+let lastCountPulse = null;
+let lastFinishedId = null;
+let fireflyTimer = null;
+let fireflyActive = false;
+let titleClicks = [];
+let easterEggCooling = false;
+
+// --- Service Worker ---
+if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker.register('/sw.js').catch(() => {
+ // fail silently; optional log
+ });
+ });
+}
+
+// --- Install Prompt ---
+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();
+
+// --- Sweet-ish Alerts ---
+function removeSwal() {
+ const existing = document.querySelector('.swal-overlay');
+ if (existing) existing.remove();
+}
+
+function showConfirm({ title = 'Are you sure?', text = '', confirmText = 'Yes', cancelText = 'Cancel', danger = false } = {}) {
+ return new Promise(resolve => {
+ removeSwal();
+ const overlay = document.createElement('div');
+ overlay.className = 'swal-overlay';
+ overlay.innerHTML = `
+
+
${title}
+
${text}
+
+
+
+
+
+ `;
+ 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 = 'Notice', text = '' } = {}) {
+ return new Promise(resolve => {
+ removeSwal();
+ const overlay = document.createElement('div');
+ overlay.className = 'swal-overlay';
+ overlay.innerHTML = `
+
+
${title}
+
${text}
+
+
+
+
+ `;
+ 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);
+ });
+}
+
+// --- Theme Logic ---
+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 => `
+
+ `).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,
+ animationsEnabled,
+ 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 = `
+
+ ${p.name}
+ `;
+ 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();
+}
+
+// --- Firefly Animation ---
+function spawnFirefly({ markActive = false, source = 'ambient', 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; // 5vh–60vh
+ const scale = 0.9 + Math.random() * 0.4;
+ const duration = 12 + Math.random() * 8; // 12–20s
+ 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 = false, 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; // 5vh–60vh
+ const scale = 0.85 + Math.random() * 0.4;
+ const duration = 14 + Math.random() * 8; // 14–22s
+ const tilt = (Math.random() * 16 + 8) * (Math.random() < 0.5 ? -1 : 1); // +/-8–24deg
+ const sway = 4 + Math.random() * 6; // px
+ const flipDur = 5 + Math.random() * 4; // 5–9s
+ 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 = 10000 + Math.random() * 10000; // 10–20s
+ 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 < 7000);
+ titleClicks.push(now);
+ if (titleClicks.length >= 5 && !easterEggCooling) {
+ easterEggCooling = true;
+ triggerBurst();
+ setTimeout(() => {
+ easterEggCooling = false;
+ titleClicks = [];
+ }, 8000);
+ }
+ });
+}
+
+function triggerBurst() {
+ if (!animationsEnabled) return;
+ const burstCount = isDarkMode ? 24 : 18;
+ const spawner = isDarkMode ? (opts) => spawnFirefly({ ...opts, side: 'any' }) : spawnSeed;
+ for (let i = 0; i < burstCount; i++) {
+ const jitter = Math.random() * 200;
+ setTimeout(() => spawner({ source: 'burst' }), i * 140 + jitter);
+ }
+}
+
+// --- Focus Mode Logic ---
+let wakeLock = null;
+let isFocusMode = false;
+const focusBtn = document.getElementById('focusBtn');
+
+// --- Migration Check ---
+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)); }
+}
+
+// --- Core Functions ---
+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');
+ }
+});
+
+// --- Interaction Logic ---
+ 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, 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, 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);
+}
+
+// --- Modal Logic ---
+function openModal(type, pId = null, partId = null) {
+ modalState = { type, pId, 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));
+ }
+
+// --- Render Logic ---
+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
+ ? ``
+ : `
+
+
+
+
+
`;
+ const countSubtext = part.minimized ? '' : `
+
+ ${part.max !== null ? `${part.count} / ${part.max}` : 'No max set'}
+
+
+ `;
+
+ partsHtml += `
+
+
+
${part.count}
+ ${countSubtext}
+
+
+
+
+
+
+
+
+
+
`;
+ });
+
+ const projectContainer = document.createElement('div');
+ projectContainer.className = `project-container ${projectCollapsedClass}`;
+ projectContainer.style = `--project-color: ${project.color}`;
+ const projectNoteId = `project-note-${project.id}`;
+ projectContainer.innerHTML = `
+
+
+
+
+
+ ${partsHtml}
+ `;
+ grid.appendChild(projectContainer);
+ });
+
+ lastCountPulse = null;
+ lastFinishedId = null;
+ app.appendChild(grid);
+}
+
+render();
+function flashSave() {
+ const el = document.getElementById('patternSaveIndicator');
+ if (!el) return;
+ el.textContent = 'Saved';
+ el.style.opacity = '1';
+ if (saveFlashTimer) clearTimeout(saveFlashTimer);
+ saveFlashTimer = setTimeout(() => {
+ el.style.opacity = '0.6';
+ }, 1200);
+}
diff --git a/assets/style.css b/assets/style.css
index 966a10c..4e6f226 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -54,7 +54,7 @@ body.dark-mode {
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
body {
- font-family: 'Quicksand', "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ font-family: 'Nunito', "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg);
min-height: 100vh;
color: var(--text);
@@ -97,7 +97,7 @@ h1 {
letter-spacing: 0.5px;
color: var(--header-text);
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
- font-family: 'Cormorant Garamond', Georgia, serif;
+ font-family: 'Playfair Display', Georgia, serif;
cursor: pointer;
user-select: none;
}
@@ -135,6 +135,189 @@ h1 {
.header-btn.is-active { background: var(--header-text); color: var(--header-bg); }
.hidden { display: none !important; }
.hidden-input { display: none; }
+
+.fab-pattern { right: auto; left: 22px; background: var(--text); }
+.fab-pattern:hover { transform: scale(1.07); }
+
+/* Pattern Composer Overlay */
+.pattern-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(44, 35, 25, 0.55);
+ backdrop-filter: blur(2px);
+ display: none;
+ align-items: center;
+ justify-content: center;
+ z-index: 215;
+ padding: 14px;
+}
+.pattern-overlay.active { display: flex; }
+.pattern-sheet {
+ width: min(1280px, 96vw);
+ max-height: 92vh;
+ overflow: auto;
+ background: var(--card-bg);
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ padding: 16px;
+ box-shadow: 0 16px 40px rgba(0,0,0,0.25);
+}
+.pattern-sheet-header {
+ display: grid;
+ grid-template-columns: 1fr auto auto auto;
+ gap: 10px;
+ align-items: center;
+ margin-bottom: 12px;
+}
+.pattern-sheet-title h2 { margin: 0; font-size: 1.2rem; color: var(--text); }
+.pattern-sheet-subtitle { margin: 2px 0 0; color: var(--text-muted); font-size: 0.9rem; }
+.pattern-sheet-header h2 { margin: 0; font-size: 1.2rem; color: var(--text); }
+.pattern-close {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ color: var(--text-muted);
+ cursor: pointer;
+}
+.pattern-close:hover { color: var(--project-color); }
+.pattern-modes { display: flex; gap: 8px; justify-content: flex-end; }
+.pattern-mode { border: 1px solid var(--border); background: var(--input-bg); color: var(--text); padding: 6px 10px; border-radius: 10px; cursor: pointer; }
+.pattern-mode.is-active { background: var(--project-color); color: var(--card-bg); border-color: var(--project-color); }
+.pattern-save-indicator { color: var(--text-muted); font-size: 0.9rem; }
+.pattern-body { display: grid; gap: 12px; }
+.pattern-row-info { color: var(--text-muted); font-size: 0.95rem; }
+.pattern-buttons { display: flex; flex-wrap: wrap; gap: 6px; }
+.pattern-buttons button { padding: 6px 10px; border-radius: 10px; border: 1px solid var(--border); background: var(--input-bg); cursor: pointer; color: #fff; }
+.pattern-buttons button:hover { border-color: var(--project-color); color: var(--project-color); }
+.pattern-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
+.pattern-tab {
+ background: var(--input-bg);
+ border: 1px solid var(--border);
+ padding: 6px 10px;
+ border-radius: 10px;
+ cursor: pointer;
+ color: var(--text);
+}
+.pattern-tab.is-active { background: var(--project-color); color: var(--card-bg); border-color: var(--project-color); }
+.pattern-section { display: none; border: 1px dashed var(--border); padding: 12px; border-radius: 12px; background: var(--card-bg); }
+.pattern-section.is-active { display: block; }
+.field-label { display: block; margin: 8px 0 4px; color: var(--text-muted); font-size: 0.9rem; }
+.pattern-section input, .pattern-section textarea {
+ width: 100%;
+ border-radius: 12px;
+ border: 1px solid var(--border);
+ background: var(--input-bg);
+ color: var(--text);
+ padding: 10px;
+ font-family: inherit;
+}
+.field-group-inline { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px; align-items: center; }
+.pattern-row-editor textarea,
+.pattern-output textarea {
+ width: 100%;
+ min-height: 60px;
+ border-radius: 12px;
+ border: 1px solid var(--border);
+ background: var(--input-bg);
+ color: #fff;
+ padding: 10px;
+ font-family: inherit;
+}
+.pattern-row-editor { display: grid; gap: 8px; }
+.pattern-row-actions { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; }
+.pattern-row-actions .primary { background: var(--project-color); color: var(--card-bg); border: none; padding: 8px 12px; border-radius: 10px; cursor: pointer; }
+.pattern-row-actions .secondary { background: var(--input-bg); color: var(--text); border: 1px solid var(--border); padding: 8px 12px; border-radius: 10px; cursor: pointer; }
+.pattern-footer { justify-content: space-between; border-top: 1px solid var(--border); padding-top: 8px; }
+.pattern-output { display: grid; gap: 6px; }
+.pattern-steps-head { display: flex; align-items: center; justify-content: space-between; }
+.pattern-step-card {
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 10px;
+ margin-bottom: 10px;
+ background: var(--card-bg);
+ transition: box-shadow 0.2s ease, transform 0.18s ease;
+}
+.pattern-step-card.card-pop { animation: cardPop 0.28s ease; }
+.pattern-step-card:hover { box-shadow: 0 8px 18px rgba(0,0,0,0.08); transform: translateY(-1px); }
+.pattern-row-list { display: grid; gap: 6px; }
+.row-item { display: grid; grid-template-columns: auto 1fr auto; gap: 6px; align-items: center; }
+.row-item label { color: var(--text-muted); font-size: 0.9rem; }
+.row-item input {
+ width: 100%;
+ padding: 6px 8px;
+ border-radius: 10px;
+ border: 1px solid var(--border);
+ background: var(--input-bg);
+ color: var(--text);
+}
+.row-item button { border: 1px solid var(--border); background: var(--btn-secondary-bg); color: var(--text); border-radius: 8px; padding: 4px 8px; cursor: pointer; }
+.pattern-step-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; flex-wrap: wrap; }
+.add-step-row { display: flex; justify-content: center; padding: 10px; border: 1px dashed var(--border); background: var(--card-bg); }
+.add-step-row .add-step-btn { padding: 10px 16px; border-radius: 12px; border: none; background: var(--project-color); color: var(--card-bg); cursor: pointer; font-weight: 700; }
+.add-step-row .add-step-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 14px rgba(0,0,0,0.12); }
+.add-step-row .add-step-btn:active { transform: translateY(0); box-shadow: none; }
+.step-number {
+ font-weight: 800;
+ color: var(--project-color);
+ margin-bottom: 4px;
+ font-size: 1rem;
+}
+@keyframes cardPop {
+ 0% { transform: translateY(6px) scale(0.97); opacity: 0; }
+ 60% { transform: translateY(-2px) scale(1.01); opacity: 1; }
+ 100% { transform: translateY(0) scale(1); opacity: 1; }
+}
+.abbrev-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
+.abbrev-group { border: 1px solid var(--border); border-radius: 12px; margin: 6px 0; background: var(--card-bg); }
+.abbrev-group[open] > summary { border-bottom: 1px solid var(--border); }
+.abbrev-group summary {
+ cursor: pointer;
+ padding: 8px 10px;
+ font-weight: 700;
+ color: var(--text);
+ list-style: none;
+}
+.abbrev-group summary::-webkit-details-marker { display: none; }
+.abbrev-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
+ gap: 6px;
+ padding: 8px 10px 10px;
+}
+.abbrev-pill {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ background: var(--input-bg);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 8px 10px;
+ cursor: pointer;
+ font-size: 0.92rem;
+ transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
+}
+.abbrev-pill:hover { transform: translateY(-1px); border-color: var(--project-color); }
+.abbrev-pill.is-selected {
+ background: var(--project-color);
+ border-color: var(--project-color);
+ color: var(--card-bg);
+}
+.abbrev-pill .code { font-weight: 700; color: inherit; }
+.abbrev-pill .desc { color: inherit; opacity: 0.85; font-size: 0.85rem; }
+.selected-abbrev {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin: 6px 0 10px;
+}
+.selected-abbrev .pill {
+ background: var(--project-color);
+ color: var(--card-bg);
+ padding: 4px 8px;
+ border-radius: 10px;
+ font-size: 0.85rem;
+}
.color-overlay {
position: fixed;
inset: 0;
@@ -156,7 +339,7 @@ h1 {
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
border: 1px solid var(--border);
}
-.color-title { margin: 0 0 12px; font-family: 'Cormorant Garamond', Georgia, serif; }
+.color-title { margin: 0 0 12px; font-family: 'Playfair Display', Georgia, serif; }
.color-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
@@ -277,7 +460,7 @@ h1 {
.project-title {
font-size: 1.4rem; font-weight: 800; color: var(--project-color);
text-transform: uppercase; letter-spacing: 1px;
- font-family: 'Cormorant Garamond', Georgia, serif;
+ font-family: 'Playfair Display', Georgia, serif;
}
.btn-toggle-project {
@@ -686,7 +869,7 @@ button:active { transform: scale(0.97); box-shadow: none; }
}
.swal-title {
- font-family: 'Cormorant Garamond', Georgia, serif;
+ font-family: 'Playfair Display', Georgia, serif;
font-size: 1.4rem;
margin: 0 0 8px;
}
diff --git a/assets/style.min.css b/assets/style.min.css
index 2947a09..4e6f226 100644
--- a/assets/style.min.css
+++ b/assets/style.min.css
@@ -1 +1,899 @@
-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
+:root {
+ /* --- Woodland Theme (Light) --- */
+ --bg: #e6e0d0; /* parchment */
+ --card-bg: #f9f4e6; /* cream */
+ --header-bg: #8a6b52; /* warm oak */
+ --header-text: #fdf8f0; /* linen */
+ --text: #433628; /* cocoa */
+ --text-muted: #7a6d5c;
+ --border: #d6cabc;
+ --shadow: 0 4px 10px rgba(67, 54, 40, 0.12);
+ --seed-opacity: 0.06;
+
+
+ --modal-bg: #f8f5e6;
+ --input-bg: #fffdf5;
+ --input-border: #d1c7b7;
+
+ --bg-finished: #dbe4ce; /* Muted sage green */
+ --lock-btn-bg: #ebe7da;
+ --lock-btn-text: #9c8e7e;
+ --btn-secondary-bg: #fdfcf5;
+ --btn-secondary-text: #4a3b2a;
+
+ /* Status Colors (Muted earthy tones) */
+ --danger: #b56b54; /* Rusty red/orange */
+ --success: #7a8b4f; /* Mossy green */
+
+ /* Dynamic Project Color (Default Rust) */
+ --project-color: #b56b54;
+}
+
+/* --- Woodland Theme (Dark) --- */
+body.dark-mode {
+ --bg: #2c3327; /* Deep forest background */
+ --card-bg: #3b342c; /* Dark wood card */
+ --header-bg: #4b3829; /* Darker wood header */
+ --header-text: #e9e4d7;
+ --text: #e9e4d7; /* Cream text */
+ --text-muted: #b5aa9b;
+ --border: #5f5245;
+ --shadow: 0 4px 10px rgba(0,0,0,0.45);
+
+ --modal-bg: #3d362d;
+ --input-bg: #2c2720;
+ --input-border: #594e3f;
+
+ --bg-finished: #2a382a; /* Dark moss */
+ --lock-btn-bg: #4a4238;
+ --lock-btn-text: #8c8276;
+ --btn-secondary-bg: #4a4238;
+ --btn-secondary-text: #d1c7b7;
+}
+
+* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
+
+body {
+ font-family: 'Nunito', "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ background-color: var(--bg);
+ min-height: 100vh;
+ color: var(--text);
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ overflow-x: hidden;
+ scrollbar-gutter: stable; /* Prevent layout shift when scrollbar appears/disappears */
+ transition: background-color 0.3s, color 0.3s;
+}
+
+body.theme-animating { animation: themeSwap 0.7s ease-in-out; }
+@keyframes themeSwap {
+ 0% { filter: brightness(1.08) saturate(1.06); opacity: 0.85; }
+ 45% { filter: brightness(1.03) saturate(1.12) contrast(1.04); opacity: 0.92; }
+ 75% { filter: brightness(0.98) saturate(1.04); opacity: 0.97; }
+ 100% { filter: brightness(1) saturate(1); opacity: 1; }
+}
+
+header {
+ background-color: var(--header-bg);
+ color: var(--header-text);
+ padding: 0.8rem 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ border-bottom: 2px solid rgba(0,0,0,0.05);
+ transition: background-color 0.3s;
+}
+
+h1 {
+ margin: 0;
+ font-size: 1.3rem;
+ font-weight: 800;
+ letter-spacing: 0.5px;
+ color: var(--header-text);
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
+ font-family: 'Playfair Display', Georgia, serif;
+ cursor: pointer;
+ user-select: none;
+}
+
+.brand { display: flex; align-items: center; gap: 10px; }
+
+.brand-icon {
+ width: 52px; height: 52px; border-radius: 8px;
+ object-fit: contain; display: block;
+ background: rgba(255,255,255,0.12); padding: 4px;
+ box-shadow: inset 0 1px 2px rgba(0,0,0,0.08);
+}
+
+.header-controls { display: flex; gap: 10px; }
+
+.header-btn {
+ background: rgba(255,255,255,0.2);
+ border: none;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.2rem;
+ cursor: pointer;
+ color: var(--header-text);
+ transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.2s;
+ transform: translateY(0);
+ font-family: inherit;
+}
+.header-btn:hover { transform: translateY(-1px) scale(1.03); box-shadow: 0 6px 14px rgba(0,0,0,0.12); }
+.header-btn:active { transform: translateY(0) scale(0.96); box-shadow: none; }
+
+.header-btn.is-active { background: var(--header-text); color: var(--header-bg); }
+.hidden { display: none !important; }
+.hidden-input { display: none; }
+
+.fab-pattern { right: auto; left: 22px; background: var(--text); }
+.fab-pattern:hover { transform: scale(1.07); }
+
+/* Pattern Composer Overlay */
+.pattern-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(44, 35, 25, 0.55);
+ backdrop-filter: blur(2px);
+ display: none;
+ align-items: center;
+ justify-content: center;
+ z-index: 215;
+ padding: 14px;
+}
+.pattern-overlay.active { display: flex; }
+.pattern-sheet {
+ width: min(1280px, 96vw);
+ max-height: 92vh;
+ overflow: auto;
+ background: var(--card-bg);
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ padding: 16px;
+ box-shadow: 0 16px 40px rgba(0,0,0,0.25);
+}
+.pattern-sheet-header {
+ display: grid;
+ grid-template-columns: 1fr auto auto auto;
+ gap: 10px;
+ align-items: center;
+ margin-bottom: 12px;
+}
+.pattern-sheet-title h2 { margin: 0; font-size: 1.2rem; color: var(--text); }
+.pattern-sheet-subtitle { margin: 2px 0 0; color: var(--text-muted); font-size: 0.9rem; }
+.pattern-sheet-header h2 { margin: 0; font-size: 1.2rem; color: var(--text); }
+.pattern-close {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ color: var(--text-muted);
+ cursor: pointer;
+}
+.pattern-close:hover { color: var(--project-color); }
+.pattern-modes { display: flex; gap: 8px; justify-content: flex-end; }
+.pattern-mode { border: 1px solid var(--border); background: var(--input-bg); color: var(--text); padding: 6px 10px; border-radius: 10px; cursor: pointer; }
+.pattern-mode.is-active { background: var(--project-color); color: var(--card-bg); border-color: var(--project-color); }
+.pattern-save-indicator { color: var(--text-muted); font-size: 0.9rem; }
+.pattern-body { display: grid; gap: 12px; }
+.pattern-row-info { color: var(--text-muted); font-size: 0.95rem; }
+.pattern-buttons { display: flex; flex-wrap: wrap; gap: 6px; }
+.pattern-buttons button { padding: 6px 10px; border-radius: 10px; border: 1px solid var(--border); background: var(--input-bg); cursor: pointer; color: #fff; }
+.pattern-buttons button:hover { border-color: var(--project-color); color: var(--project-color); }
+.pattern-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
+.pattern-tab {
+ background: var(--input-bg);
+ border: 1px solid var(--border);
+ padding: 6px 10px;
+ border-radius: 10px;
+ cursor: pointer;
+ color: var(--text);
+}
+.pattern-tab.is-active { background: var(--project-color); color: var(--card-bg); border-color: var(--project-color); }
+.pattern-section { display: none; border: 1px dashed var(--border); padding: 12px; border-radius: 12px; background: var(--card-bg); }
+.pattern-section.is-active { display: block; }
+.field-label { display: block; margin: 8px 0 4px; color: var(--text-muted); font-size: 0.9rem; }
+.pattern-section input, .pattern-section textarea {
+ width: 100%;
+ border-radius: 12px;
+ border: 1px solid var(--border);
+ background: var(--input-bg);
+ color: var(--text);
+ padding: 10px;
+ font-family: inherit;
+}
+.field-group-inline { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px; align-items: center; }
+.pattern-row-editor textarea,
+.pattern-output textarea {
+ width: 100%;
+ min-height: 60px;
+ border-radius: 12px;
+ border: 1px solid var(--border);
+ background: var(--input-bg);
+ color: #fff;
+ padding: 10px;
+ font-family: inherit;
+}
+.pattern-row-editor { display: grid; gap: 8px; }
+.pattern-row-actions { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; }
+.pattern-row-actions .primary { background: var(--project-color); color: var(--card-bg); border: none; padding: 8px 12px; border-radius: 10px; cursor: pointer; }
+.pattern-row-actions .secondary { background: var(--input-bg); color: var(--text); border: 1px solid var(--border); padding: 8px 12px; border-radius: 10px; cursor: pointer; }
+.pattern-footer { justify-content: space-between; border-top: 1px solid var(--border); padding-top: 8px; }
+.pattern-output { display: grid; gap: 6px; }
+.pattern-steps-head { display: flex; align-items: center; justify-content: space-between; }
+.pattern-step-card {
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 10px;
+ margin-bottom: 10px;
+ background: var(--card-bg);
+ transition: box-shadow 0.2s ease, transform 0.18s ease;
+}
+.pattern-step-card.card-pop { animation: cardPop 0.28s ease; }
+.pattern-step-card:hover { box-shadow: 0 8px 18px rgba(0,0,0,0.08); transform: translateY(-1px); }
+.pattern-row-list { display: grid; gap: 6px; }
+.row-item { display: grid; grid-template-columns: auto 1fr auto; gap: 6px; align-items: center; }
+.row-item label { color: var(--text-muted); font-size: 0.9rem; }
+.row-item input {
+ width: 100%;
+ padding: 6px 8px;
+ border-radius: 10px;
+ border: 1px solid var(--border);
+ background: var(--input-bg);
+ color: var(--text);
+}
+.row-item button { border: 1px solid var(--border); background: var(--btn-secondary-bg); color: var(--text); border-radius: 8px; padding: 4px 8px; cursor: pointer; }
+.pattern-step-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; flex-wrap: wrap; }
+.add-step-row { display: flex; justify-content: center; padding: 10px; border: 1px dashed var(--border); background: var(--card-bg); }
+.add-step-row .add-step-btn { padding: 10px 16px; border-radius: 12px; border: none; background: var(--project-color); color: var(--card-bg); cursor: pointer; font-weight: 700; }
+.add-step-row .add-step-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 14px rgba(0,0,0,0.12); }
+.add-step-row .add-step-btn:active { transform: translateY(0); box-shadow: none; }
+.step-number {
+ font-weight: 800;
+ color: var(--project-color);
+ margin-bottom: 4px;
+ font-size: 1rem;
+}
+@keyframes cardPop {
+ 0% { transform: translateY(6px) scale(0.97); opacity: 0; }
+ 60% { transform: translateY(-2px) scale(1.01); opacity: 1; }
+ 100% { transform: translateY(0) scale(1); opacity: 1; }
+}
+.abbrev-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
+.abbrev-group { border: 1px solid var(--border); border-radius: 12px; margin: 6px 0; background: var(--card-bg); }
+.abbrev-group[open] > summary { border-bottom: 1px solid var(--border); }
+.abbrev-group summary {
+ cursor: pointer;
+ padding: 8px 10px;
+ font-weight: 700;
+ color: var(--text);
+ list-style: none;
+}
+.abbrev-group summary::-webkit-details-marker { display: none; }
+.abbrev-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
+ gap: 6px;
+ padding: 8px 10px 10px;
+}
+.abbrev-pill {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ background: var(--input-bg);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 8px 10px;
+ cursor: pointer;
+ font-size: 0.92rem;
+ transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
+}
+.abbrev-pill:hover { transform: translateY(-1px); border-color: var(--project-color); }
+.abbrev-pill.is-selected {
+ background: var(--project-color);
+ border-color: var(--project-color);
+ color: var(--card-bg);
+}
+.abbrev-pill .code { font-weight: 700; color: inherit; }
+.abbrev-pill .desc { color: inherit; opacity: 0.85; font-size: 0.85rem; }
+.selected-abbrev {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin: 6px 0 10px;
+}
+.selected-abbrev .pill {
+ background: var(--project-color);
+ color: var(--card-bg);
+ padding: 4px 8px;
+ border-radius: 10px;
+ font-size: 0.85rem;
+}
+.color-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(44, 35, 25, 0.55);
+ display: none;
+ align-items: center;
+ justify-content: center;
+ z-index: 210;
+ padding: 20px;
+ backdrop-filter: blur(2px);
+}
+.color-overlay.active { display: flex; }
+.color-modal {
+ background: var(--card-bg);
+ color: var(--text);
+ border-radius: 16px;
+ padding: 18px 18px 14px;
+ width: min(420px, 90vw);
+ box-shadow: 0 10px 30px rgba(0,0,0,0.25);
+ border: 1px solid var(--border);
+}
+.color-title { margin: 0 0 12px; font-family: 'Playfair Display', Georgia, serif; }
+.color-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
+ gap: 10px;
+ margin-bottom: 12px;
+}
+.color-custom {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 12px;
+}
+.color-custom input[type="color"] {
+ width: 42px;
+ height: 32px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--card-bg);
+ padding: 0;
+}
+.color-swatch {
+ height: 44px;
+ border-radius: 12px;
+ border: 2px solid var(--border);
+ cursor: pointer;
+ box-shadow: inset 0 0 0 2px rgba(255,255,255,0.55);
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+.color-swatch:hover { transform: translateY(-1px) scale(1.02); box-shadow: inset 0 0 0 2px rgba(255,255,255,0.8); }
+.color-swatch:active { transform: scale(0.98); }
+.color-swatch.custom {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ color: var(--text);
+ background: linear-gradient(45deg, #f4ead8, #d6cabc);
+ box-shadow: inset 0 0 0 2px rgba(0,0,0,0.08);
+}
+
+.save-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(44, 35, 25, 0.55);
+ display: none;
+ align-items: center;
+ justify-content: center;
+ z-index: 210;
+ padding: 20px;
+ backdrop-filter: blur(2px);
+}
+.save-overlay.active { display: flex; }
+.save-modal {
+ background: var(--card-bg);
+ color: var(--text);
+ border-radius: 16px;
+ padding: 18px 18px 14px;
+ width: min(420px, 90vw);
+ box-shadow: 0 10px 30px rgba(0,0,0,0.25);
+ border: 1px solid var(--border);
+}
+.save-subtext { margin: 0 0 8px; color: var(--text-muted); }
+.save-list { display: grid; gap: 8px; max-height: 220px; overflow-y: auto; margin-bottom: 10px; }
+.save-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 10px;
+ border-radius: 10px;
+ background: var(--input-bg);
+ border: 1px solid var(--border);
+}
+.save-item input { width: 18px; height: 18px; }
+.save-actions { display: flex; justify-content: flex-end; gap: 8px; }
+.icon-woodland { width: 22px; height: 22px; }
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 1.5rem 1.25rem calc(120px + env(safe-area-inset-bottom, 0px));
+ position: relative;
+ z-index: 1; /* Keep cards above background */
+ flex: 1;
+}
+
+.projects-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+ gap: 20px;
+ align-items: start; /* Prevent shorter cards from stretching to tallest row mate */
+}
+
+@media (min-width: 1280px) {
+ .projects-grid {
+ grid-template-columns: repeat(3, minmax(320px, 1fr));
+ }
+}
+
+/* --- PROJECT CONTAINER --- */
+.project-container {
+ background: var(--card-bg);
+ border-radius: 18px;
+ padding: 5px 15px 15px 15px;
+ margin-bottom: 2rem;
+ box-shadow: var(--shadow);
+ transition: background-color 0.3s;
+ border: 1px solid var(--border);
+ z-index: 10;
+}
+
+.project-header {
+ display: flex; justify-content: space-between; align-items: center;
+ padding: 15px 5px; margin-bottom: 10px; gap: 10px;
+}
+
+.project-title-group { display: flex; align-items: center; gap: 10px; }
+
+.project-title {
+ font-size: 1.4rem; font-weight: 800; color: var(--project-color);
+ text-transform: uppercase; letter-spacing: 1px;
+ font-family: 'Playfair Display', Georgia, serif;
+}
+
+.btn-toggle-project {
+ background: none; border: none; color: var(--project-color);
+ font-size: 1.2rem; cursor: pointer; transition: transform 0.2s; padding: 5px;
+}
+.part-list { max-height: 2000px; opacity: 1; transition: max-height 0.35s ease, opacity 0.3s ease; overflow: hidden; }
+.project-collapsed .btn-toggle-project { transform: rotate(-90deg); }
+.project-collapsed .part-list { max-height: 0; opacity: 0; pointer-events: none; }
+.project-collapsed { margin-bottom: 1rem; opacity: 0.8; box-shadow: none; }
+
+.project-actions { display: flex; gap: 8px; align-items: center; }
+
+.btn-add-part {
+ background: var(--project-color); color: var(--card-bg); border: none;
+ border-radius: 20px; padding: 6px 16px; font-size: 0.9rem; font-weight: bold;
+ 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;
+}
+.btn-delete-project:hover { color: var(--danger); }
+
+.btn-rename-project {
+ background: none; border: none; color: var(--text-muted);
+ padding: 5px 8px; border-radius: 8px; font-size: 1rem; cursor: pointer;
+}
+.btn-rename-project:hover { color: var(--project-color); }
+.icon-pencil { display: inline-block; transform: scaleX(-1); }
+
+.btn-color {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ border: 2px solid var(--border);
+ background: var(--project-color);
+ cursor: pointer;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: inset 0 0 0 2px var(--card-bg);
+}
+.btn-color:hover { box-shadow: inset 0 0 0 2px var(--project-color); }
+
+/* --- PART CARD --- */
+.part-card {
+ background: var(--card-bg);
+ border-radius: 14px;
+ padding: 1rem;
+ margin-bottom: 0.8rem;
+ box-shadow: var(--shadow);
+ position: relative;
+ transition: background-color 0.3s;
+ overflow: hidden;
+ border-left: 7px solid var(--project-color);
+ border-top: 1px solid var(--border);
+ border-right: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+}
+
+.part-card.is-locked { background-color: var(--bg); opacity: 0.9; }
+
+.part-card.is-finished {
+ background-color: var(--bg-finished);
+ border-left-color: var(--success);
+ opacity: 0.9;
+}
+.finish-shimmer { animation: finishShimmer 0.8s ease; }
+@keyframes finishShimmer {
+ 0% { box-shadow: var(--shadow), 0 0 0 0 rgba(122, 139, 79, 0.32); }
+ 60% { box-shadow: var(--shadow), 0 0 0 16px rgba(122, 139, 79, 0); }
+ 100% { box-shadow: var(--shadow), 0 0 0 0 rgba(122, 139, 79, 0); }
+}
+
+.part-card.is-minimized { padding: 0.8rem 1rem; }
+.part-card.is-minimized .count-display,
+.part-card.is-minimized .controls { display: none; }
+.part-card.is-minimized .part-mini-count { display: inline-block; }
+.part-card.is-minimized .btn-toggle-part { transform: rotate(-90deg); }
+.part-card.is-minimized .count-subtext,
+.part-card.is-minimized .btn-reset-part,
+.part-card.is-minimized .btn-delete-part { display: none; }
+
+.part-header { display: flex; justify-content: space-between; align-items: center; }
+.part-card:not(.is-minimized) .part-header { margin-bottom: 15px; }
+
+.part-name-group { display: flex; align-items: center; gap: 12px; flex-grow: 1; }
+
+.check-container { position: relative; cursor: pointer; width: 26px; height: 26px; flex-shrink: 0; }
+.check-container input { opacity: 0; cursor: pointer; height: 0; width: 0; }
+.checkmark {
+ position: absolute; top: 0; left: 0; height: 26px; width: 26px;
+ background-color: var(--card-bg); border-radius: 50%; border: 2px solid var(--text-muted); transition: all 0.2s;
+}
+.check-container input:checked ~ .checkmark { background-color: var(--success); border-color: var(--success); }
+.checkmark:after { content: ""; position: absolute; display: none; }
+.check-container input:checked ~ .checkmark:after { display: block; }
+.check-container .checkmark:after {
+ left: 9px; top: 5px; width: 6px; height: 12px;
+ border: solid var(--card-bg); border-width: 0 2px 2px 0; transform: rotate(45deg);
+}
+
+.part-name {
+ font-size: 1.1rem; font-weight: 700; color: var(--text);
+ border-bottom: 2px dashed var(--project-color); /* Dashed looks more like stitching */
+ cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 160px;
+}
+
+.part-mini-count {
+ display: none; font-weight: 800; color: var(--project-color); margin-left: 8px; font-size: 1.2rem;
+}
+
+.is-finished .part-name { text-decoration: line-through; color: var(--success); border-bottom: none; }
+.is-finished .part-mini-count { color: var(--success); }
+
+.part-actions { display: flex; gap: 8px; align-items: center; }
+.part-actions { justify-content: flex-end; min-width: 120px; }
+.icon-btn {
+ background: none; border: none; font-size: 1.3rem; padding: 5px; color: var(--text-muted);
+ cursor: pointer; transition: color 0.2s;
+ display: inline-flex; align-items: center; justify-content: center;
+ width: 32px; height: 32px;
+}
+.icon-btn:disabled { opacity: 0.4; pointer-events: none; }
+.btn-delete-part:hover { color: var(--danger); }
+.btn-toggle-part { transition: transform 0.2s; }
+
+.count-display {
+ font-size: 3.5rem; font-weight: 800; text-align: center; color: var(--project-color); margin: 0.5rem 0 0.4rem 0; touch-action: manipulation;
+ text-shadow: 1px 1px 0px var(--card-bg);
+}
+.is-locked .count-display { color: var(--lock-btn-text); pointer-events: none; }
+.is-finished .count-display { color: var(--success); pointer-events: none; }
+
+.count-subtext {
+ text-align: center;
+ font-size: 0.9rem;
+ color: var(--text-muted);
+ margin: 0 0 0.6rem 0;
+}
+.count-subtext strong { color: var(--project-color); }
+.count-bump-up { animation: countUp 0.26s ease; }
+.count-bump-down { animation: countDown 0.26s ease; }
+@keyframes countUp {
+ 0% { transform: scale(0.95); color: var(--btn-secondary-text); }
+ 60% { transform: scale(1.08); color: var(--project-color); }
+ 100% { transform: scale(1); color: var(--project-color); }
+}
+@keyframes countDown {
+ 0% { transform: scale(1.05); color: var(--project-color); }
+ 60% { transform: scale(0.9); color: var(--text-muted); }
+ 100% { transform: scale(1); color: var(--project-color); }
+}
+
+.note-toggle {
+ background: none; border: 1px dashed var(--border); color: var(--text-muted);
+ padding: 4px 10px; border-radius: 10px; cursor: pointer; font-size: 0.9rem;
+ margin: 12px auto 6px;
+ display: block;
+}
+.note-toggle:hover { color: var(--project-color); border-color: var(--project-color); }
+
+.note-area {
+ display: none;
+ margin-top: 10px;
+}
+.note-area.show { display: block; }
+.note-area textarea {
+ width: 100%;
+ min-height: 90px;
+ border-radius: 10px;
+ border: 1px solid var(--border);
+ background: var(--input-bg);
+ color: var(--text);
+ padding: 10px;
+ font-size: 0.95rem;
+ resize: vertical;
+}
+.controls { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; padding: 0 5px; }
+
+button.action-btn {
+ border: none; border-radius: 12px; padding: 12px 0; font-size: 1.5rem;
+ cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.1s;
+ box-shadow: 0 3px 6px rgba(0,0,0,0.05);
+}
+
+.btn-minus { background-color: var(--btn-secondary-bg); color: var(--btn-secondary-text); border: 1px solid var(--border); }
+.btn-plus { background-color: var(--project-color); color: var(--card-bg); }
+.btn-lock { background-color: var(--lock-btn-bg); color: var(--lock-btn-text); font-size: 1.1rem; }
+.btn-lock.locked-active { background-color: var(--border); color: var(--text-muted); border: 2px solid var(--text-muted); box-shadow: none;}
+
+.hidden-controls { visibility: hidden; pointer-events: none; }
+.dimmed { opacity: 0.4; pointer-events: none; }
+button:active { transform: scale(0.97); box-shadow: none; }
+
+/* Floating Add Project Button */
+.fab {
+ position: fixed; bottom: 30px; right: 30px;
+ background-color: var(--text); color: var(--bg); /* Adapts to mode */
+ width: 65px; height: 65px; border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 2.2rem; box-shadow: 0 6px 15px rgba(74, 59, 42, 0.3);
+ border: none; z-index: 3; transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+.fab:hover { transform: translateY(-3px) scale(1.04) rotate(-2deg); box-shadow: 0 10px 24px rgba(0,0,0,0.18); }
+.fab:active { transform: translateY(0) scale(0.94); box-shadow: 0 3px 8px rgba(0,0,0,0.2); }
+.fab { z-index: 2; }
+
+/* --- MODAL --- */
+.modal-overlay {
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
+ background-color: rgba(44, 35, 25, 0.6); z-index: 200; align-items: center; justify-content: center; backdrop-filter: blur(2px);
+ display: flex; opacity: 0; visibility: hidden; pointer-events: none;
+ transition: opacity 0.3s ease, visibility 0.3s ease;
+}
+.modal-overlay.active { opacity: 1; visibility: visible; pointer-events: auto; }
+.modal-content {
+ background: var(--modal-bg); color: var(--text);
+ padding: 25px; border-radius: 20px; width: 85%; max-width: 400px;
+ animation: none; box-shadow: var(--shadow); border: 1px solid var(--border);
+ transform-origin: center;
+}
+.modal-overlay.active .modal-content { animation: modalIn 0.32s ease; }
+@keyframes modalIn {
+ 0% { transform: translateY(10px) scale(0.96); opacity: 0; }
+ 60% { transform: translateY(-4px) scale(1.02); opacity: 1; }
+ 100% { transform: translateY(0) scale(1); opacity: 1; }
+}
+.modal-title { margin: 0 0 15px 0; font-size: 1.3rem; color: var(--text); font-weight: 800; }
+.modal-input {
+ width: 100%; padding: 15px; font-size: 1.2rem;
+ background: var(--input-bg); color: var(--text);
+ border: 2px solid var(--input-border); border-radius: 12px; margin-bottom: 25px; outline: none; transition: border-color 0.2s;
+}
+.modal-input:focus { border-color: var(--project-color); }
+.modal-actions { display: flex; justify-content: flex-end; gap: 10px; }
+.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;}
+
+.footer-bg {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: -180px;
+ height: clamp(760px, 1200px, 1200px);
+ background: url('textures/mushroom.svg') no-repeat center bottom;
+ background-size: cover;
+ opacity: 1;
+ width: 125vw;
+ pointer-events: none;
+ z-index: 0;
+}
+
+.firefly-wrap {
+ position: fixed;
+ width: 28px;
+ height: 28px;
+ animation: fireflyGlide var(--fly-duration, 14s) linear forwards;
+ z-index: 0; /* Drift beneath page content */
+ pointer-events: none;
+}
+.firefly {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(255,245,200,0.9) 0%, rgba(255,245,200,0.4) 45%, rgba(255,245,200,0.1) 70%, rgba(255,245,200,0) 85%);
+ opacity: 0.9;
+ mix-blend-mode: screen;
+ filter: blur(1px) drop-shadow(0 0 14px rgba(255, 245, 200, 0.8)) brightness(1.15);
+ animation: fireflyFlutter 1.6s ease-in-out infinite alternate, fireflyFlicker 1.1s ease-in-out infinite alternate;
+}
+
+.seed-wrap {
+ position: fixed;
+ width: 36px;
+ height: 36px;
+ animation: seedGlide var(--seed-duration, 16s) linear forwards;
+ z-index: 0;
+ pointer-events: none;
+}
+.seed {
+ width: 200px;
+ height: 200px;
+ background: url('textures/seed.svg') no-repeat center/contain;
+ opacity: var(--seed-opacity, 0.07);
+ mix-blend-mode: screen;
+ filter: drop-shadow(0 4px 10px rgba(200, 190, 160, 0.55));
+ animation: seedDrift 2.6s ease-in-out infinite alternate,
+ seedFlicker 1.4s ease-in-out infinite alternate,
+ seedFlip var(--seed-flip-duration, 6s) ease-in-out infinite;
+}
+@keyframes fireflyGlide {
+ 0% { transform: translate3d(var(--fly-start-x, -10vw), var(--fly-start-y, 0), 0) scale(var(--fly-scale, 1)); opacity: 0; }
+ 12% { opacity: 0.55; }
+ 35% { transform: translate3d(var(--fly-mid-x, 25vw), var(--fly-mid-y, -6px), 0) scale(var(--fly-scale, 1)); opacity: 0.65; }
+ 65% { transform: translate3d(var(--fly-mid2-x, 65vw), var(--fly-mid2-y, 6px), 0) scale(var(--fly-scale, 1)); opacity: 0.55; }
+ 90% { opacity: 0.35; }
+ 100% { transform: translate3d(var(--fly-end-x, 110vw), var(--fly-end-y, 0), 0) scale(var(--fly-scale, 1)); opacity: 0; }
+}
+@keyframes fireflyFlutter {
+ 0% { transform: translateY(-2px) translateX(-3px) scale(0.95); }
+ 50% { transform: translateY(3px) translateX(2px) scale(1.05); }
+ 100% { transform: translateY(-2px) translateX(1px) scale(0.97); }
+}
+@keyframes fireflyFlicker {
+ 0% { opacity: 0.65; filter: blur(1px) drop-shadow(0 0 10px rgba(255,245,200,0.65)); }
+ 50% { opacity: 1; filter: blur(0.5px) drop-shadow(0 0 16px rgba(255,245,200,1)); }
+ 100% { opacity: 0.75; filter: blur(1px) drop-shadow(0 0 12px rgba(255,245,200,0.75)); }
+}
+
+@keyframes seedGlide {
+ 0% { transform: translate3d(var(--seed-start, -12vw), 0, 0) scale(var(--seed-scale, 1)); opacity: 0; }
+ 10% { opacity: 0.35; }
+ 40% { transform: translate3d(calc(var(--seed-mid, 30vw)), -10px, 0) scale(var(--seed-scale, 1)); opacity: 0.5; }
+ 70% { transform: translate3d(calc(var(--seed-mid, 30vw) * 2), 8px, 0) scale(var(--seed-scale, 1)); opacity: 0.4; }
+ 100% { transform: translate3d(var(--seed-end, 110vw), -6px, 0) scale(var(--seed-scale, 1)); opacity: 0; }
+}
+@keyframes seedDrift {
+ 0% { transform: translateY(-4px) translateX(calc(var(--seed-sway, 6px) * -1)) rotate(calc(var(--seed-tilt, 10deg) * -0.6)) scale(0.94); opacity: 0.65; }
+ 50% { transform: translateY(5px) translateX(calc(var(--seed-sway, 6px) * 0.8)) rotate(calc(var(--seed-tilt, 10deg))) scale(1.05); opacity: 0.9; }
+ 100% { transform: translateY(-3px) translateX(calc(var(--seed-sway, 6px) * 0.4)) rotate(calc(var(--seed-tilt, 10deg) * 0.2)) scale(0.97); opacity: 0.7; }
+}
+@keyframes seedFlicker {
+ 0% { opacity: 0.65; filter: drop-shadow(0 2px 4px rgba(200,190,160,0.35)); }
+ 50% { opacity: 0.9; filter: drop-shadow(0 4px 8px rgba(200,190,160,0.5)); }
+ 100% { opacity: 0.7; filter: drop-shadow(0 3px 6px rgba(200,190,160,0.4)); }
+}
+@keyframes seedFlip {
+ 0% { transform: rotate(0deg) scale(1); }
+ 20% { transform: rotate(8deg) scale(1.02); }
+ 40% { transform: rotate(-10deg) scale(0.98); }
+ 60% { transform: rotate(14deg) scale(1.03); }
+ 80% { transform: rotate(-6deg) scale(0.99); }
+ 100% { transform: rotate(0deg) scale(1); }
+}
+
+@media (max-width: 768px) {
+ .footer-bg {
+ bottom: -40px;
+ height: clamp(260px, 24vh, 450px);
+ }
+}
+
+@media (max-width: 1024px) and (min-width: 769px) {
+ .container {
+ padding: 1.5rem 1.25rem calc(140px + env(safe-area-inset-bottom, 0px));
+ }
+ .footer-bg {
+ height: clamp(300px, 28vh, 480px);
+ bottom: -180px;
+ }
+}
+
+
+/* Sweet-ish alerts */
+.swal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(44, 35, 25, 0.55);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 300;
+ padding: 20px;
+ animation: overlayFade 0.22s ease;
+}
+
+.swal-dialog {
+ background: var(--card-bg);
+ color: var(--text);
+ border-radius: 16px;
+ padding: 22px 20px 18px;
+ width: min(420px, 90vw);
+ box-shadow: 0 10px 30px rgba(0,0,0,0.25);
+ border: 1px solid var(--border);
+ text-align: center;
+ position: relative;
+ animation: dialogPop 0.28s ease;
+}
+
+.swal-title {
+ font-family: 'Playfair Display', Georgia, serif;
+ font-size: 1.4rem;
+ margin: 0 0 8px;
+}
+
+.swal-text { margin: 0 0 16px; color: var(--text-muted); }
+
+.swal-actions { display: flex; justify-content: center; gap: 10px; }
+
+.swal-btn {
+ border: none;
+ border-radius: 12px;
+ padding: 12px 18px;
+ font-weight: 700;
+ cursor: pointer;
+ min-width: 110px;
+ transition: transform 0.08s;
+}
+.swal-btn:active { transform: scale(0.97); }
+.swal-confirm { background: var(--project-color); color: var(--card-bg); }
+.swal-cancel { background: var(--lock-btn-bg); color: var(--text); }
+.swal-danger { background: var(--danger); color: var(--card-bg); }
+@keyframes overlayFade { from { opacity: 0; } to { opacity: 1; } }
+@keyframes dialogPop {
+ 0% { transform: translateY(8px) scale(0.96); opacity: 0; }
+ 60% { transform: translateY(-3px) scale(1.02); opacity: 1; }
+ 100% { transform: translateY(0) scale(1); opacity: 1; }
+}
diff --git a/index.html b/index.html
index 675f056..944908c 100644
--- a/index.html
+++ b/index.html
@@ -6,7 +6,7 @@
Toadstool Cottage Counter
-
+
@@ -35,6 +35,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Row 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Steps
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+