// Tweaks app — handles theme variables + edit-mode protocol. Content is static HTML. const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "palette": ["#0A1628", "#C9A961", "#F2EDE4"], "accentIntensity": 1, "displayFont": "Instrument Serif", "showGrid": false, "density": "regular" }/*EDITMODE-END*/; const PALETTES = { "Granat / Złoto": ["#0A1628", "#C9A961", "#F2EDE4"], "Granat / Mosiądz": ["#0B1A2E", "#B8935A", "#EDE7DA"], "Atrament / Złoto": ["#080F1A", "#D4B574", "#F4EFE6"], "Stalowy / Brąz": ["#13202D", "#B8825A", "#EFEAE0"], "Nokturn / Srebro": ["#0A1322", "#A8B0BD", "#EDEEF1"] }; const FONTS = { "Instrument Serif": "'Instrument Serif', Georgia, serif", "Cormorant Garamond": "'Cormorant Garamond', Georgia, serif", "Libre Caslon Text": "'Libre Caslon Text', Georgia, serif", "Newsreader": "'Newsreader', Georgia, serif" }; function applyTheme(t) { const root = document.documentElement; const [bg, accent, ink] = t.palette; root.style.setProperty('--bg', bg); root.style.setProperty('--accent', accent); root.style.setProperty('--ink', ink); // Derive other shades const elev = mixHex(bg, '#FFFFFF', 0.04); const deep = mixHex(bg, '#000000', 0.4); const panel = mixHex(bg, '#FFFFFF', 0.08); const accentDim = mixHex(accent, bg, 0.4); const accentSoft = hexToRgba(accent, 0.12 * t.accentIntensity); const rule = hexToRgba(accent, 0.18 * t.accentIntensity); const ruleFaint = hexToRgba(ink, 0.08); const inkMuted = mixHex(ink, bg, 0.45); const inkDim = mixHex(ink, bg, 0.65); root.style.setProperty('--bg-elev', elev); root.style.setProperty('--bg-deep', deep); root.style.setProperty('--bg-panel', panel); root.style.setProperty('--accent-dim', accentDim); root.style.setProperty('--accent-soft', accentSoft); root.style.setProperty('--rule', rule); root.style.setProperty('--rule-faint', ruleFaint); root.style.setProperty('--ink-muted', inkMuted); root.style.setProperty('--ink-dim', inkDim); root.style.setProperty('--font-display', FONTS[t.displayFont] || FONTS['Instrument Serif']); document.body.classList.toggle('show-grid', !!t.showGrid); document.body.dataset.density = t.density; } function hexToRgb(hex) { const h = hex.replace('#', ''); return { r: parseInt(h.slice(0,2), 16), g: parseInt(h.slice(2,4), 16), b: parseInt(h.slice(4,6), 16), }; } function rgbToHex(r,g,b) { return '#' + [r,g,b].map(v => Math.round(Math.max(0,Math.min(255,v))).toString(16).padStart(2,'0')).join(''); } function mixHex(a, b, t) { const A = hexToRgb(a), B = hexToRgb(b); return rgbToHex(A.r + (B.r-A.r)*t, A.g + (B.g-A.g)*t, A.b + (B.b-A.b)*t); } function hexToRgba(hex, a) { const {r,g,b} = hexToRgb(hex); return `rgba(${r},${g},${b},${a})`; } function TweakApp() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); React.useEffect(() => { applyTheme(t); }, [t]); // Find current palette name const paletteName = Object.entries(PALETTES).find(([_, v]) => v[0] === t.palette[0] && v[1] === t.palette[1] )?.[0] || Object.keys(PALETTES)[0]; return ( setTweak('palette', PALETTES[v])} /> setTweak('accentIntensity', v)} /> setTweak('displayFont', v)} /> setTweak('density', v)} /> setTweak('showGrid', v)} /> ); } // FAQ accordion — vanilla, plays nicely with static HTML document.addEventListener('click', (e) => { const item = e.target.closest('.faq__item'); if (!item) return; if (e.target.closest('.faq__a')) return; // don't toggle when clicking inside answer item.classList.toggle('is-open'); }); // Smooth scroll for nav links document.addEventListener('click', (e) => { const a = e.target.closest('a[href^="#"]'); if (!a) return; const href = a.getAttribute('href'); if (href.length < 2) return; const target = document.querySelector(href); if (!target) return; e.preventDefault(); target.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); // Sub-nav: highlight active section on scroll (function() { const subnav = document.querySelector('.subnav'); if (!subnav) return; // Measure main-nav height once and expose as CSS var so subnav can sit just below it const mainNav = document.querySelector('.nav'); if (mainNav) { const setNavH = () => { document.documentElement.style.setProperty('--main-nav-h', mainNav.offsetHeight + 'px'); }; setNavH(); window.addEventListener('resize', setNavH, { passive: true }); } const links = subnav.querySelectorAll('.subnav__link'); if (!links.length) return; const sections = [...links].map(l => document.querySelector(l.getAttribute('href'))).filter(Boolean); if (!sections.length) return; function onScroll() { const y = window.scrollY + 200; let idx = -1; sections.forEach((s, i) => { if (s.offsetTop <= y) idx = i; }); links.forEach((l, i) => l.classList.toggle('is-active', i === idx)); } window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); })(); // Program rail active-state on scroll (function() { const days = document.querySelectorAll('.day'); const railItems = document.querySelectorAll('.program__rail-day'); if (!days.length || !railItems.length) return; function onScroll() { const vh = window.innerHeight; let activeIdx = 0; days.forEach((d, i) => { const r = d.getBoundingClientRect(); if (r.top < vh * 0.4) activeIdx = i; }); railItems.forEach((el, i) => el.classList.toggle('is-active', i === activeIdx)); } window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); })(); ReactDOM.createRoot(document.getElementById('tweaks-root')).render();