We would love to hear from you. Call, book, or send a message, whichever suits.

Get a quote or book a Brisbane plumber today

There are three ways to reach us. Call (07) 3523 1888 and speak to Natalie, book online through ServiceM8, or send a quick message and we will come straight back to you. Real people, real answers, and no call centre runaround.

call
event
mail

Email

Send us a quick message and we will come straight back to you.

help@calloctoplus.com.au
support_agent

Phone

Call Natalie. She answers every call personally, Monday to Friday, 7am to 4pm.

(07) 3523 1888
check_circle

Office

Drop in Monday to Friday, 7am to 4pm, or book a time that suits. 19/104 Barwon Street, Morningside QLD 4170.

Get directions
Where We Work

Serving Brisbane From Our Morningside Base

We work within a 12km radius of our Morningside office, right across Brisbane. Primary suburbs include Morningside, Bulimba, Balmoral, Hawthorne, Cannon Hill, Seven Hills, Norman Park, Camp Hill and Coorparoo. We also cover Ascot, Hamilton, New Farm, Paddington, Red Hill, Wynnum, Manly and more.

north_east
location_on
Morningside
location_on
Bulimba
location_on
Hawthorne
location_on
Cannon Hill
location_on
Camp Hill
location_on
Coorparoo
location_on
Balmoral
location_on
Norman Park
location_on
Seven Hills
/* ========================================= Lumos GSAP Starter Interactions Version: v1.1 ========================================= */ document.addEventListener("DOMContentLoaded", () => { /* ========================================= Validación inicial de GSAP ========================================= */ if (typeof gsap === "undefined") return; gsap.registerPlugin(ScrollTrigger); const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (prefersReducedMotion) return; /* ========================================= Animaciones de counters, cards e íconos ========================================= */ function initCounters() { /* Elementos */ const cards = document.querySelectorAll(".counter_card"); const counters = document.querySelectorAll(".counter_digit"); const icons = document.querySelectorAll(".counter_icon"); if (!cards.length && !counters.length && !icons.length) return; /* ----------------------------------------- Animación de entrada de las cards ----------------------------------------- */ if (cards.length) { gsap.fromTo(cards, { y: 32, opacity: 0, scale: 0.96, filter: "blur(8px)" }, { y: 0, opacity: 1, scale: 1, filter: "blur(0px)", duration: 0.75, ease: "power3.out", stagger: 0.12, scrollTrigger: { trigger: cards[0], start: "top 85%", once: true } } ); } /* ----------------------------------------- Animación numérica de los counters ----------------------------------------- */ counters.forEach((counter) => { const originalValue = counter.textContent.trim(); // Normaliza el número para aceptar separadores de miles const numericValue = originalValue.replace(/,/g, ""); const endValue = parseFloat(numericValue); if (isNaN(endValue)) return; const before = counter.dataset.before || ""; const after = counter.dataset.after || ""; const duration = parseFloat(counter.dataset.duration) || 2.2; const delay = parseFloat(counter.dataset.delay) || 0.2; // Detecta los decimales desde el valor numérico real. // Opcionalmente se pueden definir manualmente con data-decimals. const decimals = counter.dataset.decimals !== undefined ? Math.max(0, parseInt(counter.dataset.decimals, 10) || 0) : (String(endValue).split(".")[1] || "").length; // Mantiene separadores de miles y decimales const formatter = new Intl.NumberFormat("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); let obj = { value: 0 }; gsap.to(obj, { value: endValue, duration, delay, ease: "power4.out", scrollTrigger: { trigger: counter, start: "top 85%", once: true }, // Actualiza el texto mientras avanza la animación onUpdate: () => { counter.textContent = before + formatter.format(obj.value) + after; }, // Ajusta el valor final y agrega un pequeño rebote onComplete: () => { counter.textContent = before + formatter.format(endValue) + after; gsap.fromTo(counter, { scale: 1 }, { scale: 1.08, duration: 0.18, yoyo: true, repeat: 1, ease: "power2.out" } ); } }); }); /* ----------------------------------------- Animación de íconos ----------------------------------------- */ icons.forEach((icon, index) => { const iconInner = icon.querySelector(".material_icon") || icon; const tl = gsap.timeline({ scrollTrigger: { trigger: icon, start: "top 85%", once: true }, delay: 0.35 + index * 0.12 }); tl.fromTo(iconInner, { scale: 0, rotate: -20, opacity: 0, filter: "blur(8px)" }, { scale: 1.18, rotate: 0, opacity: 1, filter: "blur(0px)", duration: 0.55, ease: "back.out(2.4)" } ) .to(iconInner, { scale: 1, duration: 0.25, ease: "power2.out" }) .to(iconInner, { y: -4, duration: 1.8, repeat: -1, yoyo: true, ease: "sine.inOut" }); }); } /* ========================================= Inicialización general ========================================= */ initCounters(); initAutoTabs(); initHeadingReveal(); });