Skip to content
SEO clean, structured learning
AD-FREE focused lessons
PROJECTS finish what you start
Knitting courses that help you learn with confidence .
Minimalist, structured, and practical knitting education—built for beginners who want a clear path, and for experienced knitters who want to master techniques like cables, lace, and brioche.
Clarity-first curriculum
Each course is a clean sequence: skill → pattern → project → feedback loop.
Technique-to-project mapping
Learn purl/ribbing, then immediately use them in wearable, gift-ready pieces.
Time-smart lessons
Short modules, clear checklists, and timers for practice sessions.
Next cohort starts in
—
Daily practice timer
00:00
Start
Quick start
Answer 3 questions and get a personalized learning path. You can apply it to the catalog in one click.
Your level
Beginner
Intermediate
Advanced
Primary goal
Finish my first project
Build strong fundamentals
Improve texture & fit
Learn complex techniques
Minutes/day
Project preference
Accessories (hat, scarf)
Socks
Sweater
Lace pieces
We’ll never send spam. If you choose to message us, we’ll reply within one business day.
Beginner foundations
Cast-on → knit/purl → ribbing → clean edges.
Texture & structure
Cables, twisted stitches, shaping basics.
Lightweight finesse
Lace charts, lifelines, blocking mastery.
Please enter a valid daily time (5–180 minutes).
Filters updated. You can open the Path Builder or go to the catalog.
Printable lesson checklists and finishing guides (seams, blocking, gauge) so your projects look professional.
See what’s inside
Use built-in timers for stitch drills, swatching, and breaks. Learn faster without burning out.
How it works
No clutter, no distractions
A minimalist learning environment: clear navigation, clean typography, and consistent course structure.
A learning path that matches your pace
Choose what you want to knit, how much time you have, and your current level. The Path Builder will recommend a realistic sequence of skills and projects—and pass the filters to the catalog so you can act immediately.
Skills
Fundamentals, tension, ribbing, shaping, charts, finishing.
Projects
Accessories → socks → sweaters → advanced textures & lace.
Pacing
Daily minutes become a plan with weekly milestones.
Technique focus
Pick what matters: fit, texture, speed, or complexity.
Today’s micro-challenge
A tiny drill you can complete in under 10 minutes to improve consistency.
Need help choosing yarn?
Get a quick, practical answer. We’ll guide you by project and technique.
Contact
+1 (415) 805-2749
Open form
Start with a path, not a guess
Use the Path Builder to get a clear next step. Then jump straight to matching courses in the catalog—filters included.
Cookies & local storage
We use essential storage to remember your theme and Path Builder preferences. Optional analytics are off by default.
Path Builder
Tell us your level, goal, and time. We’ll generate a realistic 4-week sequence and pass matching filters to the catalog.
Level foundation
Beginner
Intermediate
Advanced
Goal finish
Finish my first project
Build strong fundamentals
Improve texture & fit
Learn complex techniques
Project preference accessories
Accessories (hat, scarf)
Socks
Sweater
Lace pieces
Minutes/day 15
Fundamentals
Ribbing
Shaping
Cables
Lace
Brioche
Finishing
Speed
Enter valid minutes/day (5–180). Then generate.
Path updated. You can apply it to the catalog.
Your 4-week path
Generate to see weekly milestones and recommended course filters.
level: beginner
15 min/day
Week 1 —
Week 2 —
Week 3 —
Week 4 —
Send it to yourself
Optional: email a summary of your generated path. Stored locally only; no external requests.
Ask a question
Tell us what you’re making and where you’re stuck. We’ll reply with a practical next step. Phone: +1 (415) 805-2749
What we can help with
A quick checklist so your message gets a precise answer.
Project details Pattern link/name, yarn weight, needle size.
Where it goes wrong Row count, technique, photo description.
Your goal Speed, fit, drape, or finishing quality.
Your level Beginner / intermediate / advanced.
Micro-challenge
Use the timer if you want. This is designed to be small and repeatable.
`;
const footerFallback = `
Knitting Courses
Minimal, readable, and SEO-conscious learning. sleekhub.click
Performance note
Measuring…
JS start → now
—
Quick check: fast, image-free pages for maximum clarity.
Cookie settings
Toggle theme
Privacy
Policy
Terms
Agreement
FAQ
Help
Contact
Support
Helpful
Use the cookie dialog to manage consent quickly.
Theme toggles persist across pages with localStorage.
Fast-check runs without network requests.
Contact
Hours
Mon–Fri: 09:00–18:00 (PT)
Response time: within 1 business day
|
Cookie consent
No images in footer for maximum clarity.
Cookies
Control essential preferences and reduce unnecessary tracking.
×
We use essential cookies to keep you signed in and remember preferences. You can manage cookies anytime in your browser. Your choice is stored locally on this device.
Essential
Required for theme and basic preferences.
Always on
Decline
Accept
Tip: Press Esc to close.
Fast-check details
Local snapshot from your browser; no network calls.
×
Uses performance.timing fallback where available and measures DOMContentLoaded deltas when supported.
This is a simple heuristic: fewer images + fewer scripts + smaller DOM usually means faster, clearer pages.
Close
`;
async function tryFetch(url){
try{
const res = await fetch(url, {cache:'no-store'});
if(!res.ok) return null;
const txt = await res.text();
if(!txt || !txt.trim()) return null;
return txt;
}catch(e){ return null; }
}
const [headerHTML, footerHTML] = await Promise.all([
tryFetch('./header.html'),
tryFetch('./footer.html')
]);
if(headerMount){
headerMount.innerHTML = headerHTML || headerFallback;
const injectedHeader = headerMount.querySelector('header');
if(!injectedHeader){
headerMount.innerHTML = headerFallback;
}
}
if(footerMount){
footerMount.innerHTML = footerHTML || footerFallback;
const injectedFooter = footerMount.querySelector('footer');
if(!injectedFooter){
footerMount.innerHTML = footerFallback;
}
}
}
function cookie(){
const key = 'kc_cookie_consent_v1';
function get(){ try{ return localStorage.getItem(key); }catch(e){ return null; } }
function set(v){ try{ localStorage.setItem(key, v); }catch(e){} }
function init(){
const banner = $('#cookieBanner');
if(!banner) return;
const existing = get();
if(existing !== 'accept' && existing !== 'reject'){
setAriaHidden(banner, false);
}
$('#cookieAccept')?.addEventListener('click', ()=>{ set('accept'); setAriaHidden(banner, true); });
$('#cookieReject')?.addEventListener('click', ()=>{ set('reject'); setAriaHidden(banner, true); });
$('#cookieClose')?.addEventListener('click', ()=>{ set('reject'); setAriaHidden(banner, true); });
}
return {init, get, set};
}
function dialogKit(dialog){
if(!dialog) return {open:()=>{}, close:()=>{}};
function open(){
if(typeof dialog.showModal === 'function'){
if(!dialog.open) dialog.showModal();
}else{
dialog.setAttribute('open','');
}
trapFocus(dialog);
}
function close(){
if(typeof dialog.close === 'function' && dialog.open) dialog.close();
dialog.removeAttribute('open');
releaseTrap(dialog);
}
dialog.addEventListener('click', (e)=>{
const rect = dialog.getBoundingClientRect();
const isInDialog = (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom);
if(!isInDialog){
close();
}
});
dialog.addEventListener('cancel', (e)=>{ e.preventDefault(); close(); });
return {open, close};
}
const focusTraps = new Map();
function trapFocus(dialog){
const focusableSelector = 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';
const focusables = () => $$(focusableSelector, dialog).filter(el=>!el.hasAttribute('disabled') && el.getAttribute('aria-hidden')!=='true');
const first = () => focusables()[0];
const last = () => { const f = focusables(); return f[f.length-1]; };
const onKeyDown = (e)=>{
if(e.key === 'Escape') return;
if(e.key !== 'Tab') return;
const f = focusables();
if(!f.length) return;
const active = document.activeElement;
if(e.shiftKey){
if(active === f[0] || active === dialog){
e.preventDefault();
f[f.length-1].focus();
}
}else{
if(active === f[f.length-1]){
e.preventDefault();
f[0].focus();
}
}
};
if(!focusTraps.has(dialog)){
dialog.addEventListener('keydown', onKeyDown);
focusTraps.set(dialog, onKeyDown);
}
setTimeout(()=>{ (first() || dialog).focus?.(); }, 0);
}
function releaseTrap(dialog){
const fn = focusTraps.get(dialog);
if(fn){
dialog.removeEventListener('keydown', fn);
focusTraps.delete(dialog);
}
}
function cohortCountdown(){
const el = $('#cohortCountdown');
if(!el) return;
const now = new Date();
const start = new Date(now);
start.setDate(start.getDate() + 9);
start.setHours(10, 0, 0, 0);
function tick(){
const n = new Date();
let diff = start.getTime() - n.getTime();
if(diff < 0) diff = 0;
const d = Math.floor(diff / (1000*60*60*24));
const h = Math.floor((diff / (1000*60*60)) % 24);
const m = Math.floor((diff / (1000*60)) % 60);
el.textContent = d + 'd ' + pad2(h) + 'h ' + pad2(m) + 'm';
}
tick();
setInterval(tick, 30_000);
}
function stopwatch(bind){
let running = false;
let t0 = 0;
let acc = 0;
let raf = 0;
function render(){
const ms = (running ? (performance.now()-t0) : 0) + acc;
const totalSec = Math.floor(ms/1000);
const mm = Math.floor(totalSec/60);
const ss = totalSec%60;
bind.onRender?.(pad2(mm)+':'+pad2(ss));
if(running) raf = requestAnimationFrame(render);
}
function start(){
if(running) return;
running = true;
t0 = performance.now();
bind.onRunningChange?.(true);
raf = requestAnimationFrame(render);
}
function stop(){
if(!running) return;
running = false;
acc += performance.now()-t0;
bind.onRunningChange?.(false);
cancelAnimationFrame(raf);
render();
}
function reset(){
running = false;
acc = 0;
t0 = 0;
bind.onRunningChange?.(false);
cancelAnimationFrame(raf);
bind.onRender?.('00:00');
}
function toggle(){ running ? stop() : start(); }
reset();
return {start, stop, reset, toggle, get running(){ return running; }};
}
function validateMinutes(v){
const n = Number(String(v).trim());
if(!Number.isFinite(n)) return null;
const i = Math.round(n);
if(i < 5 || i > 180) return null;
return i;
}
function setLocalJSON(key, obj){ try{ localStorage.setItem(key, JSON.stringify(obj)); }catch(e){} }
function getLocalJSON(key, fallback){ try{ return safeJSONParse(localStorage.getItem(key), fallback); }catch(e){ return fallback; } }
function buildCatalogURL(filters){
const params = new URLSearchParams();
Object.entries(filters||{}).forEach(([k,v])=>{
if(v == null) return;
if(Array.isArray(v)){
v.filter(Boolean).forEach(x=>params.append(k, x));
}else{
if(String(v).trim() !== '') params.set(k, String(v));
}
});
const qs = params.toString();
return './catalog.html' + (qs ? ('?' + qs) : '');
}
function pathBuilderEngine(input){
const level = input.level;
const goal = input.goal;
const project = input.project;
const minutes = input.minutes;
const focus = input.focus || [];
const intensity = minutes <= 10 ? 'micro' : minutes <= 20 ? 'steady' : minutes <= 35 ? 'strong' : 'deep';
const baseTech = [];
if(level === 'beginner'){
baseTech.push('cast-on & bind-off', 'knit/purl control', 'tension & edges', 'simple decreases/increases');
}else if(level === 'intermediate'){
baseTech.push('consistent gauge', 'reading patterns', 'shaping confidence', 'clean finishing');
}else{
baseTech.push('chart fluency', 'precision shaping', 'refined finishing', 'problem-solving');
}
const focusMap = {
fundamentals: ['tension drills', 'edges', 'counting rows'],
ribbing: ['ribbing setup', 'elastic bind-off', 'neat transitions'],
shaping: ['increases/decreases', 'short rows', 'try-on methods'],
cables: ['cable without needle', 'cable charts', 'fixing crossings'],
lace: ['lifelines', 'chart reading', 'blocking lace'],
brioche: ['brk/brp', 'fixing dropped brioche', 'two-color basics'],
finishing: ['blocking', 'seaming', 'weaving ends'],
speed: ['efficient movement', 'batching steps', 'error recovery']
};
const focusTopics = focus.flatMap(f=>focusMap[f]||[]);
const techniqueFocus = Array.from(new Set([...baseTech, ...focusTopics])).slice(0, 8);
const projectTrack = project === 'accessories'
? ['scarf sample', 'hat basics', 'finishing polish']
: project === 'socks'
? ['heel concepts', 'gusset shaping', 'toe & grafting']
: project === 'sweater'
? ['yoke/raglan shaping', 'fit checkpoints', 'neckline finishing']
: ['lace swatch', 'chart-based panel', 'blocking mastery'];
const goalHint = goal === 'first_project'
? 'fast-to-finish projects with minimal new techniques'
: goal === 'skills_building'
? 'foundations + repeatable drills'
: goal === 'texture_and_fit'
? 'fit checkpoints + texture control'
: 'advanced techniques + chart fluency';
const pace = intensity === 'micro' ? '2–3 short sessions/week' : intensity === 'steady' ? '4–5 sessions/week' : intensity === 'strong' ? '5–6 sessions/week' : 'daily sessions';
const weeklyMinutes = minutes * (intensity === 'micro' ? 3 : intensity === 'steady' ? 5 : intensity === 'strong' ? 6 : 7);
function weekPlan(week){
const w = week;
const t = techniqueFocus;
const pt = projectTrack;
const weekBlocks = [];
if(w === 1){
weekBlocks.push(`Stitch control: ${t[0] || 'tension'} + ${t[1] || 'edges'}.`);
weekBlocks.push(`Mini-project: start a ${pt[0]} to validate gauge and comfort.`);
weekBlocks.push(`Checkpoint: count rows, measure, adjust needle size if needed.`);
}else if(w === 2){
weekBlocks.push(`Build: ${t[2] || 'pattern reading'} + ${t[3] || 'shaping basics'}.`);
weekBlocks.push(`Project step: progress your ${pt[1] || 'main piece'} with consistent rhythm.`);
weekBlocks.push(`Fixing: practice ladder-down repair on a swatch for 5 minutes.`);
}else if(w === 3){
const adv = (focus.includes('cables') || focus.includes('lace') || focus.includes('brioche') || goal === 'complex_techniques');
weekBlocks.push(adv ? `Technique focus: ${t[4] || 'charts'} + ${t[5] || 'precision steps'} (short drills).` : `Consistency: repeat Week 2 skills with cleaner edges and fewer mistakes.`);
weekBlocks.push(`Project step: add a structured element for the ${pt[2] || 'final'} (e.g., shaping/texture).`);
weekBlocks.push(`Checkpoint: block a small swatch; record your best gauge.`);
}else{
weekBlocks.push(`Finishing: ${t[6] || 'weaving ends'} + ${t[7] || 'blocking'} with a clear checklist.`);
weekBlocks.push(`Project finish: complete + review fit/drape; note what you’d do differently.`);
weekBlocks.push(`Next step: choose 1 technique to deepen next month.`);
}
return weekBlocks.join(' ');
}
const weeks = [1,2,3,4].map(weekPlan);
const filterLevel = level;
const filterProject = project;
let filterDifficulty = level === 'beginner' ? 'intro' : level === 'intermediate' ? 'core' : 'pro';
if(goal === 'first_project' && level !== 'beginner') filterDifficulty = 'core';
if(goal === 'complex_techniques' && level === 'beginner') filterDifficulty = 'core';
const focusNormalized = focus.length ? focus : (level === 'beginner' ? ['fundamentals','ribbing'] : level === 'intermediate' ? ['shaping','finishing'] : ['cables','lace']);
const payload = {
level: filterLevel,
project: filterProject,
difficulty: filterDifficulty,
focus: focusNormalized,
pace: intensity,
minutes: minutes
};
const subtitle = `${pace} · ~${weeklyMinutes} min/week · ${goalHint}`;
const title = level === 'beginner'
? 'Your 4-week foundation path'
: level === 'intermediate'
? 'Your 4-week skill-builder path'
: 'Your 4-week advanced refinement path';
return {title, subtitle, weeks, payload};
}
function initQuickStart(){
const qsLevel = $('#qsLevel');
const qsGoal = $('#qsGoal');
const qsTime = $('#qsTime');
const qsProject = $('#qsProject');
const err = $('#qsError');
const ok = $('#qsOk');
function showErr(msg){
if(err) err.textContent = msg || 'Please enter a valid daily time (5–180 minutes).';
setAriaHidden(err, false);
setAriaHidden(ok, true);
}
function showOk(){
setAriaHidden(err, true);
setAriaHidden(ok, false);
setTimeout(()=>setAriaHidden(ok, true), 2200);
}
function getQS(){
const minutes = validateMinutes(qsTime?.value ?? '');
return {
level: qsLevel?.value || 'beginner',
goal: qsGoal?.value || 'first_project',
project: qsProject?.value || 'accessories',
minutes
};
}
function saveQS(){
const d = getQS();
if(d.minutes == null){
showErr('Please enter a valid daily time (5–180 minutes).');
return null;
}
const pref = getLocalJSON('kc_path_pref_v1', {});
pref.level = d.level;
pref.goal = d.goal;
pref.project = d.project;
pref.minutes = d.minutes;
if(!Array.isArray(pref.focus) || pref.focus.length===0){
pref.focus = d.level==='beginner' ? ['fundamentals','ribbing'] : d.level==='intermediate' ? ['shaping','finishing'] : ['cables','lace'];
}
setLocalJSON('kc_path_pref_v1', pref);
showOk();
return pref;
}
$('#applyQuickFilters')?.addEventListener('click', (e)=>{
const pref = saveQS();
if(!pref){ e.preventDefault(); return; }
const filters = {
level: pref.level,
project: pref.project,
minutes: pref.minutes,
focus: pref.focus
};
e.currentTarget.setAttribute('href', buildCatalogURL(filters));
});
[qsLevel, qsGoal, qsProject].forEach(el=>{
el?.addEventListener('change', ()=>{ saveQS(); });
});
qsTime?.addEventListener('input', ()=>{
setAriaHidden(err, true);
setAriaHidden(ok, true);
});
qsTime?.addEventListener('blur', ()=>{ if(validateMinutes(qsTime.value)==null) showErr(); else saveQS(); });
const existing = getLocalJSON('kc_path_pref_v1', null);
if(existing){
if(qsLevel) qsLevel.value = existing.level || qsLevel.value;
if(qsGoal) qsGoal.value = existing.goal || qsGoal.value;
if(qsProject) qsProject.value = existing.project || qsProject.value;
if(qsTime && existing.minutes) qsTime.value = String(existing.minutes);
}
}
function initPathBuilder(){
const dlg = $('#pathBuilderDialog');
const kit = dialogKit(dlg);
const pbLevel = $('#pbLevel');
const pbGoal = $('#pbGoal');
const pbProject = $('#pbProject');
const pbMinutes = $('#pbMinutes');
const pbError = $('#pbError');
const pbOk = $('#pbOk');
const pbTitle = $('#pbTitle');
const pbSubtitle = $('#pbSubtitle');
const pbWeeks = $('#pbWeeks');
const pbPayload = $('#pbPayload');
const pbTagLevel = $('#pbTagLevel');
const pbTagMinutes = $('#pbTagMinutes');
const hintLevel = $('#pbLevelHint');
const hintGoal = $('#pbGoalHint');
const hintProject = $('#pbProjectHint');
const hintMinutes = $('#pbMinutesHint');
const focusButtons = $$('.u0p9b', dlg);
function getFocus(){
return focusButtons.filter(b=>b.getAttribute('aria-pressed')==='true').map(b=>b.dataset.focus);
}
function setFocus(list){
const set = new Set(list||[]);
focusButtons.forEach(b=>{
const on = set.has(b.dataset.focus);
b.setAttribute('aria-pressed', on ? 'true' : 'false');
});
}
function loadPrefs(){
const pref = getLocalJSON('kc_path_pref_v1', {});
if(pbLevel && pref.level) pbLevel.value = pref.level;
if(pbGoal && pref.goal) pbGoal.value = pref.goal;
if(pbProject && pref.project) pbProject.value = pref.project;
if(pbMinutes && pref.minutes) pbMinutes.value = String(pref.minutes);
if(pref.focus && Array.isArray(pref.focus) && pref.focus.length) setFocus(pref.focus);
updateHints();
return pref;
}
function savePrefs(){
const minutes = validateMinutes(pbMinutes?.value ?? '');
const pref = getLocalJSON('kc_path_pref_v1', {});
pref.level = pbLevel?.value || 'beginner';
pref.goal = pbGoal?.value || 'first_project';
pref.project = pbProject?.value || 'accessories';
if(minutes != null) pref.minutes = minutes;
pref.focus = getFocus();
setLocalJSON('kc_path_pref_v1', pref);
return {pref, minutes};
}
function showPBErr(msg){
if(pbError) pbError.textContent = msg || 'Enter valid minutes/day (5–180). Then generate.';
setAriaHidden(pbError, false);
setAriaHidden(pbOk, true);
}
function showPBOk(){
setAriaHidden(pbError, true);
setAriaHidden(pbOk, false);
setTimeout(()=>setAriaHidden(pbOk, true), 2200);
}
function updateHints(){
if(hintLevel) hintLevel.textContent = (pbLevel?.value || 'beginner');
if(hintGoal) hintGoal.textContent = (pbGoal?.value || 'first_project').replaceAll('_',' ');
if(hintProject) hintProject.textContent = (pbProject?.value || 'accessories');
const m = validateMinutes(pbMinutes?.value ?? '');
if(hintMinutes) hintMinutes.textContent = m == null ? '5–180' : String(m);
if(pbTagLevel) pbTagLevel.textContent = 'level: ' + (pbLevel?.value || 'beginner');
if(pbTagMinutes) pbTagMinutes.textContent = (m == null ? '—' : (String(m) + ' min/day'));
}
function generate(){
const minutes = validateMinutes(pbMinutes?.value ?? '');
if(minutes == null){
showPBErr('Enter valid minutes/day (5–180).');
updateHints();
return null;
}
const focus = getFocus();
const input = {level: pbLevel?.value || 'beginner', goal: pbGoal?.value || 'first_project', project: pbProject?.value || 'accessories', minutes, focus};
const out = pathBuilderEngine(input);
if(pbTitle) pbTitle.textContent = out.title;
if(pbSubtitle) pbSubtitle.textContent = out.subtitle;
if(pbWeeks){
const items = $$('.u5y2v', pbWeeks);
const weeks = out.weeks;
items.forEach((card, idx)=>{
const span = card.querySelector('span');
if(span) span.textContent = weeks[idx] || '—';
});
}
const payloadText = JSON.stringify(out.payload);
if(pbPayload) pbPayload.textContent = payloadText;
const pref = getLocalJSON('kc_path_pref_v1', {});
pref.level = input.level; pref.goal = input.goal; pref.project = input.project; pref.minutes = minutes; pref.focus = focus;
pref.payload = out.payload;
pref.generatedAt = Date.now();
setLocalJSON('kc_path_pref_v1', pref);
const url = buildCatalogURL(out.payload);
$('#pbApplyToCatalog')?.setAttribute('href', url);
showPBOk();
updateHints();
return out;
}
function copyPayload(){
const text = pbPayload?.textContent || '';
if(!text || text === '—') return;
navigator.clipboard?.writeText(text).then(()=>{
if(pbOk){
pbOk.textContent = 'Copied.';
setAriaHidden(pbOk, false);
setTimeout(()=>{ pbOk.textContent = 'Path updated. You can apply it to the catalog.'; setAriaHidden(pbOk, true); }, 1200);
}
}).catch(()=>{});
}
function openFromQuickStart(){
const pref = getLocalJSON('kc_path_pref_v1', null);
if(pref){
if(pbLevel && pref.level) pbLevel.value = pref.level;
if(pbGoal && pref.goal) pbGoal.value = pref.goal;
if(pbProject && pref.project) pbProject.value = pref.project;
if(pbMinutes && pref.minutes) pbMinutes.value = String(pref.minutes);
if(pref.focus) setFocus(pref.focus);
updateHints();
if(pref.payload){
$('#pbApplyToCatalog')?.setAttribute('href', buildCatalogURL(pref.payload));
if(pbPayload) pbPayload.textContent = JSON.stringify(pref.payload);
}
}
kit.open();
}
$('#openPathBuilderTop')?.addEventListener('click', openFromQuickStart);
$('#openPathBuilderSide')?.addEventListener('click', openFromQuickStart);
$('#openPathBuilderBottom')?.addEventListener('click', openFromQuickStart);
$('#closePathBuilder')?.addEventListener('click', kit.close);
$('#pbGenerate')?.addEventListener('click', ()=>{ savePrefs(); generate(); });
$('#pbCopyPayload')?.addEventListener('click', copyPayload);
[pbLevel, pbGoal, pbProject].forEach(el=>el?.addEventListener('change', ()=>{
savePrefs(); updateHints();
}));
pbMinutes?.addEventListener('input', ()=>{
setAriaHidden(pbError, true); setAriaHidden(pbOk, true);
updateHints();
});
pbMinutes?.addEventListener('blur', ()=>{
savePrefs();
if(validateMinutes(pbMinutes.value)==null) showPBErr('Enter valid minutes/day (5–180).');
});
focusButtons.forEach(b=>{
b.addEventListener('click', ()=>{
const on = b.getAttribute('aria-pressed') === 'true';
b.setAttribute('aria-pressed', on ? 'false' : 'true');
const {pref} = savePrefs();
updateHints();
if(pref.payload){
$('#pbApplyToCatalog')?.setAttribute('href', buildCatalogURL(pref.payload));
}
});
});
loadPrefs();
const pref = getLocalJSON('kc_path_pref_v1', null);
if(pref && pref.payload){
if(pbPayload) pbPayload.textContent = JSON.stringify(pref.payload);
$('#pbApplyToCatalog')?.setAttribute('href', buildCatalogURL(pref.payload));
}else{
$('#pbApplyToCatalog')?.setAttribute('href', './catalog.html');
}
const emailForm = $('#pbEmailForm');
const pbEmail = $('#pbEmail');
const pbName = $('#pbName');
const pbEmailErr = $('#pbEmailErr');
const pbEmailOk = $('#pbEmailOk');
const emailKey = 'kc_path_email_saved_v1';
function isValidEmail(v){
const s = String(v||'').trim();
if(s.length < 6) return false;
return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(s);
}
emailForm?.addEventListener('submit', (e)=>{
e.preventDefault();
setAriaHidden(pbEmailErr, true);
setAriaHidden(pbEmailOk, true);
const email = String(pbEmail?.value||'').trim();
if(!isValidEmail(email)){
setAriaHidden(pbEmailErr, false);
return;
}
const pref = getLocalJSON('kc_path_pref_v1', {});
if(!pref.payload){
const maybe = generate();
if(!maybe){
setAriaHidden(pbEmailErr, false);
pbEmailErr.textContent = 'Generate a path first (valid minutes/day).';
return;
}
}
const saved = {
email,
name: String(pbName?.value||'').trim(),
savedAt: Date.now(),
title: pbTitle?.textContent || 'Your 4-week path',
subtitle: pbSubtitle?.textContent || '',
payload: getLocalJSON('kc_path_pref_v1', {}).payload || null,
weeks: $$('.u5y2v', pbWeeks).map(c=>({
week: c.querySelector('b')?.textContent || '',
text: c.querySelector('span')?.textContent || ''
}))
};
setLocalJSON(emailKey, saved);
setAriaHidden(pbEmailOk, false);
setTimeout(()=>setAriaHidden(pbEmailOk, true), 2200);
});
$('#pbEmailView')?.addEventListener('click', ()=>{
const saved = getLocalJSON(emailKey, null);
const info = dialogKit($('#infoDialog'));
if(!saved){
$('#infoTitle').textContent = 'Saved path';
$('#infoSubtitle').textContent = 'Nothing saved yet.';
$('#infoBody').innerHTML = 'Tip Generate a path, enter your email, then click “Save summary locally”.
';
info.open();
return;
}
const dt = new Date(saved.savedAt);
$('#infoTitle').textContent = 'Saved path summary';
$('#infoSubtitle').textContent = saved.email + ' · ' + dt.toLocaleString();
const lines = (saved.weeks||[]).map(w=>`${(w.week||'').replace('Week','Week')} ${(w.text||'').replaceAll('<','<').replaceAll('>','>')}
`).join('');
const payload = saved.payload ? JSON.stringify(saved.payload) : '—';
$('#infoBody').innerHTML =
`${(saved.title||'Your path')} ${(saved.subtitle||'').replaceAll('<','<').replaceAll('>','>')}
` +
`Catalog filters ${payload.replaceAll('<','<').replaceAll('>','>')}
` +
`${lines}
`;
info.open();
});
const infoDlg = $('#infoDialog');
const infoKit = dialogKit(infoDlg);
$('#closeInfo')?.addEventListener('click', infoKit.close);
}
function initInfoModals(){
const infoDlg = $('#infoDialog');
const infoKit = dialogKit(infoDlg);
$('#closeInfo')?.addEventListener('click', infoKit.close);
function openInfo(title, subtitle, bodyHTML){
$('#infoTitle').textContent = title;
$('#infoSubtitle').textContent = subtitle;
$('#infoBody').innerHTML = bodyHTML;
infoKit.open();
}
$('#openChecklistInfo')?.addEventListener('click', ()=>{
openInfo(
'Downloadable checklists',
'A minimalist set of printable tools for cleaner results.',
'' +
'
Gauge checklist Swatch size, washing notes, measuring method, and adjustment steps.
' +
'
Finishing checklist Weave ends, block, seam, edge treatment, and final inspection.
' +
'
Mistake recovery Ladder-down repairs, lifelines, and reading your stitches.
' +
'
Project review Fit/drape notes and what to change next time.
' +
'
'
);
});
$('#openTimersInfo')?.addEventListener('click', ()=>{
openInfo(
'Practice timers',
'Short sessions beat long, inconsistent ones.',
'' +
'
Stitch drill 5–8 minutes: knit/purl transitions, tension, and edge control.
' +
'
Swatch block 10–12 minutes: knit, measure, and record; repeat weekly.
' +
'
Break cadence Use short breaks to avoid hand fatigue and keep consistency.
' +
'
'
);
});
}
function initConsult(){
const dlg = $('#consultDialog');
const kit = dialogKit(dlg);
$('#openConsultTop')?.addEventListener('click', kit.open);
$('#openConsultSide')?.addEventListener('click', kit.open);
$('#closeConsult')?.addEventListener('click', kit.close);
const form = $('#consultForm');
const name = $('#cfName');
const email = $('#cfEmail');
const topic = $('#cfTopic');
const consent = $('#cfConsent');
const msg = $('#cfMessage');
const err = $('#cfErr');
const ok = $('#cfOk');
const key = 'kc_consult_draft_v1';
function isValidEmail(v){
const s = String(v||'').trim();
if(s.length < 6) return false;
return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(s);
}
function showErr(){
setAriaHidden(err, false);
setAriaHidden(ok, true);
}
function showOk(){
setAriaHidden(err, true);
setAriaHidden(ok, false);
setTimeout(()=>setAriaHidden(ok, true), 2400);
}
function load(){
const d = getLocalJSON(key, null);
if(!d) return;
if(name) name.value = d.name || '';
if(email) email.value = d.email || '';
if(topic && d.topic) topic.value = d.topic;
if(consent && d.reply) consent.value = d.reply;
if(msg) msg.value = d.message || '';
}
form?.addEventListener('submit', (e)=>{
e.preventDefault();
setAriaHidden(err, true);
setAriaHidden(ok, true);
const n = String(name?.value||'').trim();
const em = String(email?.value||'').trim();
const ms = String(msg?.value||'').trim();
if(n.length < 2 || !isValidEmail(em) || ms.length < 20){
showErr();
return;
}
const data = {name:n, email:em, topic: topic?.value || 'next', reply: consent?.value || '1bd', message: ms, savedAt: Date.now()};
setLocalJSON(key, data);
showOk();
});
$('#cfView')?.addEventListener('click', ()=>{
const saved = getLocalJSON(key, null);
const infoKit = dialogKit($('#infoDialog'));
$('#closeInfo')?.addEventListener('click', infoKit.close);
if(!saved){
$('#infoTitle').textContent = 'Saved message';
$('#infoSubtitle').textContent = 'No saved message found.';
$('#infoBody').innerHTML = 'Tip Fill the form and click “Save message locally”.
';
infoKit.open();
return;
}
const dt = new Date(saved.savedAt);
$('#infoTitle').textContent = 'Saved message draft';
$('#infoSubtitle').textContent = saved.email + ' · ' + dt.toLocaleString();
$('#infoBody').innerHTML =
'' +
`
Name ${saved.name.replaceAll('<','<').replaceAll('>','>')}
` +
`
Topic ${String(saved.topic).replaceAll('_',' ').replaceAll('<','<').replaceAll('>','>')}
` +
`
Reply window ${saved.reply === '2bd' ? 'Within 2 business days' : 'Within 1 business day'}
` +
`
Message ${saved.message.replaceAll('<','<').replaceAll('>','>')}
` +
'
';
infoKit.open();
});
load();
}
function initPracticeTimer(){
const status = $('#practiceTimerStatus');
const btn = $('#togglePracticeTimer');
if(!status || !btn) return;
const sw = stopwatch({
onRender: (txt)=>{ status.textContent = txt; },
onRunningChange: (is)=>{ btn.textContent = is ? 'Pause' : 'Start'; }
});
btn.addEventListener('click', ()=> sw.toggle());
}
function initChallenge(){
const challengePool = [
{title:'Neat ribbing edges', steps:[
'Cast on 24 stitches and work 8 rows of 1x1 rib.',
'On each right-side row, focus on the first and last 2 stitches: keep tension consistent.',
'Switch to stockinette for 6 rows and observe the transition line.',
'Note: if it flares, try a smaller needle for ribbing next time.'
], minutes: 8},
{title:'Purl consistency drill', steps:[
'Work 6 rows of garter, then switch to stockinette.',
'On purl rows, pause every 8 stitches and relax your grip.',
'Aim for even loop size; avoid pulling the working yarn too tight.',
'Finish with 6 rows of garter and compare both edges.'
], minutes: 9},
{title:'Read your stitches', steps:[
'Knit 10 rows in stockinette.',
'On the next row, stop every 6 stitches and identify: knit vs purl bumps below.',
'Intentionally create one mistake, then ladder down to fix it.',
'Write one sentence: what did you notice about your tension?'
], minutes: 10},
{title:'Simple decrease accuracy', steps:[
'Knit 2 rows stockinette on 30 stitches.',
'On the next right-side row: k2tog every 6 stitches across.',
'On the following right-side row: ssk every 6 stitches across.',
'Compare left-leaning vs right-leaning decreases.'
], minutes: 10},
{title:'Lifeline setup (lace-ready)', steps:[
'Knit 12 stitches stockinette for 8 rows.',
'Thread a smooth scrap yarn through one full row of stitches (lifeline).',
'Knit 4 more rows, then intentionally drop a stitch above the lifeline.',
'Recover it and observe how the lifeline keeps you safe.'
], minutes: 9}
];
function pick(){
const seed = Math.floor(Date.now()/ (1000*60*10));
const idx = seed % challengePool.length;
return challengePool[idx];
}
const titleEl = $('#challengeTitle');
const dlg = $('#challengeDialog');
const kit = dialogKit(dlg);
$('#closeChallenge')?.addEventListener('click', kit.close);
let current = pick();
function render(){
current = pick();
if(titleEl) titleEl.textContent = current.title;
$('#challengeModalTitle').textContent = current.title;
const stepsHTML = '' +
current.steps.map((s,i)=>`
Step ${i+1} ${String(s).replaceAll('<','<').replaceAll('>','>')}
`).join('') +
'
';
$('#challengeSteps').innerHTML = stepsHTML;
}
$('#refreshChallenge')?.addEventListener('click', ()=>{ render(); });
$('#openChallengeModal')?.addEventListener('click', ()=>{ render(); kit.open(); });
const timerStatus = $('#challengeTimerStatus');
const tToggle = $('#challengeTimerToggle');
const tReset = $('#challengeTimerReset');
const sw = stopwatch({
onRender: (txt)=>{ if(timerStatus) timerStatus.textContent = txt; },
onRunningChange: (is)=>{ if(tToggle) tToggle.textContent = is ? 'Pause' : 'Start'; }
});
tToggle?.addEventListener('click', ()=> sw.toggle());
tReset?.addEventListener('click', ()=> sw.reset());
render();
}
function initThemeToggle(){
updateThemeIcon();
$('#themeToggle')?.addEventListener('click', ()=>{
const cur = getTheme();
applyTheme(cur === 'dark' ? 'light' : 'dark');
});
}
function initCatalogCTAs(){
function attachDynamicHref(anchorId){
const a = $(anchorId);
if(!a) return;
a.addEventListener('click', (e)=>{
const pref = getLocalJSON('kc_path_pref_v1', null);
if(pref && pref.payload){
a.setAttribute('href', buildCatalogURL(pref.payload));
}else{
a.setAttribute('href', './catalog.html');
}
});
}
attachDynamicHref('#ctaExploreCatalog');
attachDynamicHref('#ctaCatalogBottom');
}
function closeInfoOnOutside(){
const infoDlg = $('#infoDialog');
const kit = dialogKit(infoDlg);
$('#closeInfo')?.addEventListener('click', kit.close);
}
(async function init(){
await mountPartials();
cookie().init();
initThemeToggle();
initQuickStart();
initPathBuilder();
initInfoModals();
initConsult();
initPracticeTimer();
initChallenge();
initCatalogCTAs();
cohortCountdown();
closeInfoOnOutside();
})();
})();