Sureserve operates as a Smart Meter Operator, responsible for installing smart meters on behalf of energy suppliers. A core part of their operation involves making high‑volume outbound calls to customers to book smart meter installation appointments - making efficiency, accuracy, and scale critical to success.
To support this, Sureserve required a reliable dialler solution capable of handling outbound calling at scale while reducing manual effort. As the business grew, there was also a need to improve customer engagement channels and streamline pre‑call processes.
One challenge that emerged over time was knowledge continuity. While the system was already in place when the current team joined, training knowledge had been retained by previous staff and was lost during role transitions. This limited full visibility of the platform’s capabilities, even as usage expanded across the business.
(function () {
const READ_WPM = 220; // average words per minute
const $readTime = document.getElementById('read-time');
const $progress = document.getElementById('progress-bar');
// Get all rich text elements from case_studies-sections div
function getAllRichTexts() {
const caseStudySection = document.querySelector('.case_studies-sections');
if (!caseStudySection) return [];
return Array.from(caseStudySection.querySelectorAll('.text-rich-text'));
}
const $articles = getAllRichTexts();
if (!$articles.length) return;
// --- Reading time ---
function computeReadingTime() {
let totalText = '';
// Collect text from all rich text elements
$articles.forEach($article => {
const text = ($article.innerText || $article.textContent || '').trim();
totalText += text + ' ';
});
const words = totalText.length ? totalText.split(/\s+/).length : 0;
const minutes = Math.max(1, Math.ceil(words / READ_WPM));
if ($readTime) $readTime.textContent = `${minutes} MIN READ`;
}
// --- Scroll progress ---
let startY = 0;
let endY = 0;
function clamp01(x) { return Math.max(0, Math.min(1, x)); }
function measure() {
// Calculate bounds based on all articles
let minTop = Infinity;
let maxBottom = -Infinity;
$articles.forEach($article => {
const rect = $article.getBoundingClientRect();
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const docTopToArticleTop = rect.top + scrollY;
const articleHeight = $article.offsetHeight;
minTop = Math.min(minTop, docTopToArticleTop);
maxBottom = Math.max(maxBottom, docTopToArticleTop + articleHeight);
});
const viewportH = window.innerHeight || document.documentElement.clientHeight;
startY = minTop - 16;
endY = maxBottom - viewportH + 16;
if (endY < startY) endY = startY + 1;
}
function onScroll() {
if (!$progress) return;
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const t = clamp01((scrollY - startY) / (endY - startY));
$progress.style.width = (t * 100).toFixed(2) + '%';
}
function watchImages() {
$articles.forEach($article => {
const images = $article.querySelectorAll('img');
images.forEach(img => {
if (!img.complete) {
img.addEventListener('load', () => { measure(); onScroll(); }, { once: true });
img.addEventListener('error', () => { measure(); onScroll(); }, { once: true });
}
});
});
}
computeReadingTime();
measure();
watchImages();
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', () => { measure(); onScroll(); });
// Watch for changes in all articles
const mo = new MutationObserver(() => { computeReadingTime(); measure(); onScroll(); });
$articles.forEach($article => {
mo.observe($article, { childList: true, subtree: true, characterData: true });
});
})();
(() => {
const rich = document.querySelector('#rich-text');
const toc = document.querySelector('#toc');
if (!rich || !toc) return;
// Only H2s inside the Rich Text
const headings = [...rich.querySelectorAll('h2')];
if (!headings.length) { toc.style.display = 'none'; return; }
// Slugify + ensure unique IDs (handles accents like šđčćž)
const slugCounts = {};
const slugify = (str) => {
const base = (str || '')
.trim()
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // remove diacritics
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
const n = (slugCounts[base] = (slugCounts[base] || 0) + 1);
return n > 1 ? `${base}-${n}` : base || `section-${n}`;
};
// Build anchors directly inside #toc
toc.innerHTML = '';
headings.forEach((h, idx) => {
if (!h.id) h.id = slugify(h.textContent || `section-${idx+1}`);
const a = document.createElement('a');
a.href = `#${h.id}`;
a.className = 'content_link';
a.dataset.target = h.id;
a.setAttribute('aria-label', h.textContent || `Section ${idx+1}`);
const p = document.createElement('p');
p.className = 'text-size-small';
p.textContent = h.textContent || `Section ${idx+1}`;
a.appendChild(p);
toc.appendChild(a);
});
// Offset for fixed navs - with extra spacing for visibility
const getOffset = () => {
const nav = document.querySelector('.navbar, .w-nav, [data-nav]');
const navHeight = nav ? nav.getBoundingClientRect().height : 0;
// Add 30px buffer to ensure heading is clearly visible below fixed navbar
return navHeight + 30;
};
toc.addEventListener('click', (e) => {
const link = e.target.closest('a.content_link[href^="#"]');
if (!link) return;
e.preventDefault();
e.stopPropagation(); // Stop other event listeners
const id = link.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
const targetTop = target.getBoundingClientRect().top + window.scrollY;
const finalY = targetTop - 150;
// Use only smooth scroll
window.scrollTo({ top: finalY, behavior: 'smooth' });
history.replaceState(null, '', `#${id}`);
});
})();
Sureserve uses MaxContact from Firstcom Europe as the foundation of their outbound calling operation.
The dialler functionality allows teams to contact customers efficiently, significantly reducing the time and resource required for pre‑call processes. Alongside voice, Sureserve has expanded its use of SMS messaging, enabling customers to confirm appointments quickly and conveniently - now the most popular confirmation method.
Firstcom’s support team, particularly Cheryl and Megan, played a key role in strengthening the partnership. Through on‑site visits and hands‑on support, they helped build customer knowledge, provided guidance on best‑practice usage, and supported Sureserve’s ambitions to further develop their communications strategy, including exploring additional messaging channels such as WhatsApp.
(function () {
const READ_WPM = 220; // average words per minute
const $readTime = document.getElementById('read-time');
const $progress = document.getElementById('progress-bar');
// Get all rich text elements from case_studies-sections div
function getAllRichTexts() {
const caseStudySection = document.querySelector('.case_studies-sections');
if (!caseStudySection) return [];
return Array.from(caseStudySection.querySelectorAll('.text-rich-text'));
}
const $articles = getAllRichTexts();
if (!$articles.length) return;
// --- Reading time ---
function computeReadingTime() {
let totalText = '';
// Collect text from all rich text elements
$articles.forEach($article => {
const text = ($article.innerText || $article.textContent || '').trim();
totalText += text + ' ';
});
const words = totalText.length ? totalText.split(/\s+/).length : 0;
const minutes = Math.max(1, Math.ceil(words / READ_WPM));
if ($readTime) $readTime.textContent = `${minutes} MIN READ`;
}
// --- Scroll progress ---
let startY = 0;
let endY = 0;
function clamp01(x) { return Math.max(0, Math.min(1, x)); }
function measure() {
// Calculate bounds based on all articles
let minTop = Infinity;
let maxBottom = -Infinity;
$articles.forEach($article => {
const rect = $article.getBoundingClientRect();
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const docTopToArticleTop = rect.top + scrollY;
const articleHeight = $article.offsetHeight;
minTop = Math.min(minTop, docTopToArticleTop);
maxBottom = Math.max(maxBottom, docTopToArticleTop + articleHeight);
});
const viewportH = window.innerHeight || document.documentElement.clientHeight;
startY = minTop - 16;
endY = maxBottom - viewportH + 16;
if (endY < startY) endY = startY + 1;
}
function onScroll() {
if (!$progress) return;
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const t = clamp01((scrollY - startY) / (endY - startY));
$progress.style.width = (t * 100).toFixed(2) + '%';
}
function watchImages() {
$articles.forEach($article => {
const images = $article.querySelectorAll('img');
images.forEach(img => {
if (!img.complete) {
img.addEventListener('load', () => { measure(); onScroll(); }, { once: true });
img.addEventListener('error', () => { measure(); onScroll(); }, { once: true });
}
});
});
}
computeReadingTime();
measure();
watchImages();
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', () => { measure(); onScroll(); });
// Watch for changes in all articles
const mo = new MutationObserver(() => { computeReadingTime(); measure(); onScroll(); });
$articles.forEach($article => {
mo.observe($article, { childList: true, subtree: true, characterData: true });
});
})();
(() => {
const rich = document.querySelector('#rich-text');
const toc = document.querySelector('#toc');
if (!rich || !toc) return;
// Only H2s inside the Rich Text
const headings = [...rich.querySelectorAll('h2')];
if (!headings.length) { toc.style.display = 'none'; return; }
// Slugify + ensure unique IDs (handles accents like šđčćž)
const slugCounts = {};
const slugify = (str) => {
const base = (str || '')
.trim()
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // remove diacritics
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
const n = (slugCounts[base] = (slugCounts[base] || 0) + 1);
return n > 1 ? `${base}-${n}` : base || `section-${n}`;
};
// Build anchors directly inside #toc
toc.innerHTML = '';
headings.forEach((h, idx) => {
if (!h.id) h.id = slugify(h.textContent || `section-${idx+1}`);
const a = document.createElement('a');
a.href = `#${h.id}`;
a.className = 'content_link';
a.dataset.target = h.id;
a.setAttribute('aria-label', h.textContent || `Section ${idx+1}`);
const p = document.createElement('p');
p.className = 'text-size-small';
p.textContent = h.textContent || `Section ${idx+1}`;
a.appendChild(p);
toc.appendChild(a);
});
// Offset for fixed navs - with extra spacing for visibility
const getOffset = () => {
const nav = document.querySelector('.navbar, .w-nav, [data-nav]');
const navHeight = nav ? nav.getBoundingClientRect().height : 0;
// Add 30px buffer to ensure heading is clearly visible below fixed navbar
return navHeight + 30;
};
toc.addEventListener('click', (e) => {
const link = e.target.closest('a.content_link[href^="#"]');
if (!link) return;
e.preventDefault();
e.stopPropagation(); // Stop other event listeners
const id = link.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
const targetTop = target.getBoundingClientRect().top + window.scrollY;
const finalY = targetTop - 150;
// Use only smooth scroll
window.scrollTo({ top: finalY, behavior: 'smooth' });
history.replaceState(null, '', `#${id}`);
});
})();
The impact of MaxContact has been significant for Sureserve’s operational efficiency.
Key outcomes include -
- Dramatic productivity gains: A pre‑call process that previously required 10 people working all day now takes just three people less than two hours using the dialler functionality within the MaxContact platform
- Improved customer engagement through expanded SMS functionality, now the preferred appointment confirmation method
- Greater operational visibility for managers, supporting performance oversight and optimisation
- Scalable foundations for future enhancements, including additional digital communication channels
While the technology itself has delivered strong results, Sureserve values Firstcom’s openness to feedback and continuous improvement. The ability to influence system development and adapt solutions to real operational needs has reinforced confidence in MaxContact as a long‑term platform.
Sureserve would recommend Firstcom Europe - particularly MaxContact - to other organisations seeking an effective outbound engagement solution, highlighting both the strength of the product and the supportive, hands‑on approach of the Firstcom team.
“MaxContact and Firstcom Europe have made a huge difference to how we operate. What used to take a full team all day can now be completed by just a few people in a fraction of the time. The outbound dialler and omnichannel functionality have transformed our contact strategy and how we book appointments and engage with customers.
MaxContact is a great platform, and it’s clear that customer feedback is genuinely taken on board to keep improving the system.” – Chris Hayes, Contact Centre Manager, Sureserve
(function () {
const READ_WPM = 220; // average words per minute
const $readTime = document.getElementById('read-time');
const $progress = document.getElementById('progress-bar');
// Get all rich text elements from case_studies-sections div
function getAllRichTexts() {
const caseStudySection = document.querySelector('.case_studies-sections');
if (!caseStudySection) return [];
return Array.from(caseStudySection.querySelectorAll('.text-rich-text'));
}
const $articles = getAllRichTexts();
if (!$articles.length) return;
// --- Reading time ---
function computeReadingTime() {
let totalText = '';
// Collect text from all rich text elements
$articles.forEach($article => {
const text = ($article.innerText || $article.textContent || '').trim();
totalText += text + ' ';
});
const words = totalText.length ? totalText.split(/\s+/).length : 0;
const minutes = Math.max(1, Math.ceil(words / READ_WPM));
if ($readTime) $readTime.textContent = `${minutes} MIN READ`;
}
// --- Scroll progress ---
let startY = 0;
let endY = 0;
function clamp01(x) { return Math.max(0, Math.min(1, x)); }
function measure() {
// Calculate bounds based on all articles
let minTop = Infinity;
let maxBottom = -Infinity;
$articles.forEach($article => {
const rect = $article.getBoundingClientRect();
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const docTopToArticleTop = rect.top + scrollY;
const articleHeight = $article.offsetHeight;
minTop = Math.min(minTop, docTopToArticleTop);
maxBottom = Math.max(maxBottom, docTopToArticleTop + articleHeight);
});
const viewportH = window.innerHeight || document.documentElement.clientHeight;
startY = minTop - 16;
endY = maxBottom - viewportH + 16;
if (endY < startY) endY = startY + 1;
}
function onScroll() {
if (!$progress) return;
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const t = clamp01((scrollY - startY) / (endY - startY));
$progress.style.width = (t * 100).toFixed(2) + '%';
}
function watchImages() {
$articles.forEach($article => {
const images = $article.querySelectorAll('img');
images.forEach(img => {
if (!img.complete) {
img.addEventListener('load', () => { measure(); onScroll(); }, { once: true });
img.addEventListener('error', () => { measure(); onScroll(); }, { once: true });
}
});
});
}
computeReadingTime();
measure();
watchImages();
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', () => { measure(); onScroll(); });
// Watch for changes in all articles
const mo = new MutationObserver(() => { computeReadingTime(); measure(); onScroll(); });
$articles.forEach($article => {
mo.observe($article, { childList: true, subtree: true, characterData: true });
});
})();
(() => {
const rich = document.querySelector('#rich-text');
const toc = document.querySelector('#toc');
if (!rich || !toc) return;
// Only H2s inside the Rich Text
const headings = [...rich.querySelectorAll('h2')];
if (!headings.length) { toc.style.display = 'none'; return; }
// Slugify + ensure unique IDs (handles accents like šđčćž)
const slugCounts = {};
const slugify = (str) => {
const base = (str || '')
.trim()
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // remove diacritics
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
const n = (slugCounts[base] = (slugCounts[base] || 0) + 1);
return n > 1 ? `${base}-${n}` : base || `section-${n}`;
};
// Build anchors directly inside #toc
toc.innerHTML = '';
headings.forEach((h, idx) => {
if (!h.id) h.id = slugify(h.textContent || `section-${idx+1}`);
const a = document.createElement('a');
a.href = `#${h.id}`;
a.className = 'content_link';
a.dataset.target = h.id;
a.setAttribute('aria-label', h.textContent || `Section ${idx+1}`);
const p = document.createElement('p');
p.className = 'text-size-small';
p.textContent = h.textContent || `Section ${idx+1}`;
a.appendChild(p);
toc.appendChild(a);
});
// Offset for fixed navs - with extra spacing for visibility
const getOffset = () => {
const nav = document.querySelector('.navbar, .w-nav, [data-nav]');
const navHeight = nav ? nav.getBoundingClientRect().height : 0;
// Add 30px buffer to ensure heading is clearly visible below fixed navbar
return navHeight + 30;
};
toc.addEventListener('click', (e) => {
const link = e.target.closest('a.content_link[href^="#"]');
if (!link) return;
e.preventDefault();
e.stopPropagation(); // Stop other event listeners
const id = link.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
const targetTop = target.getBoundingClientRect().top + window.scrollY;
const finalY = targetTop - 150;
// Use only smooth scroll
window.scrollTo({ top: finalY, behavior: 'smooth' });
history.replaceState(null, '', `#${id}`);
});
})();
(function () {
const READ_WPM = 220; // average words per minute
const $readTime = document.getElementById('read-time');
const $progress = document.getElementById('progress-bar');
// Get all rich text elements from case_studies-sections div
function getAllRichTexts() {
const caseStudySection = document.querySelector('.case_studies-sections');
if (!caseStudySection) return [];
return Array.from(caseStudySection.querySelectorAll('.text-rich-text'));
}
const $articles = getAllRichTexts();
if (!$articles.length) return;
// --- Reading time ---
function computeReadingTime() {
let totalText = '';
// Collect text from all rich text elements
$articles.forEach($article => {
const text = ($article.innerText || $article.textContent || '').trim();
totalText += text + ' ';
});
const words = totalText.length ? totalText.split(/\s+/).length : 0;
const minutes = Math.max(1, Math.ceil(words / READ_WPM));
if ($readTime) $readTime.textContent = `${minutes} MIN READ`;
}
// --- Scroll progress ---
let startY = 0;
let endY = 0;
function clamp01(x) { return Math.max(0, Math.min(1, x)); }
function measure() {
// Calculate bounds based on all articles
let minTop = Infinity;
let maxBottom = -Infinity;
$articles.forEach($article => {
const rect = $article.getBoundingClientRect();
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const docTopToArticleTop = rect.top + scrollY;
const articleHeight = $article.offsetHeight;
minTop = Math.min(minTop, docTopToArticleTop);
maxBottom = Math.max(maxBottom, docTopToArticleTop + articleHeight);
});
const viewportH = window.innerHeight || document.documentElement.clientHeight;
startY = minTop - 16;
endY = maxBottom - viewportH + 16;
if (endY < startY) endY = startY + 1;
}
function onScroll() {
if (!$progress) return;
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const t = clamp01((scrollY - startY) / (endY - startY));
$progress.style.width = (t * 100).toFixed(2) + '%';
}
function watchImages() {
$articles.forEach($article => {
const images = $article.querySelectorAll('img');
images.forEach(img => {
if (!img.complete) {
img.addEventListener('load', () => { measure(); onScroll(); }, { once: true });
img.addEventListener('error', () => { measure(); onScroll(); }, { once: true });
}
});
});
}
computeReadingTime();
measure();
watchImages();
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', () => { measure(); onScroll(); });
// Watch for changes in all articles
const mo = new MutationObserver(() => { computeReadingTime(); measure(); onScroll(); });
$articles.forEach($article => {
mo.observe($article, { childList: true, subtree: true, characterData: true });
});
})();
(() => {
const rich = document.querySelector('#rich-text');
const toc = document.querySelector('#toc');
if (!rich || !toc) return;
// Only H2s inside the Rich Text
const headings = [...rich.querySelectorAll('h2')];
if (!headings.length) { toc.style.display = 'none'; return; }
// Slugify + ensure unique IDs (handles accents like šđčćž)
const slugCounts = {};
const slugify = (str) => {
const base = (str || '')
.trim()
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // remove diacritics
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
const n = (slugCounts[base] = (slugCounts[base] || 0) + 1);
return n > 1 ? `${base}-${n}` : base || `section-${n}`;
};
// Build anchors directly inside #toc
toc.innerHTML = '';
headings.forEach((h, idx) => {
if (!h.id) h.id = slugify(h.textContent || `section-${idx+1}`);
const a = document.createElement('a');
a.href = `#${h.id}`;
a.className = 'content_link';
a.dataset.target = h.id;
a.setAttribute('aria-label', h.textContent || `Section ${idx+1}`);
const p = document.createElement('p');
p.className = 'text-size-small';
p.textContent = h.textContent || `Section ${idx+1}`;
a.appendChild(p);
toc.appendChild(a);
});
// Offset for fixed navs - with extra spacing for visibility
const getOffset = () => {
const nav = document.querySelector('.navbar, .w-nav, [data-nav]');
const navHeight = nav ? nav.getBoundingClientRect().height : 0;
// Add 30px buffer to ensure heading is clearly visible below fixed navbar
return navHeight + 30;
};
toc.addEventListener('click', (e) => {
const link = e.target.closest('a.content_link[href^="#"]');
if (!link) return;
e.preventDefault();
e.stopPropagation(); // Stop other event listeners
const id = link.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
const targetTop = target.getBoundingClientRect().top + window.scrollY;
const finalY = targetTop - 150;
// Use only smooth scroll
window.scrollTo({ top: finalY, behavior: 'smooth' });
history.replaceState(null, '', `#${id}`);
});
})();
(function () {
const READ_WPM = 220; // average words per minute
const $readTime = document.getElementById('read-time');
const $progress = document.getElementById('progress-bar');
// Get all rich text elements from case_studies-sections div
function getAllRichTexts() {
const caseStudySection = document.querySelector('.case_studies-sections');
if (!caseStudySection) return [];
return Array.from(caseStudySection.querySelectorAll('.text-rich-text'));
}
const $articles = getAllRichTexts();
if (!$articles.length) return;
// --- Reading time ---
function computeReadingTime() {
let totalText = '';
// Collect text from all rich text elements
$articles.forEach($article => {
const text = ($article.innerText || $article.textContent || '').trim();
totalText += text + ' ';
});
const words = totalText.length ? totalText.split(/\s+/).length : 0;
const minutes = Math.max(1, Math.ceil(words / READ_WPM));
if ($readTime) $readTime.textContent = `${minutes} MIN READ`;
}
// --- Scroll progress ---
let startY = 0;
let endY = 0;
function clamp01(x) { return Math.max(0, Math.min(1, x)); }
function measure() {
// Calculate bounds based on all articles
let minTop = Infinity;
let maxBottom = -Infinity;
$articles.forEach($article => {
const rect = $article.getBoundingClientRect();
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const docTopToArticleTop = rect.top + scrollY;
const articleHeight = $article.offsetHeight;
minTop = Math.min(minTop, docTopToArticleTop);
maxBottom = Math.max(maxBottom, docTopToArticleTop + articleHeight);
});
const viewportH = window.innerHeight || document.documentElement.clientHeight;
startY = minTop - 16;
endY = maxBottom - viewportH + 16;
if (endY < startY) endY = startY + 1;
}
function onScroll() {
if (!$progress) return;
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const t = clamp01((scrollY - startY) / (endY - startY));
$progress.style.width = (t * 100).toFixed(2) + '%';
}
function watchImages() {
$articles.forEach($article => {
const images = $article.querySelectorAll('img');
images.forEach(img => {
if (!img.complete) {
img.addEventListener('load', () => { measure(); onScroll(); }, { once: true });
img.addEventListener('error', () => { measure(); onScroll(); }, { once: true });
}
});
});
}
computeReadingTime();
measure();
watchImages();
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', () => { measure(); onScroll(); });
// Watch for changes in all articles
const mo = new MutationObserver(() => { computeReadingTime(); measure(); onScroll(); });
$articles.forEach($article => {
mo.observe($article, { childList: true, subtree: true, characterData: true });
});
})();
(() => {
const rich = document.querySelector('#rich-text');
const toc = document.querySelector('#toc');
if (!rich || !toc) return;
// Only H2s inside the Rich Text
const headings = [...rich.querySelectorAll('h2')];
if (!headings.length) { toc.style.display = 'none'; return; }
// Slugify + ensure unique IDs (handles accents like šđčćž)
const slugCounts = {};
const slugify = (str) => {
const base = (str || '')
.trim()
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // remove diacritics
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
const n = (slugCounts[base] = (slugCounts[base] || 0) + 1);
return n > 1 ? `${base}-${n}` : base || `section-${n}`;
};
// Build anchors directly inside #toc
toc.innerHTML = '';
headings.forEach((h, idx) => {
if (!h.id) h.id = slugify(h.textContent || `section-${idx+1}`);
const a = document.createElement('a');
a.href = `#${h.id}`;
a.className = 'content_link';
a.dataset.target = h.id;
a.setAttribute('aria-label', h.textContent || `Section ${idx+1}`);
const p = document.createElement('p');
p.className = 'text-size-small';
p.textContent = h.textContent || `Section ${idx+1}`;
a.appendChild(p);
toc.appendChild(a);
});
// Offset for fixed navs - with extra spacing for visibility
const getOffset = () => {
const nav = document.querySelector('.navbar, .w-nav, [data-nav]');
const navHeight = nav ? nav.getBoundingClientRect().height : 0;
// Add 30px buffer to ensure heading is clearly visible below fixed navbar
return navHeight + 30;
};
toc.addEventListener('click', (e) => {
const link = e.target.closest('a.content_link[href^="#"]');
if (!link) return;
e.preventDefault();
e.stopPropagation(); // Stop other event listeners
const id = link.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
const targetTop = target.getBoundingClientRect().top + window.scrollY;
const finalY = targetTop - 150;
// Use only smooth scroll
window.scrollTo({ top: finalY, behavior: 'smooth' });
history.replaceState(null, '', `#${id}`);
});
})();