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 <noreply@anthropic.com>
This commit is contained in:
Fraxle
2026-06-02 12:38:41 +01:00
co-authored by Claude Opus 4.8
parent 206daaef5d
commit 9303dc7f4d
4 changed files with 724 additions and 6 deletions
+31 -1
View File
@@ -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
================================================ */
+518
View File
@@ -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 @@
<li>WordPress website</li>
</ul>
<p class="pricing-tagline">Perfect for businesses taking their first steps into the digital realm.</p>
<a href="index.php?package=first-spark#contact" class="btn btn-accent pricing-enquire">
<i class="fas fa-envelope"></i> Enquire about this
</a>
</div>
<!-- The Enchanted Workshop -->
@@ -448,6 +620,9 @@
<li>Search engine optimisation</li>
</ul>
<p class="pricing-tagline">For businesses ready to expand their reach and establish a stronger online presence.</p>
<a href="index.php?package=enchanted-workshop#contact" class="btn btn-accent pricing-enquire">
<i class="fas fa-envelope"></i> Enquire about this
</a>
</div>
<!-- The Arcane Forge - Most Popular -->
@@ -466,6 +641,9 @@
<li>Training and support</li>
</ul>
<p class="pricing-tagline">When a standard website simply won't contain your ambitions.</p>
<a href="index.php?package=arcane-forge#contact" class="btn btn-accent pricing-enquire">
<i class="fas fa-envelope"></i> Enquire about this
</a>
</div>
</div>
@@ -486,6 +664,9 @@
<li>Store management training</li>
</ul>
<p class="pricing-tagline">Everything needed to open your digital marketplace.</p>
<a href="index.php?package=merchants-portal#contact" class="btn btn-accent pricing-enquire">
<i class="fas fa-envelope"></i> Enquire about this
</a>
</div>
<!-- The Grand Emporium -->
@@ -505,6 +686,9 @@
<li>Launch support</li>
</ul>
<p class="pricing-tagline">For businesses seeking a digital flagship rather than a simple online shop.</p>
<a href="index.php?package=grand-emporium#contact" class="btn btn-accent pricing-enquire">
<i class="fas fa-envelope"></i> Enquire about this
</a>
</div>
</div>
</div>
@@ -524,6 +708,9 @@
<div class="bench-name">The Wizard's Bench</div>
<div class="bench-rate">£40 <span>/ hour</span></div>
<p class="bench-desc">Technical support billed by the hour. Minimum one hour applies. Remote support available - no call-out charge for remote sessions.</p>
<a href="index.php?package=wizards-bench#contact" class="btn btn-accent bench-enquire">
<i class="fas fa-envelope"></i> Book some support
</a>
</div>
<div class="bench-right">
<p class="pricing-includes-label">Services include</p>
@@ -560,6 +747,9 @@
<li>Security monitoring</li>
<li>Monthly backups</li>
</ul>
<a href="index.php?package=minor-ward#contact" class="btn btn-accent ward-enquire">
<i class="fas fa-envelope"></i> Enquire about this
</a>
</div>
<!-- Greater Ward -->
@@ -574,6 +764,9 @@
<li>Priority email support</li>
<li>Performance monitoring</li>
</ul>
<a href="index.php?package=greater-ward#contact" class="btn btn-accent ward-enquire">
<i class="fas fa-envelope"></i> Enquire about this
</a>
</div>
<!-- Merchant's Ward -->
@@ -589,6 +782,9 @@
<li>Priority response times</li>
<li>Monthly support allowance</li>
</ul>
<a href="index.php?package=merchants-ward#contact" class="btn btn-accent ward-enquire">
<i class="fas fa-envelope"></i> Enquire about this
</a>
</div>
</div>
</div>
@@ -616,5 +812,327 @@
</footer>
<script src="js/main.js"></script>
<script>
/* ===== Package detail modals ===== */
(function () {
var DETAILS = {
'first-spark': {
emoji: '🌱', name: 'The First Spark', price: 'From £250',
lead: 'A clean, professional first website to get your business online quickly and affordably.',
low: { amt: '£250', desc: 'A tidy 13 page, template-based site with your branding, a contact form and one round of revisions.' },
high: { amt: '~£450', desc: 'Up to 5 pages with light customisation, basic SEO setup and a second round of revisions.' },
specs: [
{ i: 'fa-file-lines', k: 'Pages', v: 'Up to 5' },
{ i: 'fa-rotate', k: 'Revisions', v: '2 rounds' },
{ i: 'fa-clock', k: 'Typical turnaround', v: '12 weeks' },
{ i: 'fa-wand-magic-sparkles', k: 'Platform', v: 'WordPress' }
]
},
'enchanted-workshop': {
emoji: '✨', name: 'The Enchanted Workshop', price: '£750 £1,500',
lead: 'A custom-designed site for businesses ready to stand out and grow their audience.',
low: { amt: '£750', desc: '57 pages with a custom design, a blog or news section and social media links.' },
high: { amt: '£1,500', desc: 'Up to 10 pages with photo galleries, analytics and full search-engine optimisation.' },
specs: [
{ i: 'fa-file-lines', k: 'Pages', v: '5 10' },
{ i: 'fa-rotate', k: 'Revisions', v: '3 rounds' },
{ i: 'fa-clock', k: 'Typical turnaround', v: '24 weeks' },
{ i: 'fa-wand-magic-sparkles', k: 'Platform', v: 'WordPress' }
]
},
'arcane-forge': {
emoji: '🔮', name: 'The Arcane Forge', price: '£1,500 £3,000',
lead: 'Advanced layouts and bespoke functionality for businesses with bigger ambitions.',
low: { amt: '£1,500', desc: '810 pages with advanced layouts, enhanced forms and a tailored design.' },
high: { amt: '£3,000', desc: '12+ pages with custom functionality, third-party integrations, performance tuning and training.' },
specs: [
{ i: 'fa-file-lines', k: 'Pages', v: '10+' },
{ i: 'fa-rotate', k: 'Revisions', v: 'Unlimited in scope' },
{ i: 'fa-clock', k: 'Typical turnaround', v: '35 weeks' },
{ i: 'fa-plug', k: 'Integrations', v: 'Included' }
]
},
'merchants-portal': {
emoji: '🛒', name: "The Merchant's Portal", price: '£3,000 £5,000',
lead: 'Everything you need to open a fully working online shop.',
low: { amt: '£3,000', desc: 'Shopify or WooCommerce setup with up to ~50 products, payment gateways and shipping.' },
high: { amt: '£5,000', desc: 'Up to ~200 products with a custom store design, customer accounts and store-management training.' },
specs: [
{ i: 'fa-box', k: 'Products', v: 'Up to ~200' },
{ i: 'fa-credit-card', k: 'Payments', v: 'Gateways configured' },
{ i: 'fa-clock', k: 'Typical turnaround', v: '46 weeks' },
{ i: 'fa-store', k: 'Platform', v: 'Shopify / WooCommerce' }
]
},
'grand-emporium': {
emoji: '⚡', name: 'The Grand Emporium', price: '£5,000 £7,500+',
lead: 'A digital flagship for established and high-volume retailers.',
low: { amt: '£5,000', desc: 'Multi-category store with subscription products and advanced customer accounts.' },
high: { amt: '£7,500+', desc: 'Automation and workflows, marketing integrations, bespoke functionality and full launch support.' },
specs: [
{ i: 'fa-box', k: 'Products', v: 'Unlimited' },
{ i: 'fa-robot', k: 'Automation', v: 'Workflows & marketing' },
{ i: 'fa-clock', k: 'Typical turnaround', v: '610 weeks' },
{ i: 'fa-rocket', k: 'Launch support', v: 'Included' }
]
},
'wizards-bench': {
emoji: '🛠️', name: "The Wizard's Bench", price: '£40 <span>/ hour</span>',
lead: 'Pay-as-you-go technical support for websites, email and office tech. Minimum one hour; remote sessions carry no call-out charge.',
specs: [
{ i: 'fa-bolt', k: 'What an hour covers', v: 'Fixes, updates, email & DNS tweaks' },
{ i: 'fa-hourglass-half', k: 'Minimum', v: '1 hour' },
{ i: 'fa-house-laptop', k: 'Remote', v: 'No call-out charge' },
{ i: 'fa-location-dot', k: 'On-site', v: 'Oxfordshire area' },
{ i: 'fa-file-invoice', k: 'Larger jobs', v: 'Quoted upfront' }
]
},
'minor-ward': {
emoji: '🛡️', name: 'Minor Ward', price: '£10 <span>/ month</span>',
lead: 'Essential upkeep to keep a simple WordPress site safe and current.',
specs: [
{ i: 'fa-arrows-rotate', k: 'Updates', v: 'WordPress core & plugins' },
{ i: 'fa-shield-halved', k: 'Security', v: 'Monitoring' },
{ i: 'fa-database', k: 'Backups', v: 'Monthly' },
{ i: 'fa-headset', k: 'Support time', v: 'Billed at bench rate' },
{ i: 'fa-circle-check', k: 'Best for', v: 'Brochure sites' }
]
},
'greater-ward': {
emoji: '🛡️', name: 'Greater Ward', price: '£25 <span>/ month</span>',
lead: 'Everything in Minor Ward, plus proactive checks and priority support for active business sites.',
specs: [
{ i: 'fa-stethoscope', k: 'Health checks', v: 'Monthly' },
{ i: 'fa-bolt', k: 'Support', v: 'Priority email' },
{ i: 'fa-gauge-high', k: 'Performance', v: 'Monitored' },
{ i: 'fa-clock', k: 'Support allowance', v: '~30 mins / month' },
{ i: 'fa-circle-check', k: 'Best for', v: 'Active business sites' }
]
},
'merchants-ward': {
emoji: '🛡️', name: "Merchant's Ward", price: '£50 <span>/ month</span>',
lead: 'Everything in Greater Ward, plus ecommerce-grade monitoring and maintenance for online stores.',
specs: [
{ i: 'fa-cart-shopping', k: 'Ecommerce', v: 'Store monitoring' },
{ i: 'fa-screwdriver-wrench', k: 'Maintenance', v: 'Store upkeep' },
{ i: 'fa-bolt', k: 'Response', v: 'Priority' },
{ i: 'fa-clock', k: 'Support allowance', v: '~1 hour / month' },
{ i: 'fa-circle-check', k: 'Best for', v: 'Online stores' }
]
}
};
// Build the modal shell once
var overlay = document.createElement('div');
overlay.className = 'pkg-modal-overlay';
overlay.innerHTML =
'<div class="pkg-modal" role="dialog" aria-modal="true" aria-labelledby="pkgModalName">' +
'<button class="pkg-modal-close" type="button" aria-label="Close">&times;</button>' +
'<div class="pkg-modal-emoji"></div>' +
'<div class="pkg-modal-name" id="pkgModalName"></div>' +
'<div class="pkg-modal-price"></div>' +
'<p class="pkg-modal-lead"></p>' +
'<div class="pkg-modal-body"></div>' +
'<a class="btn btn-accent pricing-enquire pkg-modal-enquire"><i class="fas fa-envelope"></i> Enquire about this</a>' +
'</div>';
document.body.appendChild(overlay);
var modalBox = overlay.querySelector('.pkg-modal');
var elEmoji = overlay.querySelector('.pkg-modal-emoji');
var elName = overlay.querySelector('.pkg-modal-name');
var elPrice = overlay.querySelector('.pkg-modal-price');
var elLead = overlay.querySelector('.pkg-modal-lead');
var elBody = overlay.querySelector('.pkg-modal-body');
var elEnquire = overlay.querySelector('.pkg-modal-enquire');
var elClose = overlay.querySelector('.pkg-modal-close');
var lastFocus = null;
var REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var SPARKLE_CHARS = ['✦', '✧', '✶', '✩', '✨'];
var SPARKLE_COLORS = ['rgba(255,255,255,1)', 'rgba(251,191,36,1)', 'rgba(253,224,120,1)', 'rgba(249,115,22,0.95)'];
function dropSparkle(x, y) {
var sp = document.createElement('span');
sp.className = 'pkg-sparkle';
var size = (0.5 + Math.random() * 0.9).toFixed(2);
var col = SPARKLE_COLORS[Math.floor(Math.random() * SPARKLE_COLORS.length)];
sp.textContent = SPARKLE_CHARS[Math.floor(Math.random() * SPARKLE_CHARS.length)];
sp.style.left = x + 'px';
sp.style.top = y + 'px';
sp.style.fontSize = size + 'rem';
sp.style.color = col;
document.body.appendChild(sp);
// Pop outward in a random direction and fade — reads as sparkles bursting off the box edge.
var da = Math.random() * Math.PI * 2;
var dd = 8 + Math.random() * 22;
var ddx = (Math.cos(da) * dd).toFixed(0);
var ddy = (Math.sin(da) * dd).toFixed(0);
sp.animate([
{ opacity: 1, transform: 'translate(-50%, -50%) scale(' + size + ') rotate(0deg)' },
{ opacity: 0, transform: 'translate(calc(-50% + ' + ddx + 'px), calc(-50% + ' + ddy + 'px)) scale(' + (size * 0.25).toFixed(2) + ') rotate(120deg)' }
], { duration: 460 + Math.random() * 300, easing: 'ease-out' }).onfinish = function () { sp.remove(); };
}
// Scatter sparkles around the perimeter of the box as it expands and travels.
function edgeSparkles(box, duration) {
var start = null;
function frame(ts) {
if (!start) start = ts;
var p = Math.min((ts - start) / duration, 1);
var r = box.getBoundingClientRect(); // live rect reflects the running transform
for (var i = 0; i < 2; i++) {
var side = Math.random(), x, y;
if (side < 0.25) { x = r.left + Math.random() * r.width; y = r.top; }
else if (side < 0.5) { x = r.left + Math.random() * r.width; y = r.bottom; }
else if (side < 0.75) { x = r.left; y = r.top + Math.random() * r.height; }
else { x = r.right; y = r.top + Math.random() * r.height; }
dropSparkle(x, y);
}
if (p < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
function openModal(slug, ox, oy) {
var d = DETAILS[slug];
if (!d) return;
elEmoji.textContent = d.emoji;
elName.textContent = d.name;
elPrice.innerHTML = d.price;
elLead.textContent = d.lead;
var html = '';
if (d.low && d.high) {
html += '<div class="pkg-range">' +
'<div class="pkg-range-col"><h4>Lower end</h4><div class="amt">' + d.low.amt + '</div><p>' + d.low.desc + '</p></div>' +
'<div class="pkg-range-col"><h4>Upper end</h4><div class="amt">' + d.high.amt + '</div><p>' + d.high.desc + '</p></div>' +
'</div>';
}
html += '<hr class="pkg-modal-divider"><div class="pkg-specs">';
d.specs.forEach(function (s) {
html += '<div class="pkg-spec"><span class="k"><i class="fas ' + s.i + '"></i> ' + s.k + '</span><span class="v">' + s.v + '</span></div>';
});
html += '</div>';
elBody.innerHTML = html;
elEnquire.setAttribute('href', 'index.php?package=' + slug + '#contact');
lastFocus = document.activeElement;
overlay.classList.add('open');
document.documentElement.classList.add('modal-open');
modalBox.scrollTop = 0;
if (!REDUCED) {
// Measure the modal's resting centre, then glide in along a spiral path from the click point.
var rect = modalBox.getBoundingClientRect();
var cx = rect.left + rect.width / 2;
var cy = rect.top + rect.height / 2;
if (typeof ox !== 'number') { ox = cx; oy = cy; }
var tx = ox - cx, ty = oy - cy; // click offset from the modal's resting centre
var dist = Math.hypot(tx, ty);
var startAng = Math.atan2(ty, tx); // angle of the click point around the centre
var maxR = Math.max(55, Math.min(dist * 0.5, 120)); // capped flourish radius — same feel anywhere
var turns = 1.25; // spins of the flourish — the "flick" of the wand
var duration = 1000;
// A wand-flick spring: the box starts as a dot at the click, springs out into a
// tight, controlled spiral, and glides into the centre as it grows.
// - base glides straight from the wand tip to the centre (no giant orbit)
// - a capped spiral flourish blooms out (sin envelope) then tightens to nothing
// - amplitude is clamped, so edge clicks and centre clicks look the same
var STEPS = 44, frames = [];
for (var i = 0; i <= STEPS; i++) {
var t = i / STEPS;
var g = Math.pow(t, 1.4); // growth + travel-to-centre progress
var baseX = tx * (1 - g); // glide from wand tip -> centre
var baseY = ty * (1 - g);
var amp = maxR * Math.sin(Math.PI * Math.pow(t, 0.7)); // 0 -> bloom -> 0
var ang = startAng + turns * 2 * Math.PI * t;
var px = baseX + Math.cos(ang) * amp;
var py = baseY + Math.sin(ang) * amp;
var sc = 0.05 + 0.95 * g;
frames.push({
transform: 'translate(' + px.toFixed(1) + 'px,' + py.toFixed(1) + 'px) scale(' + sc.toFixed(3) + ')',
opacity: t < 0.08 ? (t / 0.08).toFixed(2) : 1
});
}
modalBox.animate(frames, { duration: duration, easing: 'linear' });
edgeSparkles(modalBox, duration);
}
elClose.focus();
}
// Scatter sparkles evenly around the outline of a rectangle (a quick burst).
function perimeterBurst(r, n) {
var per = 2 * (r.width + r.height);
for (var i = 0; i < n; i++) {
var d = (i / n) * per + (Math.random() - 0.5) * (per / n);
var x, y;
if (d < r.width) { x = r.left + d; y = r.top; }
else if (d < r.width + r.height) { x = r.right; y = r.top + (d - r.width); }
else if (d < 2 * r.width + r.height){ x = r.right - (d - r.width - r.height); y = r.bottom; }
else { x = r.left; y = r.bottom - (d - 2 * r.width - r.height); }
dropSparkle(x, y);
}
}
function closeModal() {
if (!REDUCED && overlay.classList.contains('open')) {
perimeterBurst(modalBox.getBoundingClientRect(), 48); // leave sparkles tracing the box shape
}
overlay.classList.remove('open');
document.documentElement.classList.remove('modal-open');
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
elClose.addEventListener('click', closeModal);
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeModal();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && overlay.classList.contains('open')) closeModal();
});
// Resolve a click origin: real pointer coords, or the element's centre for keyboard activation
function clickPoint(e, el) {
if (e && e.clientX) return [e.clientX, e.clientY];
var r = el.getBoundingClientRect();
return [r.left + r.width / 2, r.top + r.height / 2];
}
// Wire up every card from its enquire link's package slug
document.querySelectorAll('a.pricing-enquire, a.bench-enquire, a.ward-enquire').forEach(function (link) {
var query = ((link.getAttribute('href') || '').split('?')[1] || '').split('#')[0];
var slug = new URLSearchParams(query).get('package');
if (!slug || !DETAILS[slug]) return;
// Insert a "See full details" trigger just before the enquire button
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'pkg-details-btn';
btn.innerHTML = '<i class="fas fa-circle-info"></i> See full details';
btn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
var pt = clickPoint(e, btn);
openModal(slug, pt[0], pt[1]);
});
link.parentNode.insertBefore(btn, link);
// Clicking anywhere on the card (except links/buttons) opens it too
var card = link.closest('.pricing-card, .bench-card, .ward-card');
if (card) {
card.addEventListener('click', function (e) {
if (e.target.closest('a, button')) return;
var pt = clickPoint(e, card);
openModal(slug, pt[0], pt[1]);
});
}
});
})();
</script>
</body>
</html>
+17 -2
View File
@@ -19,6 +19,7 @@ $form_error = isset($_GET['error']) && $_GET['error'] === '1';
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link rel="stylesheet" href="css/style.css">
<noscript><style>.mascot-teleport{opacity:1;}</style></noscript>
</head>
<body>
@@ -58,7 +59,7 @@ $form_error = isset($_GET['error']) && $_GET['error'] === '1';
<span class="hero-title-accent">for Small Business</span>
</h1>
<div class="hero-mascots-mobile">
<img src="img/wizcat4.webp" alt="Danny the tech-wizard and Astro the cosmic cat" class="mascot-img">
<img src="img/wizcat4.webp" alt="Danny the tech-wizard and Astro the cosmic cat" class="mascot-img mascot-teleport">
<p class="hero-caption">Danny the tech-wizard &amp; Astro the cosmic cat</p>
</div>
<p class="hero-subtitle">Friendly website design, technology support and gadget wizardry - with a touch of enchantment. Based in Oxfordshire.</p>
@@ -73,7 +74,7 @@ $form_error = isset($_GET['error']) && $_GET['error'] === '1';
</div>
<div class="hero-mascots">
<div class="mascot-ring">
<img src="img/wizcat4.webp" alt="Danny the tech-wizard and Astro the cosmic cat" class="mascot-img">
<img src="img/wizcat4.webp" alt="Danny the tech-wizard and Astro the cosmic cat" class="mascot-img mascot-teleport">
</div>
<p class="hero-caption">Danny the tech-wizard &amp; Astro the cosmic cat</p>
</div>
@@ -240,6 +241,20 @@ $form_error = isset($_GET['error']) && $_GET['error'] === '1';
</div>
</section>
<!-- ===================== PRICING TEASER ===================== -->
<section id="pricing-teaser" class="pricing-teaser section-pad">
<div class="container">
<div class="section-header reveal">
<p class="section-eyebrow">Transparent Pricing</p>
<h2 class="section-title">Spells &amp; Pricing</h2>
<p class="section-lead">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.</p>
<a href="estimates.php" class="btn btn-primary pricing-teaser-btn">
<i class="fas fa-scroll"></i> View Estimates &amp; Packages
</a>
</div>
</div>
</section>
<!-- ===================== CONTACT ===================== -->
<section id="contact" class="contact section-pad">
<div class="container">
+158 -3
View File
@@ -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); });
})();