From 9303dc7f4db71c8f87c15f84af8b3c88ff0e3060 Mon Sep 17 00:00:00 2001 From: Fraxle Date: Tue, 2 Jun 2026 12:38:00 +0100 Subject: [PATCH] Add estimates package modals, magical animations and pricing links - estimates: per-package detail modals with min/max breakdown and specs, opening with a wand-flick spiral spring and sparkle trail, closing with a perimeter sparkle burst - home: pricing teaser section linking to estimates; contact form pre-fills a message when arriving from a package "Enquire" link - mascot: Star Trek-style teleport-in (delay, sparkle charge, bottom-up materialise with near-white energy wash and rising sparkle beam) - buttons: wand-sparkle swipe speed and density now scale with button width - themed scrollbars site-wide with stable gutter to stop modal scroll shift Co-Authored-By: Claude Opus 4.8 --- css/style.css | 32 +++- estimates.php | 518 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.php | 19 +- js/main.js | 161 +++++++++++++++- 4 files changed, 724 insertions(+), 6 deletions(-) diff --git a/css/style.css b/css/style.css index 7b7af51..7ca1386 100644 --- a/css/style.css +++ b/css/style.css @@ -32,7 +32,11 @@ /* --- Reset --- */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } -html { scroll-behavior: smooth; } +html { + scroll-behavior: smooth; + background: #251D70; /* matches dark sections so the reserved gutter blends */ + scrollbar-gutter: stable; /* reserve scrollbar space so locking scroll doesn't shift the page */ +} body { font-family: var(--font-body); color: var(--text); @@ -40,6 +44,23 @@ body { line-height: 1.6; overflow-x: hidden; } + +/* --- Custom scrollbars (themed) --- */ +html { scrollbar-width: thin; scrollbar-color: rgba(251,191,36,0.55) rgba(255,255,255,0.06); } +.pkg-modal, .nav-links { scrollbar-width: thin; scrollbar-color: rgba(251,191,36,0.55) rgba(255,255,255,0.06); } +::-webkit-scrollbar { width: 12px; height: 12px; } +::-webkit-scrollbar-track { background: rgba(255,255,255,0.05); } +::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, var(--gold), var(--orange)); + background-clip: padding-box; + border: 3px solid transparent; /* slims the thumb and leaves breathing room in the track */ + border-radius: 10px; +} +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(180deg, var(--orange), var(--gold)); + background-clip: padding-box; +} +::-webkit-scrollbar-corner { background: transparent; } img { max-width: 100%; display: block; } a { text-decoration: none; color: inherit; } ul { list-style: none; } @@ -397,6 +418,8 @@ ul { list-style: none; } display: block; filter: drop-shadow(0 8px 24px rgba(0,0,0,0.35)); } +/* Hidden until the JS "transporter" effect materialises it (see main.js) */ +.mascot-teleport { opacity: 0; } /* Wave */ .hero-wave { @@ -672,6 +695,13 @@ ul { list-style: none; } transform: translateX(4px); } +/* ================================================ + PRICING TEASER + ================================================ */ +.pricing-teaser { background: #3B2DA8; } +.pricing-teaser .section-header { margin-bottom: 0; } +.pricing-teaser-btn { margin-top: 32px; } + /* ================================================ CONTACT ================================================ */ diff --git a/estimates.php b/estimates.php index a20de2c..2b0d41e 100644 --- a/estimates.php +++ b/estimates.php @@ -324,6 +324,175 @@ color: rgba(255,255,255,0.4); } + /* Enquire button on each package */ + .pricing-enquire { + margin-top: 18px; + width: 100%; + justify-content: center; + font-size: 0.85rem; + padding: 12px 18px; + } + .bench-enquire { margin-top: 22px; } + .ward-enquire { margin-top: 20px; } + + /* "See full details" trigger */ + .pkg-details-btn { + background: none; + border: none; + cursor: pointer; + color: var(--gold); + font-family: var(--font-head); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.03em; + display: inline-flex; + align-items: center; + gap: 7px; + margin-top: 16px; + padding: 6px 0; + transition: gap var(--transition), color var(--transition); + } + .pkg-details-btn:hover { gap: 11px; color: var(--white); } + .pricing-card, .ward-card, .bench-card { cursor: pointer; } + + /* Package detail modal */ + .pkg-modal-overlay { + position: fixed; + inset: 0; + background: rgba(10,6,30,0.78); + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: opacity 0.25s ease, visibility 0.25s ease; + } + .pkg-modal-overlay.open { opacity: 1; visibility: visible; } + .pkg-modal { + background: linear-gradient(160deg, #2D1F6E 0%, #1E1550 100%); + border: 1px solid rgba(251,191,36,0.25); + border-radius: var(--radius); + max-width: 560px; + width: 100%; + max-height: 88vh; + overflow-y: auto; + padding: 40px; + position: relative; + box-shadow: 0 24px 70px rgba(0,0,0,0.5); + transform-origin: 50% 50%; + will-change: transform; + } + /* The spiral open animation is driven in JS (see below). For users who + prefer reduced motion, fall back to a gentle fade/scale instead. */ + @media (prefers-reduced-motion: reduce) { + .pkg-modal { transform: none !important; transition: none !important; } + } + .pkg-sparkle { + position: fixed; + z-index: 1100; + pointer-events: none; + line-height: 1; + transform: translate(-50%, -50%); + text-shadow: 0 0 6px rgba(255,255,255,0.4); + will-change: transform, opacity; + } + .pkg-modal-close { + position: absolute; + top: 18px; right: 18px; + width: 38px; height: 38px; + border-radius: 50%; + border: 1px solid rgba(255,255,255,0.18); + background: rgba(255,255,255,0.06); + color: rgba(255,255,255,0.8); + font-size: 1.3rem; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s, color 0.2s; + } + .pkg-modal-close:hover { background: rgba(255,255,255,0.14); color: var(--white); } + .pkg-modal-emoji { font-size: 2.4rem; line-height: 1; margin-bottom: 10px; } + .pkg-modal-name { + font-family: var(--font-head); + font-size: 1.6rem; + font-weight: 800; + color: var(--white); + margin-bottom: 4px; + padding-right: 36px; + } + .pkg-modal-price { + font-family: var(--font-head); + font-size: 1.3rem; + font-weight: 800; + color: var(--gold); + margin-bottom: 12px; + } + .pkg-modal-price span { font-size: 0.85rem; font-weight: 500; color: rgba(255,255,255,0.45); } + .pkg-modal-lead { + color: rgba(255,255,255,0.62); + font-size: 0.93rem; + line-height: 1.65; + margin-bottom: 4px; + } + .pkg-modal-divider { + border: none; + border-top: 1px solid rgba(255,255,255,0.12); + margin: 22px 0; + } + .pkg-range { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + } + .pkg-range-col { + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 14px; + padding: 16px 18px; + } + .pkg-range-col h4 { + font-family: var(--font-head); + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--gold); + margin-bottom: 6px; + } + .pkg-range-col .amt { + font-family: var(--font-head); + font-size: 1.15rem; + font-weight: 800; + color: var(--white); + margin-bottom: 10px; + } + .pkg-range-col p { font-size: 0.84rem; color: rgba(255,255,255,0.72); line-height: 1.5; } + .pkg-specs { display: flex; flex-direction: column; } + .pkg-spec { + display: flex; + justify-content: space-between; + gap: 16px; + font-size: 0.9rem; + padding: 11px 0; + border-bottom: 1px solid rgba(255,255,255,0.08); + } + .pkg-spec:last-child { border-bottom: none; } + .pkg-spec .k { color: rgba(255,255,255,0.5); display: flex; align-items: center; gap: 9px; } + .pkg-spec .k i { color: var(--gold); width: 16px; text-align: center; } + .pkg-spec .v { color: var(--white); font-weight: 600; text-align: right; } + .pkg-modal-enquire { margin-top: 26px; } + html.modal-open { overflow: hidden; } + @media (max-width: 520px) { + .pkg-range { grid-template-columns: 1fr; } + .pkg-modal { padding: 30px 22px; } + } + /* CTA strip */ .pricing-cta { background: linear-gradient(145deg, var(--purple-deep) 0%, #2D1860 100%); @@ -430,6 +599,9 @@
  • WordPress website
  • Perfect for businesses taking their first steps into the digital realm.

    + + Enquire about this + @@ -448,6 +620,9 @@
  • Search engine optimisation
  • For businesses ready to expand their reach and establish a stronger online presence.

    + + Enquire about this + @@ -466,6 +641,9 @@
  • Training and support
  • When a standard website simply won't contain your ambitions.

    + + Enquire about this + @@ -486,6 +664,9 @@
  • Store management training
  • Everything needed to open your digital marketplace.

    + + Enquire about this + @@ -505,6 +686,9 @@
  • Launch support
  • For businesses seeking a digital flagship rather than a simple online shop.

    + + Enquire about this + @@ -524,6 +708,9 @@
    The Wizard's Bench
    £40 / hour

    Technical support billed by the hour. Minimum one hour applies. Remote support available - no call-out charge for remote sessions.

    + + Book some support +

    Services include

    @@ -560,6 +747,9 @@
  • Security monitoring
  • Monthly backups
  • + + Enquire about this +
    @@ -574,6 +764,9 @@
  • Priority email support
  • Performance monitoring
  • + + Enquire about this + @@ -589,6 +782,9 @@
  • Priority response times
  • Monthly support allowance
  • + + Enquire about this + @@ -616,5 +812,327 @@ + diff --git a/index.php b/index.php index ad22424..199dd31 100644 --- a/index.php +++ b/index.php @@ -19,6 +19,7 @@ $form_error = isset($_GET['error']) && $_GET['error'] === '1'; + @@ -58,7 +59,7 @@ $form_error = isset($_GET['error']) && $_GET['error'] === '1'; for Small Business
    - Danny the tech-wizard and Astro the cosmic cat + Danny the tech-wizard and Astro the cosmic cat

    Danny the tech-wizard & Astro the cosmic cat

    Friendly website design, technology support and gadget wizardry - with a touch of enchantment. Based in Oxfordshire.

    @@ -73,7 +74,7 @@ $form_error = isset($_GET['error']) && $_GET['error'] === '1';
    - Danny the tech-wizard and Astro the cosmic cat + Danny the tech-wizard and Astro the cosmic cat

    Danny the tech-wizard & Astro the cosmic cat

    @@ -240,6 +241,20 @@ $form_error = isset($_GET['error']) && $_GET['error'] === '1'; + +
    +
    +
    +

    Transparent Pricing

    +

    Spells & Pricing

    +

    From a simple first website to a full ecommerce empire - plus ongoing care plans and ad-hoc tech support. See clear, honest estimates with no hidden fees, and enquire about any package in a click.

    + + View Estimates & Packages + +
    +
    +
    +
    diff --git a/js/main.js b/js/main.js index e58731a..916b5c1 100644 --- a/js/main.js +++ b/js/main.js @@ -8,7 +8,7 @@ if (history.scrollRestoration) { } (function () { var params = new URLSearchParams(window.location.search); - if (!params.has('sent') && !params.has('error')) { + if (!params.has('sent') && !params.has('error') && !params.has('package')) { window.scrollTo(0, 0); if (window.location.hash) { history.replaceState(null, '', window.location.pathname); @@ -16,6 +16,41 @@ if (history.scrollRestoration) { } })(); +// --- Pre-fill contact form when arriving from a pricing package --- +(function () { + var params = new URLSearchParams(window.location.search); + var pkg = params.get('package'); + if (!pkg) return; + + var MESSAGES = { + 'first-spark': "Hi Danny! I'm interested in The First Spark website package (from £250). A little about my project:\n\n", + 'enchanted-workshop': "Hi Danny! I'm interested in The Enchanted Workshop website package (£750–£1,500). A little about my project:\n\n", + 'arcane-forge': "Hi Danny! I'm interested in The Arcane Forge website package (£1,500–£3,000). A little about my project:\n\n", + 'merchants-portal': "Hi Danny! I'm interested in The Merchant's Portal ecommerce package (£3,000–£5,000). A little about my project:\n\n", + 'grand-emporium': "Hi Danny! I'm interested in The Grand Emporium ecommerce package (£5,000–£7,500+). A little about my project:\n\n", + 'wizards-bench': "Hi Danny! I'd like some ad-hoc tech support from The Wizard's Bench (£40/hour). Here's what I need help with:\n\n", + 'minor-ward': "Hi Danny! I'm interested in the Minor Ward care plan (£10/month). A little about my website:\n\n", + 'greater-ward': "Hi Danny! I'm interested in the Greater Ward care plan (£25/month). A little about my website:\n\n", + 'merchants-ward': "Hi Danny! I'm interested in the Merchant's Ward care plan (£50/month). A little about my store:\n\n" + }; + + var field = document.getElementById('message'); + if (field && MESSAGES[pkg]) { + field.value = MESSAGES[pkg]; + } + + var contact = document.getElementById('contact'); + if (contact) { + requestAnimationFrame(function () { contact.scrollIntoView(); }); + } + + if (field && MESSAGES[pkg]) { + field.focus(); + var len = field.value.length; + try { field.setSelectionRange(len, len); } catch (e) {} + } +})(); + // --- Hero starfield --- (function () { const container = document.getElementById('hero-stars'); @@ -167,7 +202,12 @@ function triggerWandEffect(btn) { var btnW = btn.offsetWidth; var btnH = btn.offsetHeight; - var duration = 520; + // Scale swipe speed and sparkle density to the button width so narrow and wide buttons + // feel consistent: constant px/ms sweep speed, and constant spacing between sparkle bursts. + var SPEED = 0.73; // px per ms the swipe travels + var SPACING = 15; // px between sparkle bursts + var duration = Math.max(160, Math.min(btnW / SPEED, 720)); // clamp for tiny / huge buttons + var CLUSTERS = Math.max(3, Math.round(btnW / SPACING)); // fewer bursts on narrow buttons var start = null; var lastCluster = -1; @@ -179,7 +219,7 @@ function triggerWandEffect(btn) { var p = Math.min(elapsed / duration, 1); var x = p * btnW; - var ci = Math.floor(elapsed / 36); + var ci = Math.floor(p * CLUSTERS); if (ci > lastCluster) { lastCluster = ci; spawnSparkleCluster(btn, x, btnH); @@ -227,3 +267,118 @@ window.addEventListener('scroll', function () { : ''; }); }, { passive: true }); + +// --- Mascot "transporter" teleport-in --- +(function () { + var imgs = document.querySelectorAll('.mascot-teleport'); + if (!imgs.length) return; + + var REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (REDUCED) { + imgs.forEach(function (im) { im.classList.remove('mascot-teleport'); im.style.opacity = '1'; }); + return; + } + + var T_CHARS = ['✦', '✧', '✶', '✩', '✨', '⋆']; + var T_COLORS = ['rgba(255,255,255,1)', 'rgba(251,191,36,1)', 'rgba(253,224,120,1)', 'rgba(45,212,191,1)']; // white, gold, teal + + function teleSpark(x, y, life) { + var s = document.createElement('span'); + var size = (0.45 + Math.random() * 0.8).toFixed(2); + var col = T_COLORS[(Math.random() * T_COLORS.length) | 0]; + s.textContent = T_CHARS[(Math.random() * T_CHARS.length) | 0]; + s.style.cssText = 'position:fixed;left:' + x + 'px;top:' + y + 'px;z-index:1200;pointer-events:none;line-height:1;' + + 'font-size:' + size + 'rem;color:' + col + ';text-shadow:0 0 4px ' + col + ',0 0 2px rgba(255,255,255,0.95);' + + 'will-change:transform,opacity;'; + document.body.appendChild(s); + s.animate([ + { transform: 'translate(-50%,-50%) scale(0)' }, + { transform: 'translate(-50%,-50%) scale(' + size + ')', offset: 0.3 }, + { transform: 'translate(-50%,-50%) scale(' + size + ')', offset: 0.7 }, + { transform: 'translate(-50%,-50%) scale(0)' } + ], { duration: life, easing: 'ease-in-out' }).onfinish = function () { s.remove(); }; + } + + function teleport(img) { + var r = img.getBoundingClientRect(); + var START_DELAY = 500; // pause before anything happens + var CHARGE = 1000; // sparkles gather along the bottom before the figure grows + var GROW = 2200; // the bottom-up materialise + var baseShadow = 'drop-shadow(0 8px 24px rgba(0,0,0,0.35))'; + function ease(t) { return t * t; } // ease-in: gentle at the bottom, no slow-down toward the top + + // Keep it invisible (clipped) but opaque from the outset, so nothing flashes during the delay + img.style.opacity = '1'; + img.style.webkitClipPath = img.style.clipPath = 'inset(100% 0 0 0)'; + + setTimeout(function () { + var start = null; + function frame(ts) { + if (!start) start = ts; + var el = ts - start; + + // --- Charge phase: just sparkles building along the bottom edge --- + if (el < CHARGE) { + var cp = el / CHARGE; // 0..1 through the charge + var cn = Math.round(1 + 5 * cp); // build up density toward the grow + for (var c = 0; c < cn; c++) { + var cx = r.left + Math.random() * r.width; + var cy = r.bottom - Math.random() * r.height * 0.06; // hug the base + teleSpark(cx, cy, 480 + Math.random() * 360); + } + requestAnimationFrame(frame); + return; + } + + // --- Grow phase: bottom-up reveal + rising beam --- + var raw = Math.min((el - CHARGE) / GROW, 1); + var p = ease(raw); + + var clip = ((1 - p) * 100).toFixed(2); + img.style.webkitClipPath = img.style.clipPath = 'inset(' + clip + '% 0 0 0)'; + + // Bright near-white energy wash that holds, then settles to the normal drop-shadow + var hx = 1 - p; + var hold = hx * hx * hx * (hx * (hx * 6 - 15) + 10); // smootherstep(1-p): holds, then fades smoothly + var bright = (1 + 1.8 * hold).toFixed(2); // up to ~2.8 — near white-hot + var wash = (1 - 0.55 * hold).toFixed(2); // desaturate early for a whiter look + var glow = (0.85 * hold).toFixed(2); + img.style.filter = baseShadow + + ' drop-shadow(0 0 16px rgba(255,255,255,' + glow + '))' + + ' brightness(' + bright + ') saturate(' + wash + ')'; + + // Sparkle beam sitting on the rising materialisation edge + var lineY = r.bottom - p * r.height; + if (raw < 0.98) { + var n = Math.round(1 + 8 * (1 - p)); // dense at the start (beam low), sparse as it rises + for (var i = 0; i < n; i++) { + var x = r.left + Math.random() * r.width; + var y = (Math.random() < 0.7) + ? lineY + (Math.random() - 0.5) * r.height * 0.12 // tight on the beam line + : lineY - Math.random() * Math.max(0, lineY - r.top); // shimmer in the un-formed area above + y = Math.max(r.top, Math.min(r.bottom, y)); + var vf = (y - r.top) / r.height; // 0 at top, 1 at bottom + teleSpark(x, y, (480 + Math.random() * 360) * (0.4 + 0.6 * vf)); // fade quicker higher up + } + } + + if (raw < 1) { + requestAnimationFrame(frame); + } else { + img.style.webkitClipPath = img.style.clipPath = ''; + img.style.filter = ''; // CSS drop-shadow returns + img.classList.remove('mascot-teleport'); + img.style.opacity = '1'; + } + } + requestAnimationFrame(frame); + }, START_DELAY); + } + + var io = new IntersectionObserver(function (entries) { + entries.forEach(function (e) { + if (e.isIntersecting) { io.unobserve(e.target); teleport(e.target); } + }); + }, { threshold: 0.25 }); + imgs.forEach(function (im) { io.observe(im); }); +})();