Files
fitlabs.dev/docs/superpowers/plans/2026-03-14-fitlabs-landing.md
Felipe Arentsen f11adceceb feat: complete FIT Labs landing page
- Astro static site with Tailwind CSS v4
- 6 sections: Hero (gradient mesh), About, Capabilities, Differentiator, CTA, Footer
- Sticky navbar with mobile menu
- Scroll reveal animations
- SEO meta tags and structured data
- Docker + nginx config for Coolify deployment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 12:26:00 -06:00

30 KiB
Raw Blame History

FIT Labs Landing Page — Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build and deploy a single-page landing for fitlabs.dev — FactorIT's AI innovation incubator.

Architecture: Astro static site with Tailwind CSS. 6 sections in a single index.astro page composed from Astro components. No client-side framework — vanilla JS only for navbar scroll, scroll-reveal animations, and gradient mesh. Deployed via Docker (nginx) on Coolify.

Tech Stack: Astro 5.x, Tailwind CSS 4.x, Lucide Icons (SVG inline), Docker + nginx

Spec: docs/superpowers/specs/2026-03-14-fitlabs-landing-design.md


File Structure

fitlabs.dev/
├── public/
│   ├── LogoFIT.png              # Logo (already exists at project root)
│   └── favicon.svg              # Favicon
├── src/
│   ├── layouts/
│   │   └── Layout.astro         # Base HTML: <head> meta/SEO/fonts, <body> slot
│   ├── components/
│   │   ├── Navbar.astro         # Sticky navbar with scroll behavior
│   │   ├── Hero.astro           # Gradient mesh hero
│   │   ├── About.astro          # "Qué es FIT Labs" section
│   │   ├── Capabilities.astro   # 5 capabilities list
│   │   ├── Differentiator.astro # Two-column differentiator
│   │   ├── Contact.astro        # CTA section
│   │   └── Footer.astro         # Footer
│   ├── pages/
│   │   └── index.astro          # Composes all sections
│   └── styles/
│       └── global.css           # Tailwind imports, custom animations, gradient mesh
├── nginx/
│   └── nginx.conf               # Nginx config for static serving
├── Dockerfile                   # Multi-stage: node build → nginx serve
├── astro.config.mjs             # Astro config (static output)
├── package.json
├── tsconfig.json
└── .gitignore

Chunk 1: Project Setup & Base Layout

Task 1: Scaffold Astro Project

Files:

  • Create: package.json, astro.config.mjs, tsconfig.json, .gitignore

  • Step 1: Initialize Astro project

cd /Users/felipearentsen/Claude/code/fitlabs.dev
npm create astro@latest . -- --template minimal --no-install --typescript strict
  • Step 2: Add Tailwind CSS integration
npx astro add tailwind -y
  • Step 3: Configure astro.config.mjs for static output
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwindcss from '@astrojs/tailwind';

export default defineConfig({
  output: 'static',
  integrations: [tailwindcss()],
  site: 'https://fitlabs.dev',
});
  • Step 4: Update .gitignore

Add to .gitignore:

node_modules/
dist/
.astro/
.superpowers/
  • Step 5: Move logo to public/
cp /Users/felipearentsen/Claude/code/fitlabs.dev/LogoFIT.png /Users/felipearentsen/Claude/code/fitlabs.dev/public/LogoFIT.png
  • Step 6: Install dependencies and verify build
npm install
npm run build

Expected: Build succeeds, dist/ directory created.

  • Step 7: Initialize git and commit
git init
git add -A
git commit -m "chore: scaffold Astro project with Tailwind CSS"

Task 2: Global Styles & Tailwind Config

Files:

  • Create: src/styles/global.css

  • Modify: tailwind.config.mjs

  • Step 1: Configure Tailwind with custom colors and fonts

// tailwind.config.mjs
/** @type {import('tailwindcss').Config} */
export default {
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
  theme: {
    extend: {
      colors: {
        dark: {
          bg: '#0a0a1a',
          surface: '#0f172a',
          footer: '#06080f',
        },
        light: {
          bg: '#f0f9ff',
          surface: '#e0e7ff',
          base: '#f8fafc',
        },
        brand: {
          blue: '#0369a1',
          violet: '#7c3aed',
          cyan: '#06b6d4',
        },
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
      },
    },
  },
  plugins: [],
};
  • Step 2: Create global.css with Tailwind imports and custom animations
/* src/styles/global.css */
@import 'tailwindcss';

@theme {
  --font-sans: 'Inter', system-ui, sans-serif;
}

/* Gradient mesh animation for hero */
@keyframes mesh-drift-1 {
  0%, 100% { transform: translate(0, 0); }
  33% { transform: translate(30px, -20px); }
  66% { transform: translate(-20px, 15px); }
}

@keyframes mesh-drift-2 {
  0%, 100% { transform: translate(0, 0); }
  33% { transform: translate(-25px, 20px); }
  66% { transform: translate(15px, -30px); }
}

@keyframes mesh-drift-3 {
  0%, 100% { transform: translate(0, 0); }
  33% { transform: translate(20px, 25px); }
  66% { transform: translate(-30px, -10px); }
}

/* Scroll reveal */
.reveal {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.reveal.visible {
  opacity: 1;
  transform: translateY(0);
}

/* Capability list slide-in */
.slide-in {
  opacity: 0;
  transform: translateX(-20px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.slide-in.visible {
  opacity: 1;
  transform: translateX(0);
}

/* Gradient text utility */
.gradient-text {
  background: linear-gradient(135deg, #0369a1, #7c3aed);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
  • Step 3: Verify build
npm run build

Expected: Build succeeds.

  • Step 4: Commit
git add tailwind.config.mjs src/styles/global.css
git commit -m "feat: add Tailwind config with brand colors and global animations"

Task 3: Base Layout with SEO

Files:

  • Create: src/layouts/Layout.astro

  • Step 1: Create Layout.astro with meta tags and Open Graph

---
// src/layouts/Layout.astro
interface Props {
  title?: string;
  description?: string;
}

const {
  title = 'FIT Labs — Innovación con IA by FactorIT',
  description = 'Soluciones de inteligencia artificial enterprise que escalan. La incubadora de innovación de FactorIT.',
} = Astro.props;
---

<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content={description} />

    <!-- Open Graph -->
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:type" content="website" />
    <meta property="og:url" content="https://fitlabs.dev" />
    <meta property="og:site_name" content="FIT Labs" />

    <!-- Twitter -->
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content={title} />
    <meta name="twitter:description" content={description} />

    <!-- Favicon -->
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap"
      rel="stylesheet"
    />

    <!-- Structured Data -->
    <script type="application/ld+json" set:html={JSON.stringify({
      "@context": "https://schema.org",
      "@type": "Organization",
      "name": "FIT Labs",
      "description": description,
      "url": "https://fitlabs.dev",
      "parentOrganization": {
        "@type": "Organization",
        "name": "FactorIT",
        "url": "https://www.factorit.com"
      }
    })} />

    <title>{title}</title>
  </head>
  <body class="font-sans antialiased">
    <slot />
  </body>
</html>
  • Step 2: Create a minimal favicon.svg
<!-- public/favicon.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <rect width="32" height="32" rx="6" fill="#0a0a1a"/>
  <text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle"
        font-family="Inter, sans-serif" font-weight="800" font-size="14" fill="#06b6d4">
    FIT
  </text>
</svg>
  • Step 3: Create index.astro shell
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
import '../styles/global.css';
---

<Layout>
  <main>
    <p class="text-white bg-dark-bg p-8">FIT Labs — Coming soon</p>
  </main>
</Layout>
  • Step 4: Verify dev server and build
npm run dev -- --port 4321 &
sleep 3
curl -s http://localhost:4321 | head -20
kill %1
npm run build

Expected: HTML returned with correct meta tags. Build succeeds.

  • Step 5: Commit
git add src/layouts/Layout.astro src/pages/index.astro public/favicon.svg
git commit -m "feat: add base layout with SEO meta tags and structured data"

Chunk 2: Page Sections

Task 4: Navbar Component

Files:

  • Create: src/components/Navbar.astro

  • Step 1: Create Navbar.astro

---
// src/components/Navbar.astro
---

<nav id="navbar" class="fixed top-0 left-0 right-0 z-50 transition-all duration-300">
  <div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
    <!-- Logo -->
    <a href="https://www.factorit.com" target="_blank" rel="noopener noreferrer">
      <img src="/LogoFIT.png" alt="FIT Labs" class="h-8" />
    </a>

    <!-- Desktop Nav -->
    <div class="hidden md:flex items-center gap-8">
      <a href="#capacidades" class="text-sm text-slate-300 hover:text-white transition-colors">Capacidades</a>
      <a href="#nosotros" class="text-sm text-slate-300 hover:text-white transition-colors">Nosotros</a>
      <a href="#contacto" class="px-4 py-2 text-sm font-semibold text-white rounded-lg bg-gradient-to-br from-brand-blue to-brand-violet hover:scale-105 transition-transform">
        Contacto
      </a>
    </div>

    <!-- Mobile Hamburger -->
    <button id="menu-toggle" class="md:hidden text-white" aria-label="Abrir menú">
      <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path id="menu-icon" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
      </svg>
    </button>
  </div>

  <!-- Mobile Overlay -->
  <div id="mobile-menu" class="fixed inset-0 bg-dark-bg/95 backdrop-blur-sm flex flex-col items-center justify-center gap-8 hidden z-40">
    <button id="menu-close" class="absolute top-6 right-6 text-white" aria-label="Cerrar menú">
      <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
      </svg>
    </button>
    <a href="#capacidades" class="text-2xl text-white hover:text-brand-cyan transition-colors mobile-link">Capacidades</a>
    <a href="#nosotros" class="text-2xl text-white hover:text-brand-cyan transition-colors mobile-link">Nosotros</a>
    <a href="#contacto" class="text-2xl text-white hover:text-brand-cyan transition-colors mobile-link">Contacto</a>
  </div>
</nav>

<script>
  // Navbar scroll: transparent → solid
  const navbar = document.getElementById('navbar');
  window.addEventListener('scroll', () => {
    if (window.scrollY > 50) {
      navbar?.classList.add('bg-dark-bg/90', 'backdrop-blur-md', 'shadow-lg');
    } else {
      navbar?.classList.remove('bg-dark-bg/90', 'backdrop-blur-md', 'shadow-lg');
    }
  });

  // Mobile menu toggle
  const toggle = document.getElementById('menu-toggle');
  const menu = document.getElementById('mobile-menu');
  const close = document.getElementById('menu-close');
  const links = document.querySelectorAll('.mobile-link');

  function openMenu() {
    menu?.classList.remove('hidden');
    document.body.style.overflow = 'hidden';
  }
  function closeMenu() {
    menu?.classList.add('hidden');
    document.body.style.overflow = '';
  }

  toggle?.addEventListener('click', openMenu);
  close?.addEventListener('click', closeMenu);
  links.forEach(link => link.addEventListener('click', closeMenu));
  menu?.addEventListener('click', (e) => {
    if (e.target === menu) closeMenu();
  });
</script>
  • Step 2: Add Navbar to index.astro
---
import Layout from '../layouts/Layout.astro';
import Navbar from '../components/Navbar.astro';
import '../styles/global.css';
---

<Layout>
  <Navbar />
  <main>
    <div class="h-screen bg-dark-bg flex items-center justify-center">
      <p class="text-white text-2xl">Hero placeholder</p>
    </div>
    <div class="h-screen bg-light-bg" id="nosotros"></div>
    <div class="h-screen bg-dark-bg" id="capacidades"></div>
    <div class="h-screen bg-light-bg"></div>
    <div class="h-screen bg-dark-bg" id="contacto"></div>
  </main>
</Layout>
  • Step 3: Verify — dev server, scroll nav behavior, mobile menu
npm run dev -- --port 4321

Open http://localhost:4321. Verify: navbar transparent at top, solid on scroll, mobile menu opens/closes.

  • Step 4: Commit
git add src/components/Navbar.astro src/pages/index.astro
git commit -m "feat: add sticky navbar with scroll behavior and mobile menu"

Task 5: Hero Section

Files:

  • Create: src/components/Hero.astro

  • Step 1: Create Hero.astro with gradient mesh

---
// src/components/Hero.astro
---

<section id="hero" class="relative min-h-screen flex items-center justify-center bg-dark-bg overflow-hidden">
  <!-- Gradient Mesh Blobs -->
  <div class="absolute inset-0 overflow-hidden">
    <div
      class="absolute top-[-10%] right-[-5%] w-[500px] h-[500px] rounded-full opacity-60"
      style="background: radial-gradient(circle, rgba(124,58,237,0.3) 0%, transparent 70%); filter: blur(80px); animation: mesh-drift-1 20s ease-in-out infinite;"
    ></div>
    <div
      class="absolute bottom-[-15%] left-[-10%] w-[600px] h-[600px] rounded-full opacity-50"
      style="background: radial-gradient(circle, rgba(6,182,212,0.25) 0%, transparent 70%); filter: blur(80px); animation: mesh-drift-2 20s ease-in-out infinite;"
    ></div>
    <div
      class="absolute top-[40%] left-[40%] -translate-x-1/2 -translate-y-1/2 w-[550px] h-[550px] rounded-full opacity-40"
      style="background: radial-gradient(circle, rgba(3,105,161,0.2) 0%, transparent 70%); filter: blur(80px); animation: mesh-drift-3 20s ease-in-out infinite;"
    ></div>
  </div>

  <!-- Content -->
  <div class="relative z-10 text-center px-6 max-w-2xl mx-auto">
    <p class="text-brand-cyan text-xs tracking-[0.2em] uppercase mb-4 font-semibold">
      FactorIT Innovation Lab
    </p>
    <h1 class="text-5xl md:text-7xl font-extrabold text-white mb-6">
      FIT Labs
    </h1>
    <p class="text-lg md:text-xl text-slate-400 mb-10 leading-relaxed">
      Donde la inteligencia artificial se convierte en soluciones enterprise que escalan
    </p>
    <a
      href="#capacidades"
      class="inline-block px-8 py-3 text-base font-semibold text-white rounded-lg bg-gradient-to-br from-brand-blue to-brand-violet hover:scale-105 hover:shadow-[0_0_30px_rgba(124,58,237,0.3)] transition-all duration-300"
    >
      Explorar capacidades ↓
    </a>
  </div>
</section>
  • Step 2: Add Hero to index.astro (replace placeholder)

Update index.astro to import and use <Hero /> instead of the hero placeholder div.

  • Step 3: Verify — animated gradient blobs drifting, content centered, CTA visible
npm run dev -- --port 4321

Open http://localhost:4321. Verify: blobs animate, text is readable, button links to #capacidades.

  • Step 4: Commit
git add src/components/Hero.astro src/pages/index.astro
git commit -m "feat: add hero section with gradient mesh animation"

Task 6: About Section

Files:

  • Create: src/components/About.astro

  • Step 1: Create About.astro

---
// src/components/About.astro
---

<section id="nosotros" class="bg-light-bg py-24 px-6">
  <div class="max-w-3xl mx-auto text-center reveal">
    <p class="text-brand-blue text-xs tracking-[0.2em] uppercase font-semibold mb-3">
      Sobre nosotros
    </p>
    <h2 class="text-3xl md:text-4xl font-bold text-slate-900 mb-6">
      La incubadora de innovación de FactorIT
    </h2>
    <p class="text-slate-500 text-lg leading-relaxed">
      FIT Labs es donde exploramos, prototipamos y llevamos a producción soluciones
      de inteligencia artificial para empresas. Nacimos del ADN tecnológico de
      FactorIT para resolver los desafíos más complejos con IA — no entregamos
      demos, entregamos sistemas que funcionan en el mundo real.
    </p>
  </div>
</section>
  • Step 2: Add About to index.astro

Import and replace the #nosotros placeholder.

  • Step 3: Verify and commit
npm run build
git add src/components/About.astro src/pages/index.astro
git commit -m "feat: add about section"

Task 7: Capabilities Section

Files:

  • Create: src/components/Capabilities.astro

  • Step 1: Create Capabilities.astro

---
// src/components/Capabilities.astro
const capabilities = [
  {
    icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>`,
    title: 'Documentos Inteligentes',
    description: 'OCR, extracción y clasificación automática',
    borderColor: 'border-brand-cyan',
  },
  {
    icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>`,
    title: 'Agentes de IA',
    description: 'Conversacionales y autónomos',
    borderColor: 'border-brand-violet',
  },
  {
    icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>`,
    title: 'Automatización con IA',
    description: 'Procesos end-to-end inteligentes',
    borderColor: 'border-brand-blue',
  },
  {
    icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="m19 9-5 5-4-4-3 3"/></svg>`,
    title: 'Análisis de Datos',
    description: 'Insights e inteligencia de negocio',
    borderColor: 'border-brand-cyan',
  },
  {
    icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7 5V8l-7 5V4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/></svg>`,
    title: 'Soluciones Industriales',
    description: 'IA aplicada a tu vertical',
    borderColor: 'border-brand-violet',
  },
];
---

<section id="capacidades" class="bg-dark-bg py-24 px-6">
  <div class="max-w-3xl mx-auto">
    <div class="text-center mb-12 reveal">
      <p class="text-brand-cyan text-xs tracking-[0.2em] uppercase font-semibold mb-3">
        Capacidades
      </p>
      <h2 class="text-3xl md:text-4xl font-bold text-white">
        Lo que construimos
      </h2>
    </div>

    <div class="flex flex-col gap-4">
      {capabilities.map((cap, i) => (
        <div
          class={`flex items-center gap-5 bg-dark-surface border-l-[3px] ${cap.borderColor} rounded-r-lg px-6 py-5 slide-in hover:bg-dark-surface/80 hover:shadow-[0_0_20px_rgba(6,182,212,0.05)] transition-all duration-300`}
          style={`transition-delay: ${i * 100}ms;`}
        >
          <div class="text-brand-cyan shrink-0" set:html={cap.icon} />
          <div>
            <h3 class="text-white font-semibold">{cap.title}</h3>
            <p class="text-slate-400 text-sm mt-1">{cap.description}</p>
          </div>
        </div>
      ))}
    </div>
  </div>
</section>
  • Step 2: Add Capabilities to index.astro

Import and replace the #capacidades placeholder.

  • Step 3: Verify — 5 items with colored borders, icons visible
npm run dev -- --port 4321
  • Step 4: Commit
git add src/components/Capabilities.astro src/pages/index.astro
git commit -m "feat: add capabilities section with icon list"

Task 8: Differentiator Section

Files:

  • Create: src/components/Differentiator.astro

  • Step 1: Create Differentiator.astro

---
// src/components/Differentiator.astro
const values = [
  { text: 'Arquitectura para escalar', borderColor: 'border-brand-blue' },
  { text: 'Performance en producción', borderColor: 'border-brand-violet' },
  { text: 'Integración con tu ecosistema', borderColor: 'border-brand-cyan' },
];
---

<section class="bg-light-bg py-24 px-6">
  <div class="max-w-4xl mx-auto reveal">
    <p class="text-brand-blue text-xs tracking-[0.2em] uppercase font-semibold mb-8">
      Nuestro enfoque
    </p>

    <div class="flex flex-col md:flex-row gap-12 md:gap-16">
      <!-- Narrative -->
      <div class="flex-1">
        <h2 class="text-2xl md:text-3xl font-bold text-slate-900 mb-4">
          Innovación con los pies en la tierra
        </h2>
        <p class="text-slate-500 leading-relaxed">
          Nos apasiona la IA, pero nos obsesiona que funcione. Cada proyecto que
          sale de FIT Labs está pensado para integrarse en operaciones reales,
          soportar carga real, y crecer cuando tu negocio crece.
        </p>
      </div>

      <!-- Values list -->
      <div class="flex-1 flex flex-col gap-3">
        {values.map((val) => (
          <div class={`bg-white border-l-[3px] ${val.borderColor} rounded-lg px-5 py-4 shadow-sm`}>
            <p class="text-slate-900 font-semibold text-sm">{val.text}</p>
          </div>
        ))}
      </div>
    </div>
  </div>
</section>
  • Step 2: Add to index.astro

Import and replace the light-bg placeholder.

  • Step 3: Verify and commit
npm run build
git add src/components/Differentiator.astro src/pages/index.astro
git commit -m "feat: add differentiator section with values"

Task 9: Contact (CTA) Section

Files:

  • Create: src/components/Contact.astro

  • Step 1: Create Contact.astro

---
// src/components/Contact.astro
---

<section id="contacto" class="relative bg-dark-bg py-24 px-6 overflow-hidden">
  <!-- Subtle glow -->
  <div
    class="absolute top-[-30%] left-1/2 -translate-x-1/2 w-[500px] h-[300px] rounded-full opacity-40"
    style="background: radial-gradient(circle, rgba(124,58,237,0.15) 0%, transparent 70%); filter: blur(60px);"
  ></div>

  <div class="relative z-10 max-w-2xl mx-auto text-center reveal">
    <h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
      ¿Tienes un desafío en mente?
    </h2>
    <p class="text-slate-400 text-lg mb-10 leading-relaxed">
      Cuéntanos qué problema quieres resolver. Nosotros vemos cómo la IA puede ayudarte.
    </p>
    <a
      href="mailto:contacto@fitlabs.dev"
      class="inline-block px-10 py-4 text-base font-semibold text-white rounded-lg bg-gradient-to-br from-brand-blue to-brand-violet hover:scale-105 hover:shadow-[0_0_30px_rgba(124,58,237,0.3)] transition-all duration-300"
    >
      Conversemos →
    </a>
    <p class="mt-5 text-sm text-slate-500">
      <a href="mailto:contacto@fitlabs.dev" class="hover:text-brand-cyan transition-colors">
        contacto@fitlabs.dev
      </a>
    </p>
  </div>
</section>
  • Step 2: Add to index.astro

Import and replace the #contacto placeholder.

  • Step 3: Verify and commit
npm run build
git add src/components/Contact.astro src/pages/index.astro
git commit -m "feat: add CTA contact section"

Files:

  • Create: src/components/Footer.astro

  • Step 1: Create Footer.astro

---
// src/components/Footer.astro
---

<footer class="bg-dark-footer py-8 px-6 border-t border-white/5">
  <div class="max-w-6xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
    <!-- Left: branding -->
    <div>
      <a href="https://www.factorit.com" target="_blank" rel="noopener noreferrer" class="text-white font-bold text-sm hover:text-brand-cyan transition-colors">
        FIT <span class="text-brand-cyan">Labs</span>
      </a>
      <p class="text-slate-500 text-xs mt-1">by FactorIT</p>
    </div>

    <!-- Right: links -->
    <div class="flex items-center gap-6 text-sm text-slate-500">
      <a href="https://www.linkedin.com/company/factorit" target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">LinkedIn</a>
      <a href="https://github.com/factorit" target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">GitHub</a>
      <a href="https://www.factorit.com" target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">factorit.com</a>
    </div>
  </div>
</footer>
  • Step 2: Add to index.astro

Import Footer and place after </main>.

  • Step 3: Verify and commit
npm run build
git add src/components/Footer.astro src/pages/index.astro
git commit -m "feat: add footer with FactorIT branding"

Chunk 3: Animations, Polish & Deployment

Task 11: Scroll Reveal Animations

Files:

  • Modify: src/layouts/Layout.astro (add script before </body>)

  • Step 1: Add IntersectionObserver script to Layout.astro

Add before </body>:

<script>
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          entry.target.classList.add('visible');
        }
      });
    },
    { threshold: 0.1 }
  );

  document.querySelectorAll('.reveal, .slide-in').forEach((el) => {
    observer.observe(el);
  });
</script>
  • Step 2: Verify — sections and capabilities animate on scroll
npm run dev -- --port 4321

Open http://localhost:4321. Scroll down. Verify: sections fade in, capability items slide in from left with staggered delay.

  • Step 3: Commit
git add src/layouts/Layout.astro
git commit -m "feat: add scroll reveal animations"

Task 12: Final index.astro Assembly

Files:

  • Modify: src/pages/index.astro

  • Step 1: Ensure index.astro imports all components in order

---
import Layout from '../layouts/Layout.astro';
import Navbar from '../components/Navbar.astro';
import Hero from '../components/Hero.astro';
import About from '../components/About.astro';
import Capabilities from '../components/Capabilities.astro';
import Differentiator from '../components/Differentiator.astro';
import Contact from '../components/Contact.astro';
import Footer from '../components/Footer.astro';
import '../styles/global.css';
---

<Layout>
  <Navbar />
  <main>
    <Hero />
    <About />
    <Capabilities />
    <Differentiator />
    <Contact />
  </main>
  <Footer />
</Layout>
  • Step 2: Full visual check on dev server
npm run dev -- --port 4321

Check all 6 sections render, navbar works, animations fire, mobile menu works, all links work.

  • Step 3: Build and verify output
npm run build
ls -la dist/
cat dist/index.html | head -30

Expected: dist/index.html exists with all content.

  • Step 4: Commit
git add src/pages/index.astro
git commit -m "feat: assemble complete landing page"

Task 13: Docker + Nginx for Coolify

Files:

  • Create: Dockerfile

  • Create: nginx/nginx.conf

  • Step 1: Create nginx.conf

# nginx/nginx.conf
worker_processes 1;

events {
  worker_connections 1024;
}

http {
  server {
    listen 8080;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    include /etc/nginx/mime.types;

    gzip on;
    gzip_min_length 1000;
    gzip_proxied expired no-cache no-store private auth;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
      expires 30d;
      add_header Cache-Control "public, immutable";
    }

    # SPA fallback
    location / {
      try_files $uri $uri/index.html /index.html;
    }

    error_page 404 /404.html;
    location = /404.html {
      root /usr/share/nginx/html;
      internal;
    }
  }
}
  • Step 2: Create Dockerfile
# Dockerfile
FROM node:lts AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx:alpine AS runtime
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 8080
  • Step 3: Test Docker build locally
docker build -t fitlabs-dev .
docker run --rm -p 8080:8080 fitlabs-dev &
sleep 2
curl -s http://localhost:8080 | head -20
docker stop $(docker ps -q --filter ancestor=fitlabs-dev)

Expected: HTML content returned, site accessible at http://localhost:8080.

  • Step 4: Commit
git add Dockerfile nginx/nginx.conf
git commit -m "feat: add Dockerfile and nginx config for Coolify deployment"

Task 14: Final Build Verification

  • Step 1: Clean build
rm -rf dist/
npm run build

Expected: Build succeeds with no warnings.

  • Step 2: Check output size
du -sh dist/
find dist/ -type f | wc -l

Expected: Small output (~100KB500KB), few files.

  • Step 3: Verify HTML includes all sections
grep -c 'id="hero"\|id="nosotros"\|id="capacidades"\|id="contacto"' dist/index.html

Expected: 4 matches.

  • Step 4: Final commit
git add -A
git commit -m "chore: final build verification — ready for Coolify deploy"

Deployment Notes

To deploy on Coolify:

  1. Push repo to a Git remote (GitHub, GitLab, etc.)
  2. In Coolify, create a new resource → Docker-based
  3. Point to the repo, Coolify will detect the Dockerfile
  4. Set port to 8080
  5. Configure domain: fitlabs.dev
  6. Deploy