// 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();