Ir al contenido

2026 05 03 nekazari commercial landing redesign

Esta página aún no está disponible en tu idioma.

Commercial Landing Redesign Implementation Plan

Section titled “Commercial Landing Redesign Implementation Plan”

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Replace CommercialLanding.tsx (generic SaaS template) with a 12-block editorial landing that communicates open-core + managed-cloud positioning, per spec 2026-05-03-nekazari-commercial-landing-redesign.md.

Architecture: Single-page CSR component composed of 12 focused sub-components. Each sub-component is a pure presentational file in src/pages/landing/blocks/. CommercialLanding.tsx becomes a thin orchestration layer (layout, scroll logic, auth). Existing PricingCards, PartnerLogos, CookieBanner, NkzAttribution are reused with wrapper adjustments. All new strings go into common.json under landing.*. No new npm dependencies.

Tech Stack: React 18 + TS 5 + Tailwind + lucide-react (icons) + existing i18n via useI18n()

Spec: nkz/internal-docs/specs/2026-05-03-nekazari-commercial-landing-redesign.md


FileActionPurpose
apps/host/public/media/hero.mp4Create (copy)Hero video from splash.mp4
apps/host/public/media/hero.webmCreate (transcode)VP9 variant
apps/host/public/media/hero-poster.jpgCreate (extract)Static fallback
apps/host/index.htmlModifyAdd JetBrains Mono from Google Fonts
apps/host/src/index.cssModifyReduced-motion rules, mono font utility
apps/host/public/locales/es/common.jsonModifyAdd landing_v2.* keys (Spanish)
apps/host/public/locales/en/common.jsonModifyAdd landing_v2.* keys (English)
apps/host/src/pages/landing/blocks/HeroTopBar.tsxCreateFixed top bar (transparent → white on scroll)
apps/host/src/pages/landing/blocks/HeroSection.tsxCreateHero with video, overlay, text, CTAs
apps/host/src/pages/landing/blocks/ScrollIndicator.tsxCreateBottom-center scroll indicator
apps/host/src/pages/landing/blocks/ProofBand.tsxCreate4-column metrics band
apps/host/src/pages/landing/blocks/ProductAnchor.tsxCreate”Qué obtienes” + Cesium screenshot
apps/host/src/pages/landing/blocks/OpenCoreSection.tsxCreatenkz-os vs Nekazari 2-column
apps/host/src/pages/landing/blocks/EcosystemModules.tsxCreate4×2 module grid
apps/host/src/pages/landing/blocks/OpenStandards.tsxCreateWordmarks band
apps/host/src/pages/landing/blocks/FinalCTA.tsxCreateDark CTA block
apps/host/src/pages/landing/blocks/LandingFooter.tsxCreateFooter matching nkz-os.org
apps/host/src/components/pricing/PricingCards.tsxModifyRemove gradients, shadow-2xl, hover transforms; add Free tier
apps/host/src/components/partners/PartnerLogos.tsxModifyGrayscale default, color on hover, “CONFÍAN EN NEKAZARI” eyebrow
apps/host/src/pages/landing/CommercialLanding.tsxRewriteThin orchestrator composing all blocks

Task 0: Asset preparation (video variants + poster)

Section titled “Task 0: Asset preparation (video variants + poster)”

Files:

  • Create: apps/host/public/media/hero.mp4

  • Create: apps/host/public/media/hero.webm

  • Create: apps/host/public/media/hero-poster.jpg

  • Step 1: Create media directory and copy splash.mp4

Terminal window
mkdir -p apps/host/public/media
cp nkz-mobile/assets/splash.mp4 apps/host/public/media/hero.mp4
  • Step 2: Generate WebM variant and poster image

No ffmpeg available in this environment. User must run:

Terminal window
cd /home/g/Documents/nekazari/nkz/apps/host/public/media
ffmpeg -i hero.mp4 -c:v libvpx-vp9 -b:v 1.5M -an hero.webm
ffmpeg -i hero.mp4 -ss 00:00:00.5 -frames:v 1 -q:v 3 hero-poster.jpg

Verify: hero.webm ≈ 2.5MB, hero-poster.jpg ≈ 120KB.


Task 1: Add JetBrains Mono font to index.html

Section titled “Task 1: Add JetBrains Mono font to index.html”

File: Modify apps/host/index.html

  • Step 1: Add JetBrains Mono link in <head>

Add after the existing Inter <link> line (line 10):

<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">

Task 2: Add reduced-motion + mono utility to index.css

Section titled “Task 2: Add reduced-motion + mono utility to index.css”

File: Modify apps/host/src/index.css

  • Step 1: Append rules at end of file
/* Landing: reduced-motion support */
@media (prefers-reduced-motion: reduce) {
.hero-video {
display: none !important;
}
.scroll-indicator-line {
animation: none !important;
}
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Landing: JetBrains Mono utility */
.font-mono-landing {
font-family: 'JetBrains Mono', 'IBM Plex Mono', 'ui-monospace', 'SF Mono', monospace;
}

Task 3: i18n — new landing_v2 strings (es + en)

Section titled “Task 3: i18n — new landing_v2 strings (es + en)”

Files:

  • Modify: apps/host/public/locales/es/common.json

  • Modify: apps/host/public/locales/en/common.json

  • Step 1: Add landing_v2 block to es/common.json

Find the closing } of the "landing" block (after the "pricing" nested object) and add a comma, then insert after "landing":

"landing_v2": {
"eyebrow_open_core": "OPEN CORE · MANAGED CLOUD",
"hero_h1": "El stack abierto del campo. Operado para ti.",
"hero_sub": "Despliega nkz-os sin gestionar Kubernetes ni FIWARE. Multitenant, NGSI-LD nativo, infraestructura europea.",
"hero_cta_primary": "Crear tenant gratuito",
"hero_cta_secondary": "Solicitar demo",
"hero_scroll": "Scroll",
"proof_parcelas": "parcelas monitorizadas",
"proof_plantas": "plantas industriales",
"proof_observaciones": "observaciones /día",
"proof_paises": "países",
"product_eyebrow": "EL PRODUCTO",
"product_h2": "Una consola operativa. Cualquier capa de datos.",
"product_body": "Visualización 3D sobre CesiumJS con módulos cargados dinámicamente, drag-and-drop de capas geoespaciales y observaciones en tiempo real. Una única interfaz para todos los datos del campo.",
"product_bullet_1": "Visualización 3D de parcelas, parcelarios, infraestructuras industriales",
"product_bullet_2": "Capas dinámicas: Vegetation Prime, LiDAR, GIS Routing, DataHub",
"product_bullet_3": "Dashboard multitenant con aislamiento estricto",
"product_bullet_4": "Multi-idioma (es, en, ca, eu, fr, pt)",
"opencore_eyebrow": "OPEN CORE",
"opencore_h2": "Construido sobre nkz-os. Operado por nosotros.",
"opencore_intro": "Nekazari opera nkz-os, el proyecto open-source que mantenemos en abierto. Puedes desplegarlo tú mismo en tu Kubernetes, o dejar que nosotros lo gestionemos para ti.",
"opencore_oss_title": "nkz-os (Self-hosted)",
"opencore_oss_1": "Open source · AGPL-3.0",
"opencore_oss_2": "GitHub: nkz-os/nkz",
"opencore_oss_3": "Comunidad + módulos abiertos",
"opencore_oss_4": "Despliegue propio en K8s",
"opencore_oss_5": "Gratis para siempre",
"opencore_oss_cta": "Documentación",
"opencore_cloud_title": "Nekazari (Cloud)",
"opencore_cloud_1": "SaaS gestionado",
"opencore_cloud_2": "Multitenant aislado",
"opencore_cloud_3": "Mismos módulos + soporte SLA",
"opencore_cloud_4": "Infra europea (DE/FR), backups",
"opencore_cloud_5": "Free tier · Pro · Enterprise",
"opencore_cloud_cta": "Crear tenant gratuito",
"ecosystem_eyebrow": "ECOSISTEMA",
"ecosystem_h2": "Un módulo para cada capa.",
"ecosystem_sub": "Construidos sobre la misma base FIWARE. Disponibles en self-hosted y cloud.",
"ecosystem_cta": "Ver todos los módulos en nkz-os.org",
"ecosystem_badge_oss": "Open source",
"ecosystem_badge_cloud": "Cloud",
"ecosystem_mod_vegetation": "Índices vegetales sobre imágenes Sentinel-2.",
"ecosystem_mod_lidar": "Ingestión SOTA de nubes de puntos LiDAR.",
"ecosystem_mod_gis": "Rutas, geoanálisis y mapas de cultivo.",
"ecosystem_mod_datahub": "Analítica de series temporales con uPlot.",
"ecosystem_mod_iot": "Provisioning FIWARE estándar de dispositivos.",
"ecosystem_mod_vpn": "Tailscale + Headscale para acceso seguro.",
"ecosystem_mod_zulip": "Comunicaciones internas con OIDC integrado.",
"ecosystem_mod_cue": "Configuración centralizada de la plataforma.",
"standards_eyebrow": "STANDARDS",
"standards_h2": "Sin lock-in. Sin formato propietario.",
"partners_eyebrow": "CONFÍAN EN NEKAZARI",
"cta_final_h2": "Empieza con un tenant gratuito. En menos de dos minutos.",
"cta_final_primary": "Crear tenant gratuito",
"cta_final_secondary": "o solicita una demo",
"footer_tagline": "Plataforma SaaS sobre nkz-os.",
"footer_col1_title": "Producto",
"footer_col1_1": "Módulos",
"footer_col1_2": "Pricing",
"footer_col1_3": "Documentación",
"footer_col1_4": "Estado",
"footer_col2_title": "Open source",
"footer_col2_1": "GitHub",
"footer_col2_2": "nkz-os.org",
"footer_col2_3": "Comunidad",
"footer_col2_4": "Roadmap",
"footer_col3_title": "Empresa",
"footer_col3_1": "Sobre nosotros",
"footer_col3_2": "Contacto",
"footer_col3_3": "Privacidad",
"footer_col3_4": "Términos",
"footer_copyright": "© 2026 Nekazari · Powered by nkz-os · AGPL-3.0"
}
  • Step 2: Add landing_v2 block to en/common.json

Same position, English values:

"landing_v2": {
"eyebrow_open_core": "OPEN CORE · MANAGED CLOUD",
"hero_h1": "The open stack for the field. Operated for you.",
"hero_sub": "Deploy nkz-os without managing Kubernetes or FIWARE. Multitenant, NGSI-LD native, European infrastructure.",
"hero_cta_primary": "Create free tenant",
"hero_cta_secondary": "Request demo",
"hero_scroll": "Scroll",
"proof_parcelas": "monitored parcels",
"proof_plantas": "industrial plants",
"proof_observaciones": "observations /day",
"proof_paises": "countries",
"product_eyebrow": "THE PRODUCT",
"product_h2": "One operational console. Any data layer.",
"product_body": "3D visualization on CesiumJS with dynamically loaded modules, drag-and-drop geospatial layers, and real-time observations. A single interface for all field data.",
"product_bullet_1": "3D visualization of parcels, cadastral maps, industrial infrastructure",
"product_bullet_2": "Dynamic layers: Vegetation Prime, LiDAR, GIS Routing, DataHub",
"product_bullet_3": "Multitenant dashboard with strict isolation",
"product_bullet_4": "Multi-language (es, en, ca, eu, fr, pt)",
"opencore_eyebrow": "OPEN CORE",
"opencore_h2": "Built on nkz-os. Operated by us.",
"opencore_intro": "Nekazari operates nkz-os, the open-source project we maintain in the open. You can deploy it yourself on your Kubernetes, or let us manage it for you.",
"opencore_oss_title": "nkz-os (Self-hosted)",
"opencore_oss_1": "Open source · AGPL-3.0",
"opencore_oss_2": "GitHub: nkz-os/nkz",
"opencore_oss_3": "Community + open modules",
"opencore_oss_4": "Self-deployed on K8s",
"opencore_oss_5": "Free forever",
"opencore_oss_cta": "Documentation",
"opencore_cloud_title": "Nekazari (Cloud)",
"opencore_cloud_1": "Managed SaaS",
"opencore_cloud_2": "Isolated multitenant",
"opencore_cloud_3": "Same modules + SLA support",
"opencore_cloud_4": "EU infra (DE/FR), backups",
"opencore_cloud_5": "Free tier · Pro · Enterprise",
"opencore_cloud_cta": "Create free tenant",
"ecosystem_eyebrow": "ECOSYSTEM",
"ecosystem_h2": "One module for every layer.",
"ecosystem_sub": "Built on the same FIWARE foundation. Available self-hosted and cloud.",
"ecosystem_cta": "See all modules on nkz-os.org",
"ecosystem_badge_oss": "Open source",
"ecosystem_badge_cloud": "Cloud",
"ecosystem_mod_vegetation": "Vegetation indices on Sentinel-2 imagery.",
"ecosystem_mod_lidar": "SOTA LiDAR point cloud ingestion.",
"ecosystem_mod_gis": "Routes, geo-analysis, and crop maps.",
"ecosystem_mod_datahub": "Time-series analytics with uPlot.",
"ecosystem_mod_iot": "Standard FIWARE device provisioning.",
"ecosystem_mod_vpn": "Tailscale + Headscale for secure access.",
"ecosystem_mod_zulip": "Internal communications with OIDC integration.",
"ecosystem_mod_cue": "Centralized platform configuration.",
"standards_eyebrow": "STANDARDS",
"standards_h2": "No lock-in. No proprietary format.",
"partners_eyebrow": "TRUSTED BY",
"cta_final_h2": "Start with a free tenant. In under two minutes.",
"cta_final_primary": "Create free tenant",
"cta_final_secondary": "or request a demo",
"footer_tagline": "SaaS platform powered by nkz-os.",
"footer_col1_title": "Product",
"footer_col1_1": "Modules",
"footer_col1_2": "Pricing",
"footer_col1_3": "Documentation",
"footer_col1_4": "Status",
"footer_col2_title": "Open source",
"footer_col2_1": "GitHub",
"footer_col2_2": "nkz-os.org",
"footer_col2_3": "Community",
"footer_col2_4": "Roadmap",
"footer_col3_title": "Company",
"footer_col3_1": "About",
"footer_col3_2": "Contact",
"footer_col3_3": "Privacy",
"footer_col3_4": "Terms",
"footer_copyright": "© 2026 Nekazari · Powered by nkz-os · AGPL-3.0"
}

File: Create apps/host/src/pages/landing/blocks/HeroTopBar.tsx

  • Step 1: Write the component
import React, { useState } from 'react';
import { Globe } from 'lucide-react';
import { useI18n } from '@/context/I18nContext';
import { useAuth } from '@/context/KeycloakAuthContext';
import { useNavigate } from 'react-router-dom';
interface Props {
isScrolled: boolean;
language: string;
supportedLanguages: Record<string, string>;
showLanguageMenu: boolean;
setShowLanguageMenu: (v: boolean) => void;
onLanguageChange: (lang: string) => void;
}
export const HeroTopBar: React.FC<Props> = ({
isScrolled,
language,
supportedLanguages,
showLanguageMenu,
setShowLanguageMenu,
onLanguageChange,
}) => {
const { t } = useI18n();
const { login, isAuthenticated } = useAuth();
const navigate = useNavigate();
const handleLogin = async () => {
if (isAuthenticated) {
navigate('/dashboard');
return;
}
try {
await login(true);
} catch {
const { getConfig } = await import('@/config/environment');
const config = getConfig();
const keycloakUrl = `${config.keycloak.url}/realms/${config.keycloak.realm}/protocol/openid-connect/auth`;
const params = new URLSearchParams({
client_id: config.keycloak.clientId,
redirect_uri: `${window.location.origin}/dashboard`,
response_type: 'code',
scope: 'openid',
prompt: 'login',
});
window.location.href = `${keycloakUrl}?${params.toString()}`;
}
};
const textColor = isScrolled ? 'text-[#0E1A14]' : 'text-white';
const bg = isScrolled
? 'bg-white/90 backdrop-blur-md border-b border-[rgba(14,26,20,0.06)]'
: 'bg-transparent';
return (
<nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${bg}`}
style={{ padding: '1.25rem 2rem' }}
>
<div className="max-w-[1200px] mx-auto flex items-center justify-between">
{/* NKZ Wordmark */}
<span className={`text-xl font-semibold tracking-tight ${textColor} transition-colors duration-300`}>
NKZ
</span>
{/* Right side: language + login */}
<div className="flex items-center gap-4">
{/* Language Selector */}
<div className="relative">
<button
type="button"
onClick={() => setShowLanguageMenu(!showLanguageMenu)}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${textColor} hover:opacity-70`}
>
<Globe className="h-4 w-4" />
{supportedLanguages[language] || 'ES'}
</button>
{showLanguageMenu && (
<>
<div className="absolute right-0 mt-2 w-40 rounded-lg shadow-lg bg-white ring-1 ring-black/5 z-20 overflow-hidden">
{Object.entries(supportedLanguages).map(([code, name]) => (
<button
key={code}
onClick={() => onLanguageChange(code)}
className={`block w-full text-left px-4 py-2 text-sm transition-colors ${
language === code
? 'bg-green-50 text-green-900 font-medium'
: 'text-gray-700 hover:bg-gray-50'
}`}
>
{name as string}
</button>
))}
</div>
<div className="fixed inset-0 z-10" onClick={() => setShowLanguageMenu(false)} />
</>
)}
</div>
{/* Login */}
<button
onClick={handleLogin}
className={`text-sm font-medium transition-colors duration-300 ${textColor} hover:opacity-70`}
>
Iniciar sesión
</button>
</div>
</div>
</nav>
);
};

File: Create apps/host/src/pages/landing/blocks/ScrollIndicator.tsx

  • Step 1: Write the component
import React from 'react';
import { useI18n } from '@/context/I18nContext';
export const ScrollIndicator: React.FC = () => {
const { t } = useI18n();
return (
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 z-10">
<span className="font-mono-landing text-[11px] uppercase tracking-[0.15em] text-white/50">
{t('landing_v2.hero_scroll')}
</span>
<div className="scroll-indicator-line w-px h-8 bg-white/40 relative overflow-hidden">
<div
className="absolute top-0 left-0 w-full h-2 bg-white/70 rounded-full"
style={{
animation: 'scrollDrop 1.5s ease-in-out infinite',
}}
/>
</div>
<style>{`
@keyframes scrollDrop {
0% { transform: translateY(-100%); opacity: 0; }
50% { opacity: 1; }
100% { transform: translateY(32px); opacity: 0; }
}
`}</style>
</div>
);
};

File: Create apps/host/src/pages/landing/blocks/HeroSection.tsx

  • Step 1: Write the component
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowRight } from 'lucide-react';
import { useI18n } from '@/context/I18nContext';
import { ScrollIndicator } from './ScrollIndicator';
export const HeroSection: React.FC = () => {
const { t } = useI18n();
const navigate = useNavigate();
return (
<section className="relative min-h-screen flex items-end pb-24 overflow-hidden">
{/* Video background */}
<video
autoPlay
muted
loop
playsInline
poster="/media/hero-poster.jpg"
preload="metadata"
className="hero-video absolute inset-0 w-full h-full object-cover hidden md:block"
aria-hidden="true"
>
<source src="/media/hero.webm" type="video/webm" />
<source src="/media/hero.mp4" type="video/mp4" />
</video>
{/* Mobile poster fallback */}
<img
src="/media/hero-poster.jpg"
alt=""
className="absolute inset-0 w-full h-full object-cover md:hidden"
aria-hidden="true"
/>
{/* Overlay gradient */}
<div
className="absolute inset-0 z-[1]"
style={{
background: 'linear-gradient(180deg, rgba(14,26,20,0.30) 0%, rgba(14,26,20,0.55) 50%, rgba(14,26,20,0.85) 100%)',
}}
/>
{/* Content */}
<div className="relative z-[2] max-w-[1200px] mx-auto px-8 w-full">
{/* Eyebrow */}
<p className="font-mono-landing text-[13px] tracking-[0.15em] uppercase text-white/70 mb-4">
{t('landing_v2.eyebrow_open_core')}
</p>
{/* H1 */}
<h1
className="text-white font-semibold mb-6 max-w-[14ch]"
style={{
fontFamily: "'Inter', sans-serif",
fontSize: 'clamp(2.5rem, 6vw, 5.5rem)',
lineHeight: 1.05,
letterSpacing: '-0.03em',
}}
>
{t('landing_v2.hero_h1')}
</h1>
{/* Sub */}
<p
className="text-white/85 mb-10 max-w-[56ch]"
style={{
fontFamily: "'Inter', sans-serif",
fontSize: 'clamp(1.05rem, 1.4vw, 1.25rem)',
lineHeight: 1.5,
fontWeight: 400,
}}
>
{t('landing_v2.hero_sub')}
</p>
{/* CTAs */}
<div className="flex flex-col sm:flex-row items-start gap-4">
<button
onClick={() => navigate('/register')}
className="inline-flex items-center px-7 py-3.5 bg-[#1F4D38] text-white font-semibold rounded-lg hover:bg-[#163A2A] hover:-translate-y-px transition-all duration-200"
style={{ fontSize: '1rem' }}
>
{t('landing_v2.hero_cta_primary')}
</button>
<a
href={`mailto:${(window as any).__ENV__?.SALES_EMAIL || 'info@nekazari.com'}`}
className="inline-flex items-center gap-1.5 text-white font-medium hover:underline transition-all duration-200 group"
style={{ fontSize: '1rem' }}
>
{t('landing_v2.hero_cta_secondary')}
<ArrowRight className="h-4 w-4 group-hover:translate-x-0.5 transition-transform" />
</a>
</div>
</div>
{/* Scroll indicator */}
<ScrollIndicator />
</section>
);
};

File: Create apps/host/src/pages/landing/blocks/ProofBand.tsx

  • Step 1: Write the component
import React from 'react';
import { useI18n } from '@/context/I18nContext';
const METRICS = [
{ value: '127', key: 'landing_v2.proof_parcelas' },
{ value: '14', key: 'landing_v2.proof_plantas' },
{ value: '3.2M', key: 'landing_v2.proof_observaciones' },
{ value: '6', key: 'landing_v2.proof_paises' },
];
export const ProofBand: React.FC = () => {
const { t } = useI18n();
return (
<section
className="bg-[#FAFAF7]"
style={{
padding: '4rem 2rem',
borderTop: '1px solid rgba(14,26,20,0.08)',
borderBottom: '1px solid rgba(14,26,20,0.08)',
}}
>
<div className="max-w-[1200px] mx-auto grid grid-cols-2 md:grid-cols-4 gap-8">
{METRICS.map((m) => (
<div key={m.key} className="text-center">
<div
className="font-mono-landing font-medium text-[#0E1A14] mb-1"
style={{ fontSize: 'clamp(2rem, 3.5vw, 3rem)' }}
>
{m.value}
</div>
<div className="text-sm text-[#5B6660] leading-tight">{t(m.key)}</div>
</div>
))}
</div>
</section>
);
};

File: Create apps/host/src/pages/landing/blocks/ProductAnchor.tsx

  • Step 1: Write the component
import React from 'react';
import { useI18n } from '@/context/I18nContext';
export const ProductAnchor: React.FC = () => {
const { t } = useI18n();
const bullets = [
'landing_v2.product_bullet_1',
'landing_v2.product_bullet_2',
'landing_v2.product_bullet_3',
'landing_v2.product_bullet_4',
];
return (
<section className="bg-white" style={{ padding: '8rem 2rem' }}>
<div className="max-w-[1200px] mx-auto grid lg:grid-cols-2 gap-16 items-center">
{/* Left: text */}
<div>
<p className="font-mono-landing text-[13px] tracking-[0.15em] uppercase text-[#5B6660] mb-4">
{t('landing_v2.product_eyebrow')}
</p>
<h2
className="text-[#0E1A14] font-semibold mb-6"
style={{
fontFamily: "'Inter', sans-serif",
fontSize: 'clamp(2rem, 4vw, 3rem)',
lineHeight: 1.15,
letterSpacing: '-0.02em',
}}
>
{t('landing_v2.product_h2')}
</h2>
<p className="text-[#5B6660] text-base leading-relaxed mb-6 max-w-[48ch]">
{t('landing_v2.product_body')}
</p>
<ul className="space-y-3">
{bullets.map((b) => (
<li key={b} className="flex items-start gap-2 text-[#0E1A14] text-sm">
<span className="text-[#1F4D38] font-medium mt-0.5">·</span>
{t(b)}
</li>
))}
</ul>
</div>
{/* Right: screenshot placeholder */}
<div className="relative">
<div
className="overflow-hidden bg-[#FAFAF7] flex items-center justify-center"
style={{
borderRadius: '12px',
boxShadow:
'0 30px 60px -20px rgba(14,26,20,0.25), 0 0 0 1px rgba(14,26,20,0.06)',
aspectRatio: '16/9',
}}
>
<p className="text-[#5B6660] text-sm">
Screenshot Cesium viewer — pendiente de captura
</p>
</div>
</div>
</div>
</section>
);
};

Note: The screenshot image is a blocking asset per spec §16. The placeholder <p> must be replaced with an <img> once the screenshot is generated. The aspectRatio: '16/9' preserves layout.


File: Create apps/host/src/pages/landing/blocks/OpenCoreSection.tsx

  • Step 1: Write the component
import React from 'react';
import { ArrowRight } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useI18n } from '@/context/I18nContext';
const ossItems = [
'landing_v2.opencore_oss_1',
'landing_v2.opencore_oss_2',
'landing_v2.opencore_oss_3',
'landing_v2.opencore_oss_4',
'landing_v2.opencore_oss_5',
];
const cloudItems = [
'landing_v2.opencore_cloud_1',
'landing_v2.opencore_cloud_2',
'landing_v2.opencore_cloud_3',
'landing_v2.opencore_cloud_4',
'landing_v2.opencore_cloud_5',
];
export const OpenCoreSection: React.FC = () => {
const { t } = useI18n();
const navigate = useNavigate();
return (
<section className="bg-white" style={{ padding: '8rem 2rem' }}>
<div className="max-w-[1200px] mx-auto">
<p className="font-mono-landing text-[13px] tracking-[0.15em] uppercase text-[#5B6660] mb-4">
{t('landing_v2.opencore_eyebrow')}
</p>
<h2
className="text-[#0E1A14] font-semibold mb-6 max-w-[20ch]"
style={{
fontFamily: "'Inter', sans-serif",
fontSize: 'clamp(2rem, 4vw, 3rem)',
lineHeight: 1.15,
letterSpacing: '-0.02em',
}}
>
{t('landing_v2.opencore_h2')}
</h2>
<p className="text-[#5B6660] text-base leading-relaxed mb-12 max-w-[64ch]">
{t('landing_v2.opencore_intro')}
</p>
{/* Two columns */}
<div className="grid md:grid-cols-2 gap-0">
{/* Left: nkz-os */}
<div className="pr-0 md:pr-12">
<h3
className="text-[#0E1A14] font-semibold mb-6"
style={{ fontFamily: "'Inter', sans-serif", fontSize: '22px' }}
>
{t('landing_v2.opencore_oss_title')}
</h3>
<ul className="space-y-3.5 mb-8">
{ossItems.map((k) => (
<li key={k} className="text-[#0E1A14] text-[15px]">
{t(k)}
</li>
))}
</ul>
<a
href="https://nkz-os.org"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-[#1F4D38] font-medium text-sm hover:underline group"
>
{t('landing_v2.opencore_oss_cta')}
<ArrowRight className="h-4 w-4 group-hover:translate-x-0.5 transition-transform" />
</a>
</div>
{/* Divider */}
<div className="hidden md:block w-px bg-[rgba(14,26,20,0.08)] mx-0 absolute left-1/2 self-stretch" />
{/* Right: Nekazari Cloud */}
<div className="pt-8 md:pt-0 md:pl-12 border-t md:border-t-0 border-[rgba(14,26,20,0.08)]">
<h3
className="text-[#0E1A14] font-semibold mb-6"
style={{ fontFamily: "'Inter', sans-serif", fontSize: '22px' }}
>
{t('landing_v2.opencore_cloud_title')}
</h3>
<ul className="space-y-3.5 mb-8">
{cloudItems.map((k) => (
<li key={k} className="text-[#0E1A14] text-[15px]">
{t(k)}
</li>
))}
</ul>
<button
onClick={() => navigate('/register')}
className="inline-flex items-center gap-1.5 text-[#1F4D38] font-medium text-sm hover:underline group"
>
{t('landing_v2.opencore_cloud_cta')}
<ArrowRight className="h-4 w-4 group-hover:translate-x-0.5 transition-transform" />
</button>
</div>
</div>
</div>
</section>
);
};

Fix: The divider approach above is flawed — absolute inside a grid. Replace the grid layout with a flex-based approach. In the actual implementation, use:

<div className="max-w-[1200px] mx-auto relative flex flex-col md:flex-row">
{/* Left column */}
<div className="flex-1 md:pr-16">...</div>
{/* Divider */}
<div className="hidden md:block w-px bg-[rgba(14,26,20,0.08)] self-stretch" />
{/* Right column */}
<div className="flex-1 md:pl-16 pt-8 md:pt-0 border-t md:border-t-0 border-[rgba(14,26,20,0.08)]">...</div>
</div>

File: Create apps/host/src/pages/landing/blocks/EcosystemModules.tsx

  • Step 1: Write the component
import React from 'react';
import { ArrowRight, Leaf, Mountain, Route, BarChart3, Radio, Shield, MessageCircle, Settings } from 'lucide-react';
import { useI18n } from '@/context/I18nContext';
interface Module {
id: string;
name: string;
descKey: string;
icon: React.ReactNode;
url: string;
}
const MODULES: Module[] = [
{ id: 'vegetation', name: 'Vegetation Prime', descKey: 'landing_v2.ecosystem_mod_vegetation', icon: <Leaf className="h-8 w-8" />, url: 'https://nkz-os.org/modules/vegetation' },
{ id: 'lidar', name: 'LiDAR', descKey: 'landing_v2.ecosystem_mod_lidar', icon: <Mountain className="h-8 w-8" />, url: 'https://nkz-os.org/modules/lidar' },
{ id: 'gis-routing', name: 'GIS Routing', descKey: 'landing_v2.ecosystem_mod_gis', icon: <Route className="h-8 w-8" />, url: 'https://nkz-os.org/modules/gis-routing' },
{ id: 'datahub', name: 'DataHub', descKey: 'landing_v2.ecosystem_mod_datahub', icon: <BarChart3 className="h-8 w-8" />, url: 'https://nkz-os.org/modules/datahub' },
{ id: 'iot', name: 'IoT', descKey: 'landing_v2.ecosystem_mod_iot', icon: <Radio className="h-8 w-8" />, url: 'https://nkz-os.org/modules/iot' },
{ id: 'vpn', name: 'VPN', descKey: 'landing_v2.ecosystem_mod_vpn', icon: <Shield className="h-8 w-8" />, url: 'https://nkz-os.org/modules/vpn' },
{ id: 'zulip', name: 'Zulip', descKey: 'landing_v2.ecosystem_mod_zulip', icon: <MessageCircle className="h-8 w-8" />, url: 'https://nkz-os.org/modules/zulip' },
{ id: 'cue', name: 'CUE', descKey: 'landing_v2.ecosystem_mod_cue', icon: <Settings className="h-8 w-8" />, url: 'https://nkz-os.org/modules/cue' },
];
export const EcosystemModules: React.FC = () => {
const { t } = useI18n();
return (
<section className="bg-[#FAFAF7]" style={{ padding: '8rem 2rem' }}>
<div className="max-w-[1200px] mx-auto">
<p className="font-mono-landing text-[13px] tracking-[0.15em] uppercase text-[#5B6660] mb-4">
{t('landing_v2.ecosystem_eyebrow')}
</p>
<h2
className="text-[#0E1A14] font-semibold mb-4"
style={{
fontFamily: "'Inter', sans-serif",
fontSize: 'clamp(2rem, 4vw, 3rem)',
lineHeight: 1.15,
letterSpacing: '-0.02em',
}}
>
{t('landing_v2.ecosystem_h2')}
</h2>
<p className="text-[#5B6660] text-base mb-12">
{t('landing_v2.ecosystem_sub')}
</p>
{/* Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8 mb-10">
{MODULES.map((mod) => (
<a
key={mod.id}
href={mod.url}
target="_blank"
rel="noopener noreferrer"
className="block p-6 rounded-lg border border-[rgba(14,26,20,0.08)] hover:border-[rgba(14,26,20,0.18)] hover:bg-[#FAFAF7] transition-all duration-200"
style={{ textDecoration: 'none' }}
>
<div className="text-[#1F4D38] mb-4">{mod.icon}</div>
<h3
className="text-[#0E1A14] font-semibold mb-2"
style={{ fontFamily: "'Inter', sans-serif", fontSize: '18px' }}
>
{mod.name}
</h3>
<p className="text-[#5B6660] text-sm leading-relaxed mb-4 line-clamp-2">
{t(mod.descKey)}
</p>
<div className="flex gap-2">
<span className="font-mono-landing text-[11px] uppercase tracking-[0.1em] text-[#5B6660] border border-[rgba(14,26,20,0.08)] rounded px-2 py-0.5">
{t('landing_v2.ecosystem_badge_oss')}
</span>
<span className="font-mono-landing text-[11px] uppercase tracking-[0.1em] text-[#5B6660] border border-[rgba(14,26,20,0.08)] rounded px-2 py-0.5">
{t('landing_v2.ecosystem_badge_cloud')}
</span>
</div>
</a>
))}
</div>
{/* CTA */}
<a
href="https://nkz-os.org/modules"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-[#1F4D38] font-medium text-sm hover:underline group"
>
{t('landing_v2.ecosystem_cta')}
<ArrowRight className="h-4 w-4 group-hover:translate-x-0.5 transition-transform" />
</a>
</div>
</section>
);
};

File: Create apps/host/src/pages/landing/blocks/OpenStandards.tsx

  • Step 1: Write the component
import React from 'react';
import { useI18n } from '@/context/I18nContext';
const STANDARDS = 'FIWARE NGSI-LD · Smart Data Models · Keycloak (OIDC) · Kubernetes · TimescaleDB · MQTT · OAuth 2.0 · MinIO (S3)';
export const OpenStandards: React.FC = () => {
const { t } = useI18n();
return (
<section className="bg-[#FAFAF7]" style={{ padding: '4rem 2rem' }}>
<div className="max-w-[1200px] mx-auto text-center">
<p className="font-mono-landing text-[13px] tracking-[0.15em] uppercase text-[#5B6660] mb-4">
{t('landing_v2.standards_eyebrow')}
</p>
<h2
className="text-[#0E1A14] font-semibold mb-8"
style={{ fontSize: '28px', letterSpacing: '-0.02em' }}
>
{t('landing_v2.standards_h2')}
</h2>
<p
className="font-mono-landing text-lg text-[#5B6660] leading-relaxed max-w-[48ch] mx-auto"
>
{STANDARDS}
</p>
</div>
</section>
);
};

File: Create apps/host/src/pages/landing/blocks/FinalCTA.tsx

  • Step 1: Write the component
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowRight } from 'lucide-react';
import { useI18n } from '@/context/I18nContext';
export const FinalCTA: React.FC = () => {
const { t } = useI18n();
const navigate = useNavigate();
return (
<section className="bg-[#0E1A14]" style={{ padding: '8rem 2rem' }}>
<div className="max-w-[1200px] mx-auto text-center">
<h2
className="text-white font-semibold mb-8"
style={{
fontFamily: "'Inter', sans-serif",
fontSize: 'clamp(2rem, 5vw, 3rem)',
lineHeight: 1.15,
letterSpacing: '-0.02em',
}}
>
{t('landing_v2.cta_final_h2')}
</h2>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<button
onClick={() => navigate('/register')}
className="inline-flex items-center px-7 py-3.5 bg-white text-[#0E1A14] font-semibold rounded-lg hover:-translate-y-px transition-all duration-200"
style={{ fontSize: '1rem' }}
>
{t('landing_v2.cta_final_primary')}
</button>
<a
href={`mailto:${(window as any).__ENV__?.SALES_EMAIL || 'info@nekazari.com'}`}
className="inline-flex items-center gap-1.5 text-white/70 font-medium hover:text-white transition-colors group"
style={{ fontSize: '1rem' }}
>
{t('landing_v2.cta_final_secondary')}
<ArrowRight className="h-4 w-4 group-hover:translate-x-0.5 transition-transform" />
</a>
</div>
</div>
</section>
);
};

File: Create apps/host/src/pages/landing/blocks/LandingFooter.tsx

  • Step 1: Write the component
import React from 'react';
import { Globe } from 'lucide-react';
import { useI18n } from '@/context/I18nContext';
import { NkzAttribution } from '@/components/attribution/NkzAttribution';
interface Props {
language: string;
supportedLanguages: Record<string, string>;
showLanguageMenu: boolean;
setShowLanguageMenu: (v: boolean) => void;
onLanguageChange: (lang: string) => void;
}
export const LandingFooter: React.FC<Props> = ({
language,
supportedLanguages,
showLanguageMenu,
setShowLanguageMenu,
onLanguageChange,
}) => {
const { t } = useI18n();
return (
<footer className="bg-[#0E1A14] text-[#A8B1AC]" style={{ padding: '5rem 2rem 3rem' }}>
<div className="max-w-[1200px] mx-auto">
{/* Top row: wordmark + tagline */}
<div className="mb-12">
<span className="text-[#FAFAF7] text-xl font-semibold">NKZ</span>
<p className="text-sm mt-2">{t('landing_v2.footer_tagline')}</p>
</div>
{/* Columns */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 mb-12">
{/* Producto */}
<div>
<h4 className="text-[#FAFAF7] font-medium text-sm mb-4">{t('landing_v2.footer_col1_title')}</h4>
<ul className="space-y-2 text-sm">
<li><a href="https://nkz-os.org/modules" className="hover:text-[#FAFAF7] transition-colors">{t('landing_v2.footer_col1_1')}</a></li>
<li><a href="#pricing" className="hover:text-[#FAFAF7] transition-colors">{t('landing_v2.footer_col1_2')}</a></li>
<li><a href="https://nkz-os.org/docs" className="hover:text-[#FAFAF7] transition-colors" target="_blank" rel="noopener noreferrer">{t('landing_v2.footer_col1_3')}</a></li>
<li><a href="https://nkz-os.org/status" className="hover:text-[#FAFAF7] transition-colors" target="_blank" rel="noopener noreferrer">{t('landing_v2.footer_col1_4')}</a></li>
</ul>
</div>
{/* Open source */}
<div>
<h4 className="text-[#FAFAF7] font-medium text-sm mb-4">{t('landing_v2.footer_col2_title')}</h4>
<ul className="space-y-2 text-sm">
<li><a href="https://github.com/nkz-os/nkz" className="hover:text-[#FAFAF7] transition-colors" target="_blank" rel="noopener noreferrer">{t('landing_v2.footer_col2_1')}</a></li>
<li><a href="https://nkz-os.org" className="hover:text-[#FAFAF7] transition-colors" target="_blank" rel="noopener noreferrer">{t('landing_v2.footer_col2_2')}</a></li>
<li><a href="https://nkz-os.org/community" className="hover:text-[#FAFAF7] transition-colors" target="_blank" rel="noopener noreferrer">{t('landing_v2.footer_col2_3')}</a></li>
<li><a href="https://nkz-os.org/roadmap" className="hover:text-[#FAFAF7] transition-colors" target="_blank" rel="noopener noreferrer">{t('landing_v2.footer_col2_4')}</a></li>
</ul>
</div>
{/* Empresa */}
<div>
<h4 className="text-[#FAFAF7] font-medium text-sm mb-4">{t('landing_v2.footer_col3_title')}</h4>
<ul className="space-y-2 text-sm">
<li><a href="https://nkz-os.org/about" className="hover:text-[#FAFAF7] transition-colors" target="_blank" rel="noopener noreferrer">{t('landing_v2.footer_col3_1')}</a></li>
<li><a href={`mailto:${(window as any).__ENV__?.SUPPORT_EMAIL || 'info@nekazari.com'}`} className="hover:text-[#FAFAF7] transition-colors">{t('landing_v2.footer_col3_2')}</a></li>
<li><a href="https://nkz-os.org/privacy" className="hover:text-[#FAFAF7] transition-colors" target="_blank" rel="noopener noreferrer">{t('landing_v2.footer_col3_3')}</a></li>
<li><a href="https://nkz-os.org/terms" className="hover:text-[#FAFAF7] transition-colors" target="_blank" rel="noopener noreferrer">{t('landing_v2.footer_col3_4')}</a></li>
</ul>
</div>
{/* Empty spacer for 4-col grid balance */}
<div />
</div>
{/* Bottom bar */}
<div className="border-t border-[rgba(168,177,172,0.15)] pt-6 flex flex-col sm:flex-row items-center justify-between gap-4 text-xs">
<span>{t('landing_v2.footer_copyright')}</span>
<div className="flex items-center gap-4">
<NkzAttribution variant="commercial" />
{/* Language selector */}
<div className="relative">
<button
onClick={() => setShowLanguageMenu(!showLanguageMenu)}
className="inline-flex items-center gap-1 text-[#A8B1AC] hover:text-[#FAFAF7] transition-colors"
>
<Globe className="h-3.5 w-3.5" />
{supportedLanguages[language] || 'ES'}
</button>
{showLanguageMenu && (
<>
<div className="absolute bottom-full right-0 mb-2 w-36 rounded-lg shadow-lg bg-white ring-1 ring-black/5 z-20 overflow-hidden">
{Object.entries(supportedLanguages).map(([code, name]) => (
<button
key={code}
onClick={() => onLanguageChange(code)}
className={`block w-full text-left px-3 py-2 text-xs transition-colors ${
language === code
? 'bg-green-50 text-green-900 font-medium'
: 'text-gray-700 hover:bg-gray-50'
}`}
>
{name as string}
</button>
))}
</div>
<div className="fixed inset-0 z-10" onClick={() => setShowLanguageMenu(false)} />
</>
)}
</div>
</div>
</div>
</div>
</footer>
);
};

Task 14: Update PricingCards (remove gradients, add Free tier)

Section titled “Task 14: Update PricingCards (remove gradients, add Free tier)”

File: Modify apps/host/src/components/pricing/PricingCards.tsx

  • Step 1: Rewrite PricingCards

Replace the entire file:

import React from 'react';
import { useI18n } from '@/context/I18nContext';
import { Check } from 'lucide-react';
export const PricingCards: React.FC = () => {
const { t } = useI18n();
return (
<div className="max-w-[1200px] mx-auto px-8 py-24" id="pricing">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-[#0E1A14] mb-4">
{t('landing.pricing.title') || 'Planes adaptados a tu terreno'}
</h2>
<p className="text-xl text-[#5B6660] max-w-3xl mx-auto">
{t('landing.pricing.subtitle') || 'Comienza gratis durante 45 días y mejora cuando lo necesites.'}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
{/* Free Tier */}
<div className="bg-white rounded-lg border border-[rgba(14,26,20,0.08)] p-8">
<h3 className="text-2xl font-bold text-[#0E1A14] mb-2">Free</h3>
<div className="flex items-baseline mb-4">
<span className="text-4xl font-extrabold text-[#0E1A14]">0€</span>
<span className="text-xl text-[#5B6660] ml-2">/mes</span>
</div>
<p className="text-[#5B6660] mb-6">Para empezar sin compromiso.</p>
<ul className="space-y-3 mb-8">
<li className="flex items-start"><Check className="h-5 w-5 text-[#1F4D38] mr-3 shrink-0 mt-0.5" /><span className="text-[#0E1A14] text-sm">1 parcela · 1 usuario</span></li>
<li className="flex items-start"><Check className="h-5 w-5 text-[#1F4D38] mr-3 shrink-0 mt-0.5" /><span className="text-[#0E1A14] text-sm">Módulos esenciales</span></li>
<li className="flex items-start"><Check className="h-5 w-5 text-[#1F4D38] mr-3 shrink-0 mt-0.5" /><span className="text-[#0E1A14] text-sm">Soporte comunidad</span></li>
</ul>
<a
href="/register"
className="block w-full py-3 px-6 text-center rounded-lg bg-white text-[#1F4D38] font-semibold border-2 border-[#1F4D38] hover:bg-[#FAFAF7] transition-colors"
>
Crear cuenta gratis
</a>
</div>
{/* Pro Tier */}
<div className="bg-white rounded-lg border border-[rgba(14,26,20,0.08)] p-8">
<h3 className="text-2xl font-bold text-[#0E1A14] mb-2">{t('landing.pricing.pro.title') || 'Pro'}</h3>
<div className="flex items-baseline mb-4">
<span className="text-4xl font-extrabold text-[#0E1A14]">{t('landing.pricing.pro.price') || '49€'}</span>
<span className="text-xl text-[#5B6660] ml-2">{t('landing.pricing.pro.period') || '/mes'}</span>
</div>
<p className="text-[#5B6660] mb-6">{t('landing.pricing.pro.desc') || 'For professional agronomists and mid-size farms.'}</p>
<ul className="space-y-3 mb-8">
<li className="flex items-start"><Check className="h-5 w-5 text-[#1F4D38] mr-3 shrink-0 mt-0.5" /><span className="text-[#0E1A14] text-sm">{t('landing.pricing.pro.feat1') || '45-day free trial'}</span></li>
<li className="flex items-start"><Check className="h-5 w-5 text-[#1F4D38] mr-3 shrink-0 mt-0.5" /><span className="text-[#0E1A14] text-sm">{t('landing.pricing.pro.feat2') || 'Up to 500 hectares and 5 users'}</span></li>
<li className="flex items-start"><Check className="h-5 w-5 text-[#1F4D38] mr-3 shrink-0 mt-0.5" /><span className="text-[#0E1A14] text-sm">{t('landing.pricing.pro.feat3') || 'All modules included'}</span></li>
<li className="flex items-start"><Check className="h-5 w-5 text-[#1F4D38] mr-3 shrink-0 mt-0.5" /><span className="text-[#0E1A14] text-sm">{t('landing.pricing.pro.feat4') || 'Priority support'}</span></li>
</ul>
<a
href="/register"
className="block w-full py-3 px-6 text-center rounded-lg bg-[#1F4D38] text-white font-semibold hover:bg-[#163A2A] transition-colors"
>
{t('landing.pricing.pro.cta') || 'Start Free Trial'}
</a>
</div>
{/* Enterprise Tier */}
<div className="bg-white rounded-lg border border-[rgba(14,26,20,0.08)] p-8">
<h3 className="text-2xl font-bold text-[#0E1A14] mb-2">{t('landing.pricing.ent.title') || 'Enterprise'}</h3>
<div className="flex items-baseline mb-4">
<span className="text-4xl font-extrabold text-[#0E1A14]">{t('landing.pricing.ent.price') || 'Custom'}</span>
</div>
<p className="text-[#5B6660] mb-6">{t('landing.pricing.ent.desc') || 'For cooperatives, large estates and institutions.'}</p>
<ul className="space-y-3 mb-8">
<li className="flex items-start"><Check className="h-5 w-5 text-[#1F4D38] mr-3 shrink-0 mt-0.5" /><span className="text-[#0E1A14] text-sm">{t('landing.pricing.ent.feat1') || 'Unlimited hectares and users'}</span></li>
<li className="flex items-start"><Check className="h-5 w-5 text-[#1F4D38] mr-3 shrink-0 mt-0.5" /><span className="text-[#0E1A14] text-sm">{t('landing.pricing.ent.feat2') || 'All modules included (AI, Lidar, etc.)'}</span></li>
<li className="flex items-start"><Check className="h-5 w-5 text-[#1F4D38] mr-3 shrink-0 mt-0.5" /><span className="text-[#0E1A14] text-sm">{t('landing.pricing.ent.feat3') || 'Dedicated tenant isolation'}</span></li>
<li className="flex items-start"><Check className="h-5 w-5 text-[#1F4D38] mr-3 shrink-0 mt-0.5" /><span className="text-[#0E1A14] text-sm">{t('landing.pricing.ent.feat4') || 'SLA and account manager'}</span></li>
</ul>
<a
href={`mailto:${(window as any).__ENV__?.SALES_EMAIL || 'sales@example.com'}`}
className="block w-full py-3 px-6 text-center rounded-lg bg-white text-[#0E1A14] border-2 border-[rgba(14,26,20,0.18)] font-semibold hover:border-[#0E1A14] transition-colors"
>
{t('landing.pricing.ent.cta') || 'Contact Sales'}
</a>
</div>
</div>
</div>
);
};

Key changes:

  • Removed bg-gradient-to-r from buttons → flat bg-[#1F4D38]
  • Removed shadow-xl, hover:scale-105, hover:-translate-y-2
  • Removed “Most Popular” badge
  • Removed rounded-3xlrounded-lg
  • Added Free tier (3-cols instead of 2)
  • border-[rgba(14,26,20,0.08)] instead of border-green-500

Task 15: Update PartnerLogos (grayscale + eyebrow)

Section titled “Task 15: Update PartnerLogos (grayscale + eyebrow)”

File: Modify apps/host/src/components/partners/PartnerLogos.tsx

  • Step 1: Update styling and add eyebrow key

Replace the component return:

export const PartnerLogos: React.FC = () => {
const { t } = useI18n();
const partners = React.useMemo(() => getPartners(), []);
if (partners.length === 0) return null;
return (
<div className="w-full bg-white py-16">
<div className="max-w-[1200px] mx-auto px-8">
<p className="font-mono-landing text-center text-[13px] tracking-[0.15em] uppercase text-[#5B6660] mb-10">
{t('landing_v2.partners_eyebrow')}
</p>
<div className="flex flex-wrap justify-center items-center gap-12 md:gap-24">
{partners.map((partner, index) => (
<a
key={index}
href={partner.url}
target="_blank"
rel="noopener noreferrer"
title={partner.name}
className="group block"
>
<img
src={partner.logo}
alt={partner.name}
loading="lazy"
className="max-h-16 w-auto object-contain opacity-60 grayscale group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-300"
/>
</a>
))}
</div>
</div>
</div>
);
};

Key changes:

  • Removed transform hover:scale-110
  • Added grayscale default, group-hover:grayscale-0
  • Changed eyebrow from t('landing.partners_title') to t('landing_v2.partners_eyebrow')
  • Changed opacity from 0.7 to 0.6
  • No H2, just eyebrow (logos speak per spec)

Task 16: Rewrite CommercialLanding.tsx as thin orchestrator

Section titled “Task 16: Rewrite CommercialLanding.tsx as thin orchestrator”

File: Rewrite apps/host/src/pages/landing/CommercialLanding.tsx

  • Step 1: Replace entire file
import React, { useState, useEffect } from 'react';
import { useI18n } from '@/context/I18nContext';
import { CookieBanner } from '@/components/CookieBanner';
import { PricingCards } from '@/components/pricing/PricingCards';
import { PartnerLogos } from '@/components/partners/PartnerLogos';
import { HeroTopBar } from './blocks/HeroTopBar';
import { HeroSection } from './blocks/HeroSection';
import { ProofBand } from './blocks/ProofBand';
import { ProductAnchor } from './blocks/ProductAnchor';
import { OpenCoreSection } from './blocks/OpenCoreSection';
import { EcosystemModules } from './blocks/EcosystemModules';
import { OpenStandards } from './blocks/OpenStandards';
import { FinalCTA } from './blocks/FinalCTA';
import { LandingFooter } from './blocks/LandingFooter';
export const CommercialLanding: React.FC = () => {
const { t, setLanguage, language, supportedLanguages } = useI18n();
const [showLanguageMenu, setShowLanguageMenu] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > window.innerHeight * 0.5);
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const handleLanguageChange = async (lang: string) => {
await setLanguage(lang as any);
setShowLanguageMenu(false);
};
return (
<div className="min-h-screen bg-white">
<CookieBanner />
<HeroTopBar
isScrolled={isScrolled}
language={language}
supportedLanguages={supportedLanguages}
showLanguageMenu={showLanguageMenu}
setShowLanguageMenu={setShowLanguageMenu}
onLanguageChange={handleLanguageChange}
/>
{/* Block 2: Hero */}
<HeroSection />
{/* Block 3: Proof band */}
<ProofBand />
{/* Block 4: Product anchor */}
<ProductAnchor />
{/* Block 5: Open Core */}
<OpenCoreSection />
{/* Block 6: Ecosystem modules */}
<EcosystemModules />
{/* Block 7: Open standards */}
<OpenStandards />
{/* Block 8: Pricing */}
<PricingCards />
{/* Block 9: Partners */}
<PartnerLogos />
{/* Block 10: Final CTA */}
<FinalCTA />
{/* Block 11: Footer */}
<LandingFooter
language={language}
supportedLanguages={supportedLanguages}
showLanguageMenu={showLanguageMenu}
setShowLanguageMenu={setShowLanguageMenu}
onLanguageChange={handleLanguageChange}
/>
</div>
);
};

Scrolling threshold: Changed from 50px to 50% of viewport height per spec §4.5 (top bar transitions after scrolling past half the hero).


Task 17: Drop fallback i18n keys in remaining 4 locales

Section titled “Task 17: Drop fallback i18n keys in remaining 4 locales”

Files: Modify apps/host/public/locales/{ca,eu,fr,pt}/common.json

  • Step 1: Copy English landing_v2 block into each locale file

For each of ca, eu, fr, pt: add the English landing_v2 block as a fallback. This ensures t('landing_v2.*') returns English strings (not the raw key) in unsupported languages.


Manual verification checklist:

  • Step 1: Start dev server
Terminal window
cd nkz/apps/host && pnpm dev
  • Step 2: Visual checks

    • Scroll through all 12 blocks: Hero → Proof → Product → OpenCore → Modules → Standards → Pricing → Partners → FinalCTA → Footer
    • Top bar transitions from transparent → white after scrolling past 50vh
    • Cookie banner appears
    • Language selector works
    • Login button works
    • CTA buttons navigate to /register
    • No horizontal overflow at 360px, 768px, 1024px, 1440px, 1920px
    • Video autoplays on desktop, poster renders on mobile (< 768px)
  • Step 3: Reduced motion

    • Enable prefers-reduced-motion: reduce in DevTools
    • Verify: video hidden, no animations
  • Step 4: Run type-check

Terminal window
cd nkz/apps/host && pnpm tsc --noEmit
  • Step 5: Run build
Terminal window
cd nkz/apps/host && pnpm build

Spec §RequirementCovered by
§312-block IA orderTask 16 (CommercialLanding.tsx)
§4.1Hero composition (video, overlay, text, CTAs)Task 6 (HeroSection)
§4.2Video sources, poster, mobile fallbackTask 6, Task 0
§4.3Overlay gradientTask 6
§4.4Hero content (eyebrow, H1, sub, CTAs)Task 6
§4.5Top bar (transparent → white)Task 4 (HeroTopBar)
§4.6Scroll indicatorTask 5 (ScrollIndicator)
§5Proof band (4 metrics)Task 7 (ProofBand)
§6Product anchor + Cesium screenshotTask 8 (ProductAnchor)
§7Open core vs managed cloud (2-col)Task 9 (OpenCoreSection)
§8Ecosystem modules grid (4×2)Task 10 (EcosystemModules)
§9Open standards bandTask 11 (OpenStandards)
§10Pricing (3 tiers, no gradients)Task 14 (PricingCards rewrite)
§11Partners (grayscale, eyebrow)Task 15 (PartnerLogos update)
§12Final CTA (dark block)Task 12 (FinalCTA)
§13Footer (nkz-os.org coherent)Task 13 (LandingFooter)
§14Visual system (fonts, palette, shadows, motion)Tasks 1-2 (fonts, CSS) + all components inline
§15Video pipeline (assets, perf, a11y)Task 0 (assets), Task 6 (perf/a11y)
§16Blocking assetsTask 0 (video), Task 8 note (screenshot placeholder)
§17Tech notes (i18n, routing, auth)Tasks 3, 16
§18.1-10Acceptance criteriaTask 18 (verification)

Gaps identified:

  • Cesium screenshot is placeholder — blocking asset per spec §16, needs capture
  • WebM/poster generation needs ffmpeg (not installed) — user must run
  • Remaining 4 locales get English fallback (acceptable per spec §18.7)

Plan complete. The implementation creates 10 new files in blocks/, rewrites CommercialLanding.tsx and PricingCards.tsx, updates PartnerLogos.tsx, adds i18n keys, and adds font/CSS utilities. Total: ~15 files changed.

Two execution options:

  1. Subagent-Driven (recommended) — Fresh subagent per task, review between tasks, fastest iteration
  2. Inline Execution — Execute tasks sequentially in this session with checkpoints