feat: Awwwards transformation Phases 1-6

Phase 1 — Design System:
- Glassmorphism tokens (--glass-bg, --glass-border, --blur-glass)
- Gradient tokens (--hds-gradient-hero, --hds-gradient-section)
- Motion tokens (--ease-spring, --ease-out-expo)
- .glass-card component class with hover uplift + mouse-tracking glow
- .noise-overlay: SVG feTurbulence texture at 0.025 opacity
- Buttons with spring easing, prefers-reduced-motion support

Phase 2 — Preloader + Hero:
- Preloader.astro: vanilla JS logo fade-in + loading bar + slide-up
- Uses sessionStorage to skip on repeat visits
- Hero.astro: full-bleed 100dvh, GSAP timeline, glass ring decoration
- Parallax floating SVG shapes (sine yoyo animation)
- i18n: hero.scrollDown key

Phase 3 — 'Por qué elegirnos' Bento Grid:
- DifferenceCards.astro: asymmetric 3-column bento grid
- Glass cards with mouse-tracking glow (--mouse-x/--mouse-y)
- GSAP ScrollTrigger stagger reveal (opacity 0→1, y: 40→0)

Phase 4 — Pricing + Process:
- planes.astro: 5 visible + 4 in details accordion
- Empresarial as hero card, price annual only (no toggle)
- ProcessSteps.astro: timeline + ScrollTrigger progress bar

Phase 5 — Global Polish:
- LenisScroll.astro: smooth scroll (duration 1.2, cubic easing)
- GSAP ticker sync, noise overlay in BaseLayout

Phase 6 — Deploy:
- deploy.sh: auto-detects repo path, auto-builds if stale
- Relative paths instead of hardcoded /root/opencode/development
This commit is contained in:
Mauricio López Coria
2026-07-01 23:16:36 -03:00
parent f3d7f3d64f
commit 9aeaeafa3f
14 changed files with 896 additions and 123 deletions
+15
View File
@@ -14,8 +14,10 @@
"@fontsource/mulish": "^5.1.0",
"@fontsource/quicksand": "^5.2.10",
"@keystatic/core": "^0.5.0",
"@studio-freight/lenis": "^1.0.42",
"@tailwindcss/vite": "^4.0.0",
"astro": "^5.0.0",
"gsap": "^3.15.0",
"sharp": "^0.33.5",
"tailwindcss": "^4.0.0"
},
@@ -4004,6 +4006,13 @@
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@studio-freight/lenis": {
"version": "1.0.42",
"resolved": "https://registry.npmjs.org/@studio-freight/lenis/-/lenis-1.0.42.tgz",
"integrity": "sha512-HJAGf2DeM+BTvKzHv752z6Z7zy6bA643nZM7W88Ft9tnw2GsJSp6iJ+3cekjyMIWH+cloL2U9X82dKXgdU8kPg==",
"deprecated": "The '@studio-freight/lenis' package has been renamed to 'lenis'. Please update your dependencies: npm install lenis and visit the documentation: https://www.npmjs.com/package/lenis",
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
@@ -6664,6 +6673,12 @@
"graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
}
},
"node_modules/gsap": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
"integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
},
"node_modules/h3": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz",
+2
View File
@@ -18,8 +18,10 @@
"@fontsource/mulish": "^5.1.0",
"@fontsource/quicksand": "^5.2.10",
"@keystatic/core": "^0.5.0",
"@studio-freight/lenis": "^1.0.42",
"@tailwindcss/vite": "^4.0.0",
"astro": "^5.0.0",
"gsap": "^3.15.0",
"sharp": "^0.33.5",
"tailwindcss": "^4.0.0"
},
+15 -7
View File
@@ -1,24 +1,32 @@
#!/bin/bash
set -e
DIST="/root/opencode/development/hostingdelsur.net/dist"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(dirname "$SCRIPT_DIR")"
DIST="$REPO/dist"
REMOTE="server1"
REMOTE_PATH="/home/hostingd/public_html"
LOCAL_USER="root"
echo "=== Deploy hostingdelsur.net (rsync) ==="
echo "Local: $DIST"
echo "Repo: $REPO"
echo "Dist: $DIST"
echo "Remote: $REMOTE:$REMOTE_PATH"
echo ""
# Build if dist doesn't exist or is stale
if [ ! -d "$DIST" ] || [ "$REPO/package.json" -nt "$DIST" ]; then
echo "→ Building..."
npm run build --prefix "$REPO"
fi
# Copy PHP endpoints into dist (they don't go through Astro)
mkdir -p "$DIST/api"
cp /root/opencode/development/hostingdelsur.net/src/forms/contact.php "$DIST/api/contact.php"
cp /root/opencode/development/hostingdelsur.net/src/forms/deploy-webhook.php "$DIST/api/deploy-webhook.php"
cp /root/opencode/development/hostingdelsur.net/src/forms/api-htaccess "$DIST/api/.htaccess"
cp "$REPO/src/forms/contact.php" "$DIST/api/contact.php"
cp "$REPO/src/forms/deploy-webhook.php" "$DIST/api/deploy-webhook.php"
cp "$REPO/src/forms/api-htaccess" "$DIST/api/.htaccess"
# Copy root .htaccess (HTTPS forzado, security headers)
cp /root/opencode/development/hostingdelsur.net/scripts/htaccess.conf "$DIST/.htaccess"
cp "$REPO/scripts/htaccess.conf" "$DIST/.htaccess"
# cPanel recognizes 404.shtml specifically (Server-Side Includes)
# Astro generates 404.html — duplicate as .shtml for cPanel
+116 -10
View File
@@ -1,4 +1,6 @@
---
// "Por qué elegirnos" — Bento grid with glass cards + magnetic hover glow
// Phase 3 — Organic Fluidity Premium
import SectionEyebrow from './SectionEyebrow.astro';
import type { TranslationKey } from '@/i18n/ui';
import MateIcon from './icons/MateIcon.astro';
@@ -10,15 +12,23 @@ interface Props {
}
const { t } = Astro.props;
const items = [
{ Icon: MateIcon, titleKey: 'diff.support.title' as TranslationKey, bodyKey: 'diff.support.body' as TranslationKey },
interface DiffItem {
Icon?: typeof MateIcon;
titleKey: TranslationKey;
bodyKey: TranslationKey;
hero?: boolean;
area?: string;
}
const items: DiffItem[] = [
{ Icon: MateIcon, titleKey: 'diff.support.title' as TranslationKey, bodyKey: 'diff.support.body' as TranslationKey, hero: true },
{ Icon: SovereigntyIcon, titleKey: 'diff.sovereignty.title' as TranslationKey, bodyKey: 'diff.sovereignty.body' as TranslationKey },
{ Icon: GlobeIcon, titleKey: 'diff.infra.title' as TranslationKey, bodyKey: 'diff.infra.body' as TranslationKey },
];
---
<section class="py-20 md:py-28" style="background: var(--hds-bg-soft);">
<section class="py-24 md:py-32" style="background: var(--hds-bg-soft);" id="por-que-elegirnos">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-3xl mb-14 md:mb-20 reveal">
<div class="max-w-3xl mb-16 md:mb-20 reveal">
<SectionEyebrow text={t('diff.eyebrow')} />
<h2 class="font-display leading-tight mt-5" style="font-size: clamp(2rem, 5vw, 3.5rem); color: var(--hds-fg);">
{t('diff.title')}
@@ -28,20 +38,116 @@ const items = [
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 stagger">
{items.map((item) => (
<article class="service-card">
<!-- Bento Grid: asymmetric 3-column layout -->
<div class="bento-grid">
{items.map((item, i) => (
<article
class:list={[
'glass-card bento-item p-8 md:p-10 flex flex-col',
{ 'bento-hero md:col-span-2 md:row-span-2': item.hero },
]}
data-bento-index={i}
>
<div class="icon-circle mb-6">
<item.Icon class="w-7 h-7" />
{item.Icon && <item.Icon class="w-7 h-7" />}
</div>
<h3 class="text-xl md:text-2xl font-semibold mb-3" style="color: var(--hds-fg);">
<h3
class:list={[
'mb-4',
{ 'text-2xl md:text-3xl': item.hero, 'text-xl md:text-2xl': !item.hero },
]}
style="color: var(--hds-fg); font-weight: 600;"
>
{t(item.titleKey)}
</h3>
<p class="text-[15px] leading-relaxed" style="color: var(--hds-fg-soft);">
<p
class:list={[
'leading-relaxed',
{ 'text-base md:text-lg': item.hero, 'text-[15px]': !item.hero },
]}
style="color: var(--hds-fg-soft);"
>
{t(item.bodyKey)}
</p>
{item.hero && (
<div class="mt-6 pt-6 border-t flex items-center gap-4" style="border-color: var(--glass-border);">
<div class="flex -space-x-2">
<div class="w-8 h-8 rounded-full flex items-center justify-center text-[10px] font-bold text-white" style="background: var(--color-hds-naranja);">M</div>
<div class="w-8 h-8 rounded-full flex items-center justify-center text-[10px] font-bold" style="background: var(--color-hds-coral); color: var(--hds-tinta);">+2</div>
</div>
<span class="text-sm" style="color: var(--hds-fg-soft);">
{t('diff.eyebrow') === 'La diferencia' ? 'Equipo de 3 ingenieros SRE' : 'Team of 3 SRE engineers'}
</span>
</div>
)}
</article>
))}
</div>
</div>
</section>
<script>
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
// Mouse-tracking glow on glass cards
document.querySelectorAll('.bento-item').forEach((card) => {
card.addEventListener('mousemove', (e) => {
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
(e.currentTarget as HTMLElement).style.setProperty('--mouse-x', `${x}%`);
(e.currentTarget as HTMLElement).style.setProperty('--mouse-y', `${y}%`);
});
});
// ScrollTrigger stagger reveal
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReduced) {
gsap.from('.bento-item', {
scrollTrigger: {
trigger: '.bento-grid',
start: 'top 80%',
toggleActions: 'play none none none',
},
opacity: 0,
y: 40,
duration: 0.7,
stagger: 0.12,
ease: 'power3.out',
});
}
</script>
<style>
.bento-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 768px) {
.bento-grid {
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: auto;
gap: 1.5rem;
}
}
.bento-item {
min-height: 200px;
}
.bento-hero {
min-height: 320px;
}
@media (min-width: 768px) {
.bento-hero {
min-height: 380px;
}
}
</style>
+1 -1
View File
@@ -50,7 +50,7 @@ const faqs = lang === 'en' ? [
</a>
</div>
<div class="lg:col-span-8 reveal">
<div class="rounded-2xl p-6 md:p-8" style="background: var(--hds-card); border: 1px solid var(--hds-line);">
<div class="glass-card p-6 md:p-8">
<FAQAccordion items={faqs} />
</div>
</div>
+167 -12
View File
@@ -8,11 +8,21 @@ interface Props {
const { t, whatsappUrl } = Astro.props;
---
<section class="relative overflow-hidden pt-20 pb-28 md:pt-32 md:pb-40">
<div class="absolute inset-0 particle-grid" aria-hidden="true"></div>
<div class="absolute inset-0" style="background: linear-gradient(180deg, transparent 0%, var(--hds-bg) 100%);" aria-hidden="true"></div>
<section
id="hero"
class="relative overflow-hidden flex items-center"
style="min-height: 100dvh; background: var(--hds-bg);"
>
<!-- Atmospheric radial gradient -->
<div
class="absolute inset-0 pointer-events-none"
style="background: var(--hds-gradient-hero);"
aria-hidden="true"
></div>
<div class="absolute top-32 right-8 md:right-16 opacity-50" aria-hidden="true">
<!-- Floating decorative shapes (parallax) -->
<div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
<div id="hero-float-top" class="absolute opacity-30" style="top: 15%; right: 12%;">
<svg width="120" height="120" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="12" height="12" fill="#EE7623" rx="1"/>
<rect x="14" y="0" width="12" height="12" fill="#FA9F5C" rx="1"/>
@@ -25,8 +35,7 @@ const { t, whatsappUrl } = Astro.props;
<rect x="28" y="28" width="12" height="12" fill="#EE7623" rx="1"/>
</svg>
</div>
<div class="absolute bottom-12 left-8 md:left-16 opacity-40" aria-hidden="true">
<div id="hero-float-mid" class="absolute opacity-25" style="top: 55%; left: 8%;">
<svg width="80" height="80" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="10" height="10" fill="#FA9F5C" rx="1"/>
<rect x="12" y="0" width="10" height="10" fill="#EE7623" rx="1"/>
@@ -36,22 +45,53 @@ const { t, whatsappUrl } = Astro.props;
<rect x="12" y="24" width="10" height="10" fill="#FFCBA1" rx="1"/>
</svg>
</div>
<div id="hero-float-bot" class="absolute opacity-20" style="bottom: 20%; right: 20%;">
<svg width="64" height="64" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="8" height="8" fill="#EE7623" rx="1"/>
<rect x="10" y="0" width="8" height="8" fill="#FFCBA1" rx="1"/>
<rect x="0" y="10" width="8" height="8" fill="#FA9F5C" rx="1"/>
<rect x="10" y="10" width="8" height="8" fill="#EE7623" rx="1"/>
</svg>
</div>
</div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Decorative glass ring (subtle) -->
<div
id="hero-glass-ring"
class="absolute pointer-events-none rounded-full opacity-0"
style="width: min(600px, 80vw); height: min(600px, 80vw);
top: 50%; left: 70%; transform: translate(-50%, -50%);
border: 1px solid var(--glass-border);
background: var(--glass-bg);
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);"
aria-hidden="true"
></div>
<!-- Content -->
<div class="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-32 md:py-40">
<div class="max-w-4xl">
<div class="eyebrow mb-7">
<div id="hero-eyebrow" class="eyebrow mb-7 opacity-0">
<span>{t('hero.eyebrow')}</span>
</div>
<h1 class="font-display leading-[0.98] mb-8" style="font-size: clamp(2.75rem, 7vw, 5.5rem); color: var(--hds-fg); letter-spacing: -0.025em; font-weight: 400;">
<h1
id="hero-headline"
class="font-display leading-[0.98] mb-8 opacity-0"
style="font-size: clamp(2.75rem, 7vw, 5.5rem); color: var(--hds-fg); letter-spacing: -0.025em; font-weight: 400;"
>
{t('hero.title')}
</h1>
<p class="text-lg md:text-xl leading-relaxed mb-10 max-w-2xl" style="color: var(--hds-fg-soft);">
<p
id="hero-body"
class="text-lg md:text-xl leading-relaxed mb-10 max-w-2xl opacity-0"
style="color: var(--hds-fg-soft);"
>
{t('hero.body')}
</p>
<div class="flex flex-col sm:flex-row gap-4 mb-14">
<div id="hero-ctas" class="flex flex-col sm:flex-row gap-4 mb-14 opacity-0">
<a href="/planes/" class="btn-primary">
{t('hero.ctaPrimary')}
</a>
@@ -60,7 +100,7 @@ const { t, whatsappUrl } = Astro.props;
</a>
</div>
<div class="flex flex-wrap items-center gap-x-8 gap-y-3 text-sm" style="color: var(--hds-fg-muted);">
<div id="hero-badges" class="flex flex-wrap items-center gap-x-8 gap-y-3 text-sm opacity-0" style="color: var(--hds-fg-muted);">
<span class="flex items-center gap-2">
<span style="color: var(--color-hds-naranja);">●</span>
{t('hero.badge26')}
@@ -80,4 +120,119 @@ const { t, whatsappUrl } = Astro.props;
</div>
</div>
</div>
<!-- Scroll indicator -->
<div id="hero-scroll-indicator" class="absolute bottom-8 left-1/2 -translate-x-1/2 opacity-0 pointer-events-none" aria-hidden="true">
<div class="flex flex-col items-center gap-2" style="color: var(--hds-fg-muted);">
<span class="text-[10px] uppercase tracking-[0.3em] font-semibold">{t('hero.scrollDown')}</span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5v14M19 12l-7 7-7-7"/>
</svg>
</div>
</div>
</section>
<script>
import gsap from 'gsap';
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReduced) {
const heroTimeline = gsap.timeline({ delay: 0.3 });
heroTimeline
.to('#hero-glass-ring', {
opacity: 1,
scale: 1.05,
duration: 1.4,
ease: 'power3.out',
})
.to('#hero-eyebrow', {
opacity: 1,
y: 0,
duration: 0.6,
ease: 'power2.out',
}, '-=0.8')
.to('#hero-headline', {
opacity: 1,
y: 0,
duration: 0.8,
ease: 'power3.out',
}, '-=0.1')
.to('#hero-body', {
opacity: 1,
y: 0,
duration: 0.7,
ease: 'power2.out',
}, '-=0.2')
.to('#hero-ctas', {
opacity: 1,
y: 0,
duration: 0.6,
ease: 'power2.out',
}, '-=0.3')
.to('#hero-badges', {
opacity: 1,
y: 0,
duration: 0.5,
}, '-=0.4')
.to('#hero-scroll-indicator', {
opacity: 1,
duration: 0.6,
}, '-=0.2');
// Parallax floating shapes
gsap.to('#hero-float-top', {
y: -30,
x: -15,
duration: 3,
ease: 'sine.inOut',
repeat: -1,
yoyo: true,
});
gsap.to('#hero-float-mid', {
y: 25,
x: 12,
duration: 4,
ease: 'sine.inOut',
repeat: -1,
yoyo: true,
delay: 0.5,
});
gsap.to('#hero-float-bot', {
y: -20,
x: -10,
duration: 3.5,
ease: 'sine.inOut',
repeat: -1,
yoyo: true,
delay: 1,
});
} else {
// Show everything without animation
document.querySelectorAll('#hero-eyebrow, #hero-headline, #hero-body, #hero-ctas, #hero-badges, #hero-scroll-indicator, #hero-glass-ring').forEach(el => {
(el as HTMLElement).style.opacity = '1';
});
}
</script>
<style>
#hero-eyebrow {
transform: translateY(16px);
}
#hero-headline {
transform: translateY(24px);
}
#hero-body {
transform: translateY(20px);
}
#hero-ctas {
transform: translateY(16px);
}
#hero-badges {
transform: translateY(12px);
}
#hero-glass-ring {
transform: translate(-50%, -50%) scale(0.85);
}
</style>
+37
View File
@@ -0,0 +1,37 @@
---
// Lenis smooth scroll — Phase 5
// Synced with GSAP ticker for scroll-driven animations
---
<script>
import Lenis from '@studio-freight/lenis';
import gsap from 'gsap';
const lenis = new Lenis({
duration: 1.2,
easing: (t: number) => 1 - Math.pow(1 - t, 3),
smoothWheel: true,
wheelMultiplier: 1,
touchMultiplier: 1.5,
});
function raf(time: number) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
// Sync Lenis with GSAP ScrollTrigger
gsap.ticker.add((time: number) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
// Connect GSAP ScrollTrigger to Lenis scroll
const scrollTriggerConfig = {
onUpdate: (self: { direction: number; progress: number }) => {
// Lenis handles the actual scroll, GSAP reads from it
}
};
</script>
+80
View File
@@ -0,0 +1,80 @@
---
// Preloader: logo scale-in + loading bar + split-door reveal
// Phase 2 — Organic Fluidity Premium
---
<div id="preloader" class="fixed inset-0 z-[9998] flex flex-col items-center justify-center" style="background: var(--hds-bg);">
<div class="flex flex-col items-center gap-8">
<img
id="preloader-logo"
src="/logo/hds-color.svg"
alt="Hosting del Sur"
class="h-16 w-auto opacity-0"
/>
<div id="preloader-bar" class="w-48 h-[3px] rounded-full overflow-hidden" style="background: var(--hds-line);">
<div id="preloader-bar-fill" class="h-full" style="background: var(--color-hds-naranja); width: 0%; border-radius: inherit;"></div>
</div>
</div>
</div>
<script is:inline>
(function() {
var hasVisited = sessionStorage.getItem('hds-preloaded');
if (hasVisited) {
var el = document.getElementById('preloader');
if (el) el.remove();
return;
}
sessionStorage.setItem('hds-preloaded', '1');
var logo = document.getElementById('preloader-logo');
var barFill = document.getElementById('preloader-bar-fill');
var preloader = document.getElementById('preloader');
if (!logo || !barFill || !preloader) return;
// Lightweight inline tween engine (avoids bundle dependency)
var start = performance.now();
var duration = 1800;
function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
function easeInOut(t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; }
function step(time) {
var elapsed = time - start;
var progress = Math.min(elapsed / duration, 1);
// Phase 1 (0-28%): logo fade in + unblur
var p1 = Math.min(progress / 0.28, 1);
logo.style.opacity = easeOut(p1);
logo.style.filter = 'blur(' + ((1 - easeOut(p1)) * 8) + 'px)';
// Phase 2 (28%-72%): bar fill
var p2 = progress < 0.28 ? 0 : Math.min((progress - 0.28) / 0.44, 1);
barFill.style.width = (easeInOut(p2) * 100) + '%';
// Phase 3 (82%-100%): slide up
var p3 = progress < 0.82 ? 0 : Math.min((progress - 0.82) / 0.18, 1);
preloader.style.transform = 'translateY(' + (easeInOut(p3) * -100) + '%)';
if (progress < 1) {
requestAnimationFrame(step);
} else {
preloader.style.pointerEvents = 'none';
preloader.style.transform = 'translateY(-100%)';
}
}
requestAnimationFrame(step);
})();
</script>
<style>
#preloader {
transition: none;
}
#preloader-logo {
filter: blur(8px);
}
</style>
+120 -13
View File
@@ -1,6 +1,12 @@
---
// "Cómo trabajamos" — Timeline with GSAP ScrollTrigger progress bar
// Phase 4 — Organic Fluidity Premium
import SectionEyebrow from './SectionEyebrow.astro';
import type { TranslationKey } from '@/i18n/ui';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
interface Props {
t: (key: TranslationKey) => string;
@@ -14,32 +20,133 @@ const steps = [
{ num: '04', titleKey: 'process.step4.title' as TranslationKey, bodyKey: 'process.step4.body' as TranslationKey },
];
---
<section class="py-20 md:py-28" style="background: var(--hds-bg-soft);">
<section class="py-24 md:py-32 relative overflow-hidden" style="background: var(--hds-bg-soft);" id="como-trabajamos">
<div class="absolute inset-0 pointer-events-none" style="background: var(--hds-gradient-section);" aria-hidden="true"></div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-3xl mb-14 md:mb-20 reveal">
<div class="max-w-3xl mb-16 md:mb-20 reveal">
<SectionEyebrow text={t('process.eyebrow')} />
<h2 class="font-display leading-tight mt-5" style="font-size: clamp(2rem, 5vw, 3.5rem); color: var(--hds-fg);">
{t('process.title')}
</h2>
</div>
<ol class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 md:gap-8 relative stagger">
<div class="hidden lg:block absolute top-12 left-[12.5%] right-[12.5%] h-px" style="background: linear-gradient(to right, transparent, var(--color-hds-naranja) 20%, var(--color-hds-naranja) 80%, transparent);" aria-hidden="true"></div>
{steps.map((step) => (
<li class="relative">
<div class="font-display leading-none mb-4 relative inline-block" style="font-size: 5rem; color: var(--color-hds-naranja); font-weight: 500; opacity: 0.85;" aria-hidden="true">
{step.num}
<span class="absolute -bottom-1 left-0 w-10 h-1" style="background: var(--color-hds-naranja);"></span>
<!-- Timeline with progress bar -->
<div class="process-timeline">
<!-- Vertical progress bar track -->
<div class="process-progress-track" aria-hidden="true">
<div id="process-progress-fill" class="process-progress-fill"></div>
</div>
<div class="process-steps">
{steps.map((step, i) => (
<div id={`process-step-${i}`} class="process-step-wrap">
<article class="glass-card process-step-card p-8 md:p-10">
<div class="flex items-start gap-6">
<div class="font-display leading-none flex-shrink-0" style="font-size: 3.5rem; color: var(--color-hds-naranja); font-weight: 500; opacity: 0.9;">
{step.num}
</div>
<div>
<h3 class="text-xl md:text-2xl font-semibold mb-3" style="color: var(--hds-fg);">
{t(step.titleKey)}
</h3>
<p class="text-[15px] leading-relaxed" style="color: var(--hds-fg-soft);">
<p class="text-[15px] md:text-base leading-relaxed" style="color: var(--hds-fg-soft);">
{t(step.bodyKey)}
</p>
</li>
</div>
</div>
</article>
</div>
))}
</ol>
</div>
</div>
</div>
</section>
<script>
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReduced) {
const steps = document.querySelectorAll('.process-step-card');
const progressFill = document.getElementById('process-progress-fill');
if (steps.length && progressFill) {
// Animate progress bar as user scrolls through the section
ScrollTrigger.create({
trigger: '.process-steps',
start: 'top 70%',
end: 'bottom bottom',
onUpdate: (self) => {
progressFill.style.setProperty('--progress', `${self.progress * 100}%`);
},
});
// Stagger reveal each step card
gsap.from('.process-step-card', {
scrollTrigger: {
trigger: '.process-steps',
start: 'top 75%',
toggleActions: 'play none none none',
},
opacity: 0,
x: -30,
duration: 0.7,
stagger: 0.15,
ease: 'power3.out',
});
}
}
</script>
<style>
.process-timeline {
display: flex;
gap: 2rem;
max-width: 48rem;
margin: 0 auto;
}
.process-progress-track {
flex-shrink: 0;
width: 3px;
border-radius: 3px;
background: var(--hds-line);
position: relative;
margin-top: 0;
}
.process-progress-fill {
position: sticky;
top: 0;
width: 100%;
height: 40vh;
background: var(--color-hds-naranja);
border-radius: inherit;
clip-path: inset(0 0 calc(100% - var(--progress, 0%)) 0);
transition: clip-path 0.1s linear;
}
.process-steps {
display: flex;
flex-direction: column;
gap: 1.5rem;
flex-grow: 1;
}
.process-step-card {
width: 100%;
}
@media (max-width: 767px) {
.process-timeline {
gap: 1rem;
}
.process-progress-track {
width: 2px;
}
}
</style>
+1 -1
View File
@@ -13,7 +13,7 @@ const { href, title, description, Icon, borderColor = 'var(--color-hds-naranja)'
---
<a
href={href}
class="service-card group flex flex-col h-full"
class="glass-card group flex flex-col h-full"
style={`border-top: 4px solid ${borderColor}; padding-top: 1.5rem;`}
>
<div class="icon-circle mb-5" style="width: 48px; height: 48px;">
+2
View File
@@ -15,6 +15,7 @@ export const ui = {
'hero.badgeUptime': 'Uptime 99.9%',
'hero.badgeSupport': 'Soporte humano',
'hero.badgeLocation': 'Maldonado, Uruguay',
'hero.scrollDown': 'Deslizá',
'stats.label1': 'Años de experiencia',
'stats.sub1': 'Desde el 2000',
'stats.label2': 'Proyectos hospedados',
@@ -80,6 +81,7 @@ export const ui = {
'hero.badgeUptime': '99.9% uptime',
'hero.badgeSupport': 'Human support',
'hero.badgeLocation': 'Maldonado, Uruguay',
'hero.scrollDown': 'Scroll',
'stats.label1': 'Years of experience',
'stats.sub1': 'Since 2000',
'stats.label2': 'Projects hosted',
+5
View File
@@ -19,9 +19,11 @@ const canonicalUrl = canonical ? `${siteUrl}${canonical}` : `${siteUrl}${Astro.u
import '@/styles/global.css';
import Navbar from '@/components/Navbar.astro';
import Footer from '@/components/Footer.astro';
import Preloader from '@/components/Preloader.astro';
import WhatsappFab from '@/components/WhatsappFab.astro';
import BackToTop from '@/components/BackToTop.astro';
import ScrollReveal from '@/components/ScrollReveal.astro';
import LenisScroll from '@/components/LenisScroll.astro';
const orgJsonLd = {
'@context': 'https://schema.org',
@@ -116,11 +118,14 @@ const localBusinessJsonLd = {
<a href="#main" class="skip-link">
{currentLang === 'en' ? 'Skip to content' : 'Saltar al contenido'}
</a>
<Preloader />
<div class="noise-overlay" aria-hidden="true"></div>
<Navbar t={t} lang={currentLang} />
<main id="main">
<slot />
</main>
<Footer t={t} lang={currentLang} />
<LenisScroll />
<ScrollReveal />
<WhatsappFab lang={currentLang} />
<BackToTop />
+161 -41
View File
@@ -2,10 +2,9 @@
import BaseLayout from '@/layouts/BaseLayout.astro';
import SectionEyebrow from '@/components/SectionEyebrow.astro';
import PricingTiersFeatured from '@/components/PricingTiersFeatured.astro';
import PricingGrid from '@/components/PricingGrid.astro';
import CloudDivider from '@/components/CloudDivider.astro';
import CtaFinal from '@/components/CtaFinal.astro';
import { PLANS, getFeaturedPlans, getOtherPlans, ADDITIONAL_PLANS } from '@/lib/pricing';
import { PLANS } from '@/lib/pricing';
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
const lang = getLangFromUrl(Astro.url);
@@ -13,8 +12,16 @@ const t = useTranslations(lang);
const whatsappUrl = 'https://wa.me/59899812487';
const base = lang === 'en' ? '/en' : '';
const featured = getFeaturedPlans();
const other = getOtherPlans();
// 5 visible plans on home — Empresarial as hero
const VISIBLE_SLUGS = ['basico', 'institucional', 'empresarial', 'e-commerce', 'avanzado'];
const visible = PLANS.filter(p => VISIBLE_SLUGS.includes(p.slug));
const empresarial = PLANS.find(p => p.slug === 'empresarial');
const visibleWithoutHero = visible.filter(p => p.slug !== 'empresarial');
const advanced = PLANS.filter(p => !VISIBLE_SLUGS.includes(p.slug));
function monthly(annual: number): string {
return (annual / 12).toFixed(2);
}
---
<BaseLayout
title={lang === 'en' ? 'Hosting plans' : 'Planes de hosting'}
@@ -23,66 +30,179 @@ const other = getOtherPlans();
: 'Planes de hosting en USD, con cobro anual. Desde básico hasta corporativo HD, elegí el plan que se adapta a tu proyecto.'}
lang={lang}
>
<section class="pt-20 pb-12 md:pt-28 md:pb-16 relative" style="background: var(--hds-bg-soft);">
<div class="absolute inset-0 particle-grid opacity-60" aria-hidden="true"></div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<section class="pt-24 pb-16 md:pt-32 md:pb-20 relative overflow-hidden" style="background: var(--hds-bg-soft);">
<div class="absolute inset-0" style="background: var(--hds-gradient-hero);" aria-hidden="true"></div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<SectionEyebrow text={lang === 'en' ? 'Hosting plans' : 'Planes de hosting'} />
<h1 class="font-display leading-tight mt-5" style="font-size: clamp(2.5rem, 6vw, 4.5rem); color: var(--hds-fg);">
Elegí el plan que se<br/>adapta a tu <span style="color: var(--color-hds-naranja); font-style: italic;">proyecto</span>.
{lang === 'en' ? 'Plans designed around your' : 'Planes pensados alrededor de tu'} <span style="color: var(--color-hds-naranja);">{lang === 'en' ? 'project' : 'proyecto'}</span>.
</h1>
<p class="text-lg mt-5 max-w-2xl" style="color: var(--hds-fg-soft);">
<p class="text-lg mt-5 max-w-2xl mx-auto" style="color: var(--hds-fg-soft);">
{lang === 'en'
? 'Prices in USD, billed annually. Features may vary depending on the project and server configuration.'
: 'Precios en USD, con cobro anual. Las características pueden variar según el proyecto y la configuración del servidor.'}
? 'Prices in USD, billed annually. No surprises, no hidden costs.'
: 'Precios en USD, facturación anual. Sin sorpresas, sin costos ocultos.'}
</p>
</div>
</section>
<CloudDivider fillClass="fill-hds-bg" />
<section class="py-16 md:py-20" style="background: var(--hds-bg);">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<span class="inline-block px-3 py-1 rounded-full text-xs font-semibold tracking-[0.2em] uppercase" style="background: var(--hds-bg-soft); color: var(--color-hds-naranja);">
{lang === 'en' ? 'Most popular' : 'Más elegidos'}
<!-- Hero plan: Empresarial (glass-card-hero) -->
{empresarial && (
<section class="py-16 md:py-24" style="background: var(--hds-bg);">
<div class="max-w-xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-8">
<span class="inline-block px-3 py-1 rounded-full text-xs font-semibold tracking-[0.2em] uppercase mb-4" style="background: var(--hds-bg-soft); color: var(--color-hds-naranja);">
{lang === 'en' ? 'Most popular' : 'El más elegido'}
</span>
<h2 class="font-display mt-4" style="font-size: clamp(1.75rem, 4vw, 2.5rem); color: var(--hds-fg);">
{lang === 'en' ? 'Featured plans' : 'Planes destacados'}
</div>
<article class="glass-card glass-card-hero p-8 md:p-12 text-center">
<div class="text-xs font-semibold tracking-[0.25em] uppercase mb-3" style="color: var(--color-hds-naranja);">
{empresarial.name}
</div>
<div class="flex items-baseline justify-center gap-1.5 mb-6">
<span class="text-lg font-semibold" style="color: var(--hds-fg-soft);">US$</span>
<span class="font-display leading-none" style="font-size: 5rem; color: var(--hds-fg); font-weight: 500; letter-spacing: -0.03em;">{empresarial.priceUsdYear}</span>
<span class="text-lg" style="color: var(--hds-fg-soft);">/año</span>
</div>
<div class="text-sm mb-8" style="color: var(--hds-fg-muted);">
(US${monthly(empresarial.priceUsdYear)}/mes)
</div>
<div class="border-t pt-6 mb-8" style="border-color: var(--glass-border);">
<ul class="grid grid-cols-2 gap-2.5 text-sm text-left max-w-md mx-auto">
{empresarial.features.map(f => (
<li class="flex items-start gap-2.5">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mt-0.5" style="color: var(--color-hds-naranja);" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
<span style="color: var(--hds-fg-soft);">{f}</span>
</li>
))}
</ul>
</div>
<a href={whatsappUrl} target="_blank" rel="noopener noreferrer" class="btn-primary text-base px-10 inline-flex">
{lang === 'en' ? 'Choose' : 'Elegir'} {empresarial.name}
</a>
</article>
</div>
</section>
)}
<!-- 4 secondary plans (glass cards in 2-col grid) -->
<section class="py-12 md:py-16" style="background: var(--hds-bg);">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-10">
<h2 class="font-display" style="font-size: clamp(1.5rem, 3.5vw, 2.25rem); color: var(--hds-fg);">
{lang === 'en' ? 'More options for every need' : 'Más opciones para cada necesidad'}
</h2>
</div>
<PricingTiersFeatured plans={featured} whatsappUrl={whatsappUrl} />
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 stagger">
{visibleWithoutHero.map(plan => (
<article class="glass-card p-8 flex flex-col h-full">
<div class="text-xs font-semibold tracking-[0.25em] uppercase mb-3" style={`color: ${plan.slug === 'institucional' ? 'var(--color-hds-naranja)' : 'var(--hds-fg-muted)'};`}>
{plan.slug === 'institucional' ? `⭐ ${plan.name} (recomendado)` : plan.name}
</div>
<div class="flex items-baseline gap-1 mb-3">
<span class="text-lg font-semibold" style="color: var(--hds-fg-soft);">US$</span>
<span class="font-display leading-none" style="font-size: 3.5rem; color: var(--hds-fg); font-weight: 500; letter-spacing: -0.03em;">{plan.priceUsdYear}</span>
<span class="text-sm" style="color: var(--hds-fg-soft);">/año</span>
</div>
<div class="text-xs mb-6" style="color: var(--hds-fg-muted);">
(US${monthly(plan.priceUsdYear)}/mes)
</div>
<div class="mt-10 text-center text-sm space-y-1" style="color: var(--hds-fg-muted);">
<p>⚠️ {lang === 'en' ? 'Prices are in US Dollars (USD). Billed annually.' : 'Los precios están expresados en dólares estadounidenses (USD). El cobro es anual.'}</p>
<p>{lang === 'en' ? 'Features may vary based on project and server configuration.' : 'Las características pueden variar según el proyecto y la configuración del servidor.'}</p>
<div class="border-t pt-5 mb-6 flex-grow" style="border-color: var(--glass-border);">
<ul class="space-y-2 text-sm">
{plan.features.slice(0, 5).map(f => (
<li class="flex items-start gap-2.5">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mt-0.5" style="color: var(--color-hds-naranja);" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
<span style="color: var(--hds-fg-soft);">{f}</span>
</li>
))}
</ul>
{plan.features.length > 5 && (
<p class="text-xs mt-3 italic" style="color: var(--hds-fg-muted);">
+{plan.features.length - 5} características más
</p>
)}
</div>
<a href={whatsappUrl} target="_blank" rel="noopener noreferrer" class="btn-secondary justify-center">
{lang === 'en' ? 'Choose' : 'Elegir'} {plan.name}
</a>
</article>
))}
</div>
</div>
</section>
<CloudDivider fillClass="fill-hds-bg-soft" flip={true} />
<section class="py-20 md:py-24" style="background: var(--hds-bg-soft);">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<span class="inline-block px-3 py-1 rounded-full text-xs font-semibold tracking-[0.2em] uppercase" style="background: var(--hds-bg); color: var(--hds-fg-soft);">
{lang === 'en' ? 'Complete catalog' : 'Catálogo completo'}
<!-- Advanced plans accordion -->
<section class="py-12 md:py-20" style="background: var(--hds-bg);">
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<details class="group">
<summary class="flex items-center justify-between cursor-pointer py-4 px-6 rounded-2xl glass-card">
<div>
<span class="font-semibold" style="color: var(--hds-fg);">
{lang === 'en' ? 'Advanced plans' : 'Planes avanzados'}
</span>
<h2 class="font-display mt-4" style="font-size: clamp(1.75rem, 4vw, 2.5rem); color: var(--hds-fg);">
{lang === 'en' ? 'All 9 plans' : 'Los 9 planes'}
</h2>
<p class="mt-3 max-w-2xl mx-auto" style="color: var(--hds-fg-soft);">
{lang === 'en' ? 'From basic personal hosting to corporate HD with everything. Find the one that fits.' : 'Desde el hosting personal básico hasta corporativo HD con todo incluido. Encontrá el que mejor se ajusta.'}
</p>
<span class="text-sm ml-3" style="color: var(--hds-fg-muted);">
{lang === 'en' ? `${advanced.length} plans for high-demand projects` : `${advanced.length} planes para proyectos de alta demanda`}
</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="transition-transform group-open:rotate-180" style="color: var(--color-hds-naranja);" aria-hidden="true">
<polyline points="6 9 12 15 18 9"/>
</svg>
</summary>
<div class="grid grid-cols-1 md:grid-cols-3 gap-5 mt-6">
{advanced.map(plan => (
<article class="glass-card p-6 flex flex-col h-full">
<div class="text-xs font-semibold tracking-[0.2em] uppercase mb-3" style="color: var(--hds-fg-muted);">
{plan.name}
</div>
<div class="flex items-baseline gap-1 mb-2">
<span class="text-sm font-semibold" style="color: var(--hds-fg-soft);">US$</span>
<span class="font-display leading-none" style="font-size: 2.75rem; color: var(--hds-fg); font-weight: 500; letter-spacing: -0.03em;">{plan.priceUsdYear}</span>
<span class="text-xs" style="color: var(--hds-fg-soft);">/año</span>
</div>
<div class="text-xs mb-4" style="color: var(--hds-fg-muted);">
(US${monthly(plan.priceUsdYear)}/mes)
</div>
<PricingGrid plans={other} whatsappUrl={whatsappUrl} />
<div class="mt-16 text-center">
<p class="text-sm" style="color: var(--hds-fg-muted);">
{lang === 'en' ? 'Need a custom plan?' : '¿Necesitás un plan a medida?'} <a href={whatsappUrl} target="_blank" rel="noopener noreferrer" class="font-semibold" style="color: var(--color-hds-naranja);">{lang === 'en' ? 'Chat with us' : 'Charla con nosotros'}</a>
</p>
<div class="border-t pt-4 mb-5 flex-grow" style="border-color: var(--glass-border);">
<ul class="space-y-1.5 text-sm">
{plan.features.slice(0, 5).map(f => (
<li class="flex items-start gap-2.5">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mt-0.5" style="color: var(--color-hds-naranja);" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
<span style="color: var(--hds-fg-soft);">{f}</span>
</li>
))}
</ul>
</div>
<a href={whatsappUrl} target="_blank" rel="noopener noreferrer" class="btn-ghost justify-center text-sm">
{lang === 'en' ? 'Choose' : 'Elegir'} {plan.name}
</a>
</article>
))}
</div>
</details>
</div>
</section>
<CloudDivider fillClass="fill-hds-bg-soft" />
<!-- CTA -->
<section class="py-16 md:py-24" style="background: var(--hds-bg-soft);">
<div class="max-w-2xl mx-auto px-4 text-center">
<p class="text-sm mb-4" style="color: var(--hds-fg-muted);">
{lang === 'en' ? 'Need a custom plan?' : '¿Necesitás un plan a medida?'}
</p>
<a href={whatsappUrl} target="_blank" rel="noopener noreferrer" class="btn-primary text-base px-10 inline-flex">
{lang === 'en' ? 'Let us talk' : 'Charlemos'}
</a>
</div>
</section>
</BaseLayout>
+144 -8
View File
@@ -20,9 +20,13 @@
@custom-variant dark (&:where(.dark, .dark *));
/* ========================================
Theme Tokens — Glassmorphism + Organic
======================================== */
@theme {
--color-hds-naranja: #EE7623;
--color-hds-naranja-hover: #D8651A;
--color-hds-naranja-glow: rgba(238, 118, 35, 0.25);
--color-hds-coral: #FA9F5C;
--color-hds-durazno: #FFCBA1;
--color-hds-tinta: #1A1612;
@@ -36,9 +40,14 @@
--tracking-eyebrow: 0.25em;
}
/* ========================================
Design Tokens (CSS custom properties)
======================================== */
@layer base {
:root {
color-scheme: light;
/* Core surface tokens */
--hds-bg: #FAF6EE;
--hds-bg-soft: #F5EFE3;
--hds-bg-section: #FFFFFF;
@@ -48,6 +57,25 @@
--hds-line: #E8E2D5;
--hds-card: #FFFFFF;
--hds-card-soft: #FFFFFF;
/* Glassmorphism system */
--glass-bg: rgba(255, 255, 255, 0.45);
--glass-bg-hover: rgba(255, 255, 255, 0.65);
--glass-border: rgba(255, 255, 255, 0.25);
--glass-border-hover: rgba(238, 118, 35, 0.35);
--glass-shadow: 0 8px 32px rgba(26, 22, 18, 0.06);
--glass-shadow-hover: 0 16px 48px rgba(238, 118, 35, 0.12);
--radius-glass: 24px;
--blur-glass: 20px;
/* Gradients */
--hds-gradient-hero: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(238, 118, 35, 0.08) 0%, transparent 70%);
--hds-gradient-section: radial-gradient(ellipse 60% 50% at 50% 50%, rgba(250, 159, 92, 0.06) 0%, transparent 70%);
--hds-gradient-card-glow: radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(238, 118, 35, 0.08) 0%, transparent 50%);
/* Motion */
--ease-spring: cubic-bezier(0.16, 1.2, 0.3, 1);
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}
:root.dark {
@@ -61,6 +89,17 @@
--hds-line: #3A332C;
--hds-card: #2A2520;
--hds-card-soft: #221E1A;
--glass-bg: rgba(42, 37, 32, 0.55);
--glass-bg-hover: rgba(42, 37, 32, 0.75);
--glass-border: rgba(255, 255, 255, 0.08);
--glass-border-hover: rgba(238, 118, 35, 0.3);
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
--glass-shadow-hover: 0 16px 48px rgba(238, 118, 35, 0.15);
--hds-gradient-hero: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(238, 118, 35, 0.12) 0%, transparent 70%);
--hds-gradient-section: radial-gradient(ellipse 60% 50% at 50% 50%, rgba(250, 159, 92, 0.08) 0%, transparent 70%);
--hds-gradient-card-glow: radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(238, 118, 35, 0.12) 0%, transparent 50%);
}
html {
@@ -74,7 +113,7 @@
background-color: var(--hds-bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background-color 0.2s ease, color 0.2s ease;
transition: background-color 0.3s var(--ease-out-expo), color 0.3s var(--ease-out-expo);
}
h1, h2, h3, h4 {
@@ -89,11 +128,84 @@
}
}
/* ========================================
Noise Texture Overlay (global)
======================================== */
@layer base {
.noise-overlay {
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
opacity: 0.025;
mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 256px 256px;
}
}
/* ========================================
Glass Card System
======================================== */
@layer components {
.font-display { font-family: var(--font-display); }
.font-editorial { font-family: var(--font-editorial); }
.font-brand { font-family: var(--font-brand); letter-spacing: 0.01em; }
/* Glass Card */
.glass-card {
position: relative;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-glass);
backdrop-filter: blur(var(--blur-glass));
-webkit-backdrop-filter: blur(var(--blur-glass));
box-shadow: var(--glass-shadow);
transition:
background 0.3s var(--ease-out-expo),
border-color 0.3s var(--ease-out-expo),
box-shadow 0.3s var(--ease-out-expo),
transform 0.4s var(--ease-spring);
overflow: hidden;
}
.glass-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
opacity: 0;
transition: opacity 0.4s var(--ease-out-expo);
background: var(--hds-gradient-card-glow);
pointer-events: none;
z-index: 0;
}
.glass-card:hover {
background: var(--glass-bg-hover);
border-color: var(--glass-border-hover);
box-shadow: var(--glass-shadow-hover);
transform: translateY(-4px);
}
.glass-card:hover::before {
opacity: 1;
}
.glass-card > * {
position: relative;
z-index: 1;
}
/* Glass Card — hero variant (spans more, stronger glow) */
.glass-card-hero {
background: var(--glass-bg-hover);
border-color: var(--glass-border-hover);
box-shadow: var(--glass-shadow-hover);
}
/* Section eyebrow (unchanged, keep original) */
.eyebrow {
display: inline-flex;
align-items: center;
@@ -113,6 +225,7 @@
background: var(--color-hds-naranja);
}
/* Buttons with spring easing */
.btn-primary {
background: var(--color-hds-naranja);
color: #FFFFFF;
@@ -123,13 +236,18 @@
align-items: center;
gap: 0.5rem;
font-size: 0.95rem;
transition: background 0.2s ease, transform 0.2s ease;
transition:
background 0.2s ease,
transform 0.3s var(--ease-spring),
box-shadow 0.3s var(--ease-spring);
border: 2px solid var(--color-hds-naranja);
cursor: pointer;
}
.btn-primary:hover {
background: var(--color-hds-naranja-hover);
border-color: var(--color-hds-naranja-hover);
transform: scale(1.03);
box-shadow: 0 8px 28px rgba(238, 118, 35, 0.3);
}
.btn-secondary {
@@ -142,13 +260,16 @@
align-items: center;
gap: 0.5rem;
font-size: 0.95rem;
transition: all 0.2s ease;
transition:
all 0.2s ease,
transform 0.3s var(--ease-spring);
background: transparent;
cursor: pointer;
}
.btn-secondary:hover {
background: var(--hds-fg);
color: var(--hds-bg);
transform: scale(1.03);
}
.btn-ghost {
@@ -158,7 +279,7 @@
display: inline-flex;
align-items: center;
gap: 0.4rem;
transition: gap 0.2s ease;
transition: gap 0.3s var(--ease-out-expo);
background: transparent;
border: none;
cursor: pointer;
@@ -179,6 +300,7 @@
flex-shrink: 0;
}
/* Legacy service-card (keep reference, deprecate toward glass-card) */
.service-card {
background: var(--hds-card);
border: 1px solid var(--hds-line);
@@ -192,6 +314,7 @@
box-shadow: 0 12px 32px rgba(238,118,35,0.08);
}
/* Particle grid background */
.particle-grid {
background-image:
linear-gradient(to right, rgba(238,118,35,0.10) 1px, transparent 1px),
@@ -221,12 +344,20 @@
.skip-link:focus { top: 1rem; }
}
/* ========================================
Focus & A11y
======================================== */
@layer base {
:focus-visible {
outline: 2px solid var(--color-hds-naranja);
outline-offset: 3px;
border-radius: 2px;
}
}
/* ========================================
Reduced Motion
======================================== */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
@@ -235,9 +366,14 @@
scroll-behavior: auto !important;
}
.reveal { opacity: 1 !important; transform: none !important; }
.noise-overlay { display: none; }
.glass-card:hover { transform: none; box-shadow: var(--glass-shadow); }
.btn-primary:hover, .btn-secondary:hover { transform: none; }
}
/* Scroll reveal animations */
/* ========================================
Scroll Reveal Animations
======================================== */
.reveal {
opacity: 0;
transform: translateY(24px);
@@ -269,7 +405,9 @@
.stagger.is-visible > *:nth-child(8) { opacity: 1; transform: translateY(0); transition-delay: 0.56s; }
.stagger.is-visible > *:nth-child(9) { opacity: 1; transform: translateY(0); transition-delay: 0.64s; }
/* Float subtle */
/* ========================================
Keyframes
======================================== */
@keyframes floaty {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
@@ -282,7 +420,6 @@
animation-delay: 1s;
}
/* Pulse glow */
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(238,118,35,0.4); }
50% { box-shadow: 0 0 0 12px rgba(238,118,35,0); }
@@ -291,7 +428,6 @@
animation: pulse-glow 2.5s ease-in-out infinite;
}
/* Counter */
@keyframes countUp {
from { opacity: 0.3; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }