Initial-commit

This commit is contained in:
Kismet Hasanaj
2026-05-02 23:53:52 +02:00
parent dbf4c80804
commit ec83a0d879
36 changed files with 19889 additions and 1 deletions
+217
View File
@@ -0,0 +1,217 @@
// =============================================================================
// Cookie & Privacy Policy
// =============================================================================
//
// A plain, honest policy page describing exactly what (very little) is
// stored in the visitor's browser. Update the `lastUpdated` field below
// whenever the policy changes.
//
// The wording is deliberately written in first-person plain English rather
// than legalese — Novarix is small enough to be transparent and brief.
// =============================================================================
import type { Metadata } from "next";
import Link from "next/link";
import { site } from "@/content";
const lastUpdated = "2 May 2026";
export const metadata: Metadata = {
title: "Cookie & Privacy Policy",
description:
"How Novarix Networks uses cookies and browser storage on this website.",
};
export default function CookiePolicyPage() {
return (
<main className="site-shell min-h-screen">
{/* Ambient background, matching the homepage */}
<div
className="pointer-events-none fixed inset-0 -z-10 overflow-hidden"
aria-hidden="true"
>
<div className="ambient-orb ambient-orb-a" />
<div className="ambient-orb ambient-orb-b" />
<div className="ambient-grid" />
</div>
{/* Lightweight header — just a back-link */}
<header className="border-b border-[var(--border)]">
<div className="mx-auto flex h-20 w-full max-w-3xl items-center px-6 sm:px-8">
<Link
href="/"
className="inline-flex items-center gap-2 text-sm text-[var(--text-soft)] transition-colors hover:text-[var(--text)]"
>
<span aria-hidden="true"></span>
Back to Novarix Networks
</Link>
</div>
</header>
<article className="mx-auto w-full max-w-3xl px-6 py-16 sm:px-8 sm:py-24">
<p className="text-xs font-semibold tracking-[0.18em] text-[var(--accent)] uppercase">
Legal
</p>
<h1 className="mt-3 text-[clamp(2rem,4vw,3rem)] leading-[1.05] font-semibold tracking-[-0.03em]">
Cookie &amp; Privacy Policy
</h1>
<p className="mt-4 text-sm text-[var(--text-soft)]">
Last updated {lastUpdated}
</p>
<div className="mt-12 space-y-10 text-base leading-relaxed text-[var(--text-soft)]">
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
In short
</h2>
<p className="mt-3">
This website does not run analytics, advertising, or any
third-party tracking. We do not sell, share, or transfer
personal data to anyone. The only things we store in your
browser are listed below and you can decline them using the
banner that appears on your first visit.
</p>
</section>
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
What we store
</h2>
<p className="mt-3">
We use two small browser storage entries both first-party,
both confined to your browser, neither shared with anyone:
</p>
<div className="mt-6 space-y-4">
<div className="rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-5 backdrop-blur sm:p-6">
<p className="font-mono text-sm font-semibold text-[var(--text)]">
novarix-cookie-consent
</p>
<p className="mt-2 text-sm">
<span className="font-semibold text-[var(--text)]">
Strictly necessary.
</span>{" "}
Stored in <code>localStorage</code>. Records your choice
from the cookie banner so we don&apos;t ask you again on
every page. Persists until you clear your browser data.
</p>
</div>
<div className="rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-5 backdrop-blur sm:p-6">
<p className="font-mono text-sm font-semibold text-[var(--text)]">
novarix-intro-seen
</p>
<p className="mt-2 text-sm">
<span className="font-semibold text-[var(--text)]">
Preference.
</span>{" "}
Stored in <code>sessionStorage</code> only when you accept
cookies. Lets us skip the animated logo intro for the rest
of your browsing session. Cleared automatically when you
close the browser tab. If you decline cookies, this is
never set and the intro may play again on a fresh tab.
</p>
</div>
</div>
</section>
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
What we don&apos;t do
</h2>
<p className="mt-3">
We don&apos;t set tracking cookies. We don&apos;t run Google
Analytics, Plausible, Fathom, Matomo, or any equivalent. We
don&apos;t serve advertising. We don&apos;t embed third-party
scripts that profile you across websites. We don&apos;t use
session-replay tools.
</p>
</section>
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
Server logs
</h2>
<p className="mt-3">
Our web server keeps short-lived access logs (IP address,
request path, timestamp, user agent) for the operational
purposes of running the site diagnosing errors, blocking
abuse, and basic capacity planning. Logs are retained for no
longer than 30 days and are not used to build profiles of
individual visitors.
</p>
</section>
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
Changing your mind
</h2>
<p className="mt-3">
Use the{" "}
<span className="font-semibold text-[var(--text)]">
Cookie preferences
</span>{" "}
link in the footer of any page to re-open the banner and
change your decision. Or clear this site&apos;s storage in
your browser settings to be asked again from scratch.
</p>
</section>
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
Who we are
</h2>
<p className="mt-3">
{site.footer.company}, {site.footer.registered.toLowerCase()}.
For privacy questions or to exercise your rights under UK GDPR
(access, correction, deletion, portability, objection), email
us at{" "}
<a
href={`mailto:${site.footer.email}`}
className="font-medium text-[var(--text)] underline-offset-4 hover:underline"
>
{site.footer.email}
</a>
.
</p>
</section>
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
Complaints
</h2>
<p className="mt-3">
If you believe we&apos;ve mishandled your personal data, you
have the right to complain to the UK&apos;s data protection
authority, the Information Commissioner&apos;s Office (ICO),
at{" "}
<a
href="https://ico.org.uk/make-a-complaint/"
className="font-medium text-[var(--text)] underline-offset-4 hover:underline"
target="_blank"
rel="noreferrer"
>
ico.org.uk
</a>
.
</p>
</section>
</div>
</article>
<footer className="border-t border-[var(--border)] py-12">
<div className="mx-auto flex w-full max-w-3xl flex-col gap-2 px-6 text-sm text-[var(--text-soft)] sm:flex-row sm:items-center sm:justify-between sm:px-8">
<span>
© {new Date().getFullYear()} {site.footer.company}
</span>
<Link
href="/"
className="transition-colors hover:text-[var(--accent)]"
>
Back to home
</Link>
</div>
</footer>
</main>
);
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

+338
View File
@@ -0,0 +1,338 @@
/* ============================================================================
Novarix Networks — Global stylesheet
============================================================================
This file controls the visual *theme* of the site (colours, fonts,
background effects, animations). Most page layout is done with Tailwind
utility classes directly inside `app/page.tsx` — this file only handles
things that don't fit neatly into utilities.
What's in here, top to bottom:
1. Design tokens — brand colours, font, shadows (@theme block)
2. Theme variables — light + dark mode CSS variables
3. Base styles — body font, focus rings, text selection
4. Site shell + ambient — background gradient, floating orbs, grid
5. Brand wordmark swap — light/dark logo switcher
6. Intro overlay — first-visit animated logo
7. Animations — keyframes used above
8. Reduced motion — respect "prefers-reduced-motion" setting
To change the brand colour palette, edit the `--color-brand-*` lines in
the @theme block below. Everything else cascades from those.
============================================================================ */
@import "tailwindcss";
/* ---------------------------------------------------------------------------
1. Design tokens — registered with Tailwind v4 via @theme.
These become utility classes (e.g. `bg-brand-500`, `text-ink-900`) and
custom CSS variables you can use anywhere in the stylesheet.
--------------------------------------------------------------------------- */
@theme {
--font-sans:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
--color-brand-50: oklch(0.97 0.015 240);
--color-brand-100: oklch(0.93 0.04 240);
--color-brand-200: oklch(0.86 0.07 240);
--color-brand-300: oklch(0.78 0.11 240);
--color-brand-400: oklch(0.7 0.14 240);
--color-brand-500: oklch(0.62 0.16 240);
--color-brand-600: oklch(0.54 0.18 245);
--color-brand-700: oklch(0.46 0.18 250);
--color-ink-50: oklch(0.985 0.003 250);
--color-ink-100: oklch(0.96 0.005 250);
--color-ink-200: oklch(0.92 0.008 250);
--color-ink-300: oklch(0.84 0.012 250);
--color-ink-400: oklch(0.65 0.018 250);
--color-ink-500: oklch(0.5 0.02 250);
--color-ink-600: oklch(0.38 0.022 250);
--color-ink-700: oklch(0.28 0.024 250);
--color-ink-800: oklch(0.2 0.026 250);
--color-ink-900: oklch(0.13 0.028 255);
--color-ink-950: oklch(0.08 0.03 260);
--radius-card: 1.25rem;
--shadow-card: 0 1px 0 0 oklch(1 0 0 / 0.04) inset, 0 12px 32px -12px oklch(0 0 0 / 0.18);
}
/* ---------------------------------------------------------------------------
2. Theme variables (light + dark)
These are the runtime CSS variables read by both this file and
app/page.tsx (via classes like `bg-[var(--surface)]`). Each variable has
a light value here and a dark override further down inside the
`@media (prefers-color-scheme: dark)` block.
--------------------------------------------------------------------------- */
:root {
color-scheme: light dark;
--bg: var(--color-ink-50);
--surface: oklch(1 0 0 / 0.7);
--surface-strong: oklch(1 0 0 / 0.92);
--text: var(--color-ink-900);
--text-soft: var(--color-ink-500);
--border: oklch(0.13 0.028 255 / 0.1);
--border-strong: oklch(0.13 0.028 255 / 0.18);
--accent: var(--color-brand-500);
--accent-soft: oklch(0.62 0.16 240 / 0.12);
--ring: oklch(0.62 0.16 240 / 0.4);
--grid: oklch(0.5 0.02 250 / 0.08);
--button-bg: var(--color-ink-900);
--button-fg: var(--color-ink-50);
--button-hover: var(--color-ink-800);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: var(--color-ink-950);
--surface: oklch(0.13 0.028 255 / 0.55);
--surface-strong: oklch(0.13 0.028 255 / 0.85);
--text: var(--color-ink-100);
--text-soft: var(--color-ink-400);
--border: oklch(1 0 0 / 0.08);
--border-strong: oklch(1 0 0 / 0.16);
--accent: var(--color-brand-400);
--accent-soft: oklch(0.7 0.14 240 / 0.18);
--ring: oklch(0.7 0.14 240 / 0.5);
--grid: oklch(0.84 0.012 250 / 0.06);
--button-bg: var(--color-ink-100);
--button-fg: var(--color-ink-950);
--button-hover: var(--color-ink-50);
}
}
/* ---------------------------------------------------------------------------
3. Base styles — applied to plain HTML elements before any classes hit.
--------------------------------------------------------------------------- */
@layer base {
html {
scroll-behavior: smooth;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
overflow-x: hidden;
}
::selection {
background: var(--accent-soft);
color: var(--text);
}
:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 3px;
border-radius: 4px;
}
}
/* ---------------------------------------------------------------------------
4. Site shell + ambient effects
The .site-shell class is on the <main> element in page.tsx. The two
radial gradients here follow the user's mouse via the --mx / --my CSS
variables that page.tsx writes on every mousemove.
--------------------------------------------------------------------------- */
.site-shell {
position: relative;
isolation: isolate;
min-height: 100vh;
background:
radial-gradient(
circle at var(--mx, 50%) var(--my, 22%),
oklch(0.62 0.16 240 / 0.12),
transparent 20%
),
radial-gradient(
circle at calc(var(--mx, 50%) * 0.65) calc(var(--my, 22%) * 1.2),
oklch(0.6 0.14 280 / 0.09),
transparent 24%
),
var(--bg);
}
.ambient-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(to right, var(--grid) 1px, transparent 1px),
linear-gradient(to bottom, var(--grid) 1px, transparent 1px);
background-size: 56px 56px;
mask-image: linear-gradient(to bottom, oklch(0 0 0 / 0.5), transparent 80%);
}
.ambient-orb {
position: absolute;
border-radius: 9999px;
filter: blur(80px);
opacity: 0.22;
animation: drift 18s ease-in-out infinite;
will-change: transform;
}
.ambient-orb-a {
width: 32rem;
height: 32rem;
top: 4rem;
right: -10rem;
background: linear-gradient(
135deg,
oklch(0.7 0.14 240 / 0.55),
oklch(0.6 0.14 200 / 0.1)
);
}
.ambient-orb-b {
width: 26rem;
height: 26rem;
top: 28rem;
left: -8rem;
background: linear-gradient(
135deg,
oklch(0.6 0.14 280 / 0.35),
oklch(0.78 0.11 200 / 0.12)
);
animation-duration: 22s;
}
/* ---------------------------------------------------------------------------
5. Brand wordmark colour-scheme swap
The header logo has two PNG variants — a colour one for light mode and a
white one for dark mode. The `brand-light` / `brand-dark` classes (set
in page.tsx) live on both <Image> tags; only the matching one is shown.
--------------------------------------------------------------------------- */
.brand-light {
display: block;
}
.brand-dark {
display: none;
}
@media (prefers-color-scheme: dark) {
.brand-light {
display: none;
}
.brand-dark {
display: block;
}
}
/* ---------------------------------------------------------------------------
6. Intro overlay
The first-visit animated SVG wordmark. The SVG itself runs a 3-second
stroke-then-fill animation (defined inside
/public/branding/animated_logo_intro.svg). The overlay fades out at the
3-second mark so the SVG completes before it disappears. Total intro
length is matched in page.tsx's setTimeout (3600ms).
When .intro-running is on the <main> element, the page content
underneath is hidden so it doesn't flash through during the animation.
--------------------------------------------------------------------------- */
.intro-overlay {
position: fixed;
inset: 0;
z-index: 2000;
display: grid;
place-items: center; /* dead-centre the SVG in the viewport */
animation: intro-fade-out 600ms ease 3s forwards;
}
.intro-backdrop {
position: absolute;
inset: 0;
background: var(--bg);
}
.intro-svg {
position: relative; /* sits above .intro-backdrop in the stacking order */
display: block;
width: min(70vw, 900px);
max-height: 70vh;
height: auto;
object-fit: contain;
}
.intro-running .site-header,
.intro-running main > section,
.intro-running main > footer {
opacity: 0;
}
@media (max-width: 720px) {
.intro-svg {
width: min(86vw, 560px);
}
}
@media (max-width: 480px) {
.intro-svg {
width: 92vw;
}
}
/* ---------------------------------------------------------------------------
7. Animations — keyframes used by the orbs and the intro overlay above.
--------------------------------------------------------------------------- */
@keyframes drift {
0%,
100% {
transform: translate3d(0, 0, 0) scale(1);
}
50% {
transform: translate3d(0, 18px, 0) scale(1.06);
}
}
@keyframes intro-wordmark {
0% {
opacity: 0;
transform: translateY(110%) scale(0.98);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes intro-fade-out {
to {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
}
/* ---------------------------------------------------------------------------
8. Reduced motion
Respects the user's OS-level "reduce motion" preference by disabling
the orb drift, the intro animation, and any transition/animation
durations across the site.
--------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
.ambient-orb,
.intro-overlay,
.intro-wordmark {
animation: none !important;
}
*,
*::before,
*::after {
transition-duration: 0ms !important;
animation-duration: 0ms !important;
}
}
+26
View File
@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
+72
View File
@@ -0,0 +1,72 @@
import type { Metadata, Viewport } from "next";
import "./globals.css";
import CookieBanner from "@/components/CookieBanner";
export const metadata: Metadata = {
metadataBase: new URL("https://novarix.uk"),
title: {
default: "Novarix Networks",
template: "%s | Novarix Networks",
},
description:
"Novarix Networks provides network consulting, remote network support, and architecture services for organisations running production network infrastructure.",
applicationName: "Novarix Networks",
keywords: [
"Novarix Networks",
"ISP",
"Managed Service Provider",
"MSP",
"Network Consulting",
"Internet Connectivity",
"BGP",
"IXP",
"CDN Edge",
"Network Engineering",
],
authors: [{ name: "Novarix Networks" }],
creator: "Novarix Networks",
publisher: "Novarix Networks",
alternates: {
canonical: "/",
},
openGraph: {
type: "website",
url: "https://novarix.uk",
siteName: "Novarix Networks",
title: "Novarix Networks",
description:
"Engineering-led network consulting, remote support, and architecture for production networks.",
},
twitter: {
card: "summary_large_image",
title: "Novarix Networks",
description:
"Engineering-led network consulting, remote support, and architecture for production networks.",
},
robots: {
index: true,
follow: true,
},
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#020617" },
],
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en-GB" suppressHydrationWarning>
<body>
{children}
<CookieBanner />
</body>
</html>
);
}
+34
View File
@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
+499
View File
@@ -0,0 +1,499 @@
"use client";
// =============================================================================
// Novarix Networks — Homepage
// =============================================================================
//
// This file controls the LAYOUT and STYLING of the homepage.
// All editable TEXT lives in `/content.ts` at the project root.
//
// How this file is organised, top to bottom:
// 1. Imports + setup
// 2. Intro overlay (the animated logo shown on first visit)
// 3. Site shell (background gradient + ambient orbs + grid)
// 4. Header (logo + navigation)
// 5. Hero (big headline + buttons)
// 6. Services (three cards under "What we do")
// 7. How we engage (three cards under "Working with us")
// 8. Contact (the dark contact card)
// 9. Footer (company details + copyright)
//
// Each section is marked with a clear comment header you can search for.
// The "use client" line at the top tells Next.js this page runs in the
// browser (needed for the animated intro and the mouse-tracking glow).
// =============================================================================
import Image from "next/image";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { site } from "@/content";
import { openCookieBanner } from "@/components/CookieBanner";
// Type for the mouse-pointer position used to move the background glow.
type PointerState = { x: number; y: number };
export default function HomePage() {
// ---------------------------------------------------------------------------
// STATE
// ---------------------------------------------------------------------------
// showIntro — true while the animated logo intro is playing.
// pointer — the mouse position (in % of page width/height). Used by the
// soft glow that follows the cursor in the background.
// ---------------------------------------------------------------------------
const [showIntro, setShowIntro] = useState(false);
const [pointer, setPointer] = useState<PointerState>({ x: 50, y: 22 });
// ---------------------------------------------------------------------------
// INTRO OVERLAY EFFECT
// Plays the animated logo + wordmark the first time someone visits the
// site in this browser tab. We remember they've seen it using
// sessionStorage so it doesn't replay on every navigation. The intro lasts
// ~3.2 seconds, then fades out.
// ---------------------------------------------------------------------------
useEffect(() => {
try {
const introSeen = window.sessionStorage.getItem("novarix-intro-seen");
// Only set the "seen" flag if the user has accepted cookies. Without
// consent we still play the intro — we just don't remember it played.
const consent = window.localStorage.getItem("novarix-cookie-consent");
if (!introSeen) {
// Sync once with sessionStorage on first client mount.
// eslint-disable-next-line react-hooks/set-state-in-effect
setShowIntro(true);
if (consent === "ack") {
window.sessionStorage.setItem("novarix-intro-seen", "true");
}
// The SVG's built-in stroke + fill animation lasts ~3s.
// We hold for an extra ~600ms so the CSS fade-out can complete.
const timer = window.setTimeout(() => {
setShowIntro(false);
}, 3600);
return () => window.clearTimeout(timer);
}
} catch {
/* storage unavailable — silently skip the intro */
}
}, []);
// ---------------------------------------------------------------------------
// BACKGROUND POSITION
// Translates the current mouse pointer into two CSS variables (--mx, --my)
// that the .site-shell background gradient reads. Memoised so React doesn't
// rebuild the style object on every render.
// ---------------------------------------------------------------------------
const backgroundStyle = useMemo(
() =>
({
["--mx"]: `${pointer.x}%`,
["--my"]: `${pointer.y}%`,
}) as React.CSSProperties,
[pointer.x, pointer.y]
);
return (
<>
{/* =====================================================================
INTRO OVERLAY
A single self-contained animated SVG of the Novarix wordmark,
drawn dead-centre in the viewport. The SVG itself contains all the
animation (strokes draw over 2s, fill in over the next 1s — see
/public/branding/animated_logo_intro.svg). Hidden after ~3.6s
(see the useEffect above + the CSS fade-out timing).
===================================================================== */}
{showIntro && (
<div className="intro-overlay" aria-hidden="true">
<div className="intro-backdrop" />
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/branding/animated_logo_intro.svg"
alt=""
className="intro-svg"
/>
</div>
)}
{/* =====================================================================
SITE SHELL
The <main> wraps the whole page. It tracks the mouse so the
background glow can follow the cursor, and applies the .site-shell
class (defined in globals.css) for the background gradient.
===================================================================== */}
<main
id="top"
className={`site-shell ${showIntro ? "intro-running" : ""}`}
style={backgroundStyle}
onMouseMove={(event) => {
const rect = event.currentTarget.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width) * 100;
const y = ((event.clientY - rect.top) / rect.height) * 100;
setPointer({ x, y });
}}
>
{/* -------------------------------------------------------------------
AMBIENT LAYER
Two soft floating "orbs" and a faint grid behind everything.
Purely decorative. All styles live in globals.css under
.ambient-orb / .ambient-grid. Sits at z-index -10 so it's
under the page content.
------------------------------------------------------------------- */}
<div
className="pointer-events-none fixed inset-0 -z-10 overflow-hidden"
aria-hidden="true"
>
<div className="ambient-orb ambient-orb-a" />
<div className="ambient-orb ambient-orb-b" />
<div className="ambient-grid" />
</div>
{/* ===================================================================
HEADER
Sticky at the top of the page. Contains the wordmark logo (links
back to top) and the primary navigation pulled from
content.ts -> site.nav. Two logo images swap automatically for
light/dark mode.
=================================================================== */}
<header className="site-header sticky top-0 z-50 border-b border-[var(--border)] bg-[color-mix(in_srgb,var(--bg)_82%,transparent)] backdrop-blur-xl">
<div className="mx-auto flex h-20 w-full max-w-6xl items-center justify-between gap-6 px-6 sm:h-24 sm:px-8">
{/* Logo / wordmark */}
<a
href="#top"
aria-label="Novarix Networks — back to top"
className="inline-flex shrink-0 items-center"
>
<Image
src="/branding/novarix-wordmark-colour.png"
alt="Novarix Networks"
width={2048}
height={430}
priority
className="brand-light h-9 w-auto max-w-[42vw] object-contain sm:h-12"
/>
<Image
src="/branding/novarix-wordmark-white.png"
alt="Novarix Networks"
width={2048}
height={430}
priority
className="brand-dark h-9 w-auto max-w-[42vw] object-contain sm:h-12"
/>
</a>
{/* Desktop navigation — hidden on small screens (sm:flex) */}
<nav
aria-label="Primary"
className="hidden items-center gap-7 text-sm text-[var(--text-soft)] sm:flex"
>
{site.nav.map((item) => (
<a
key={item.href}
href={item.href}
className="transition-colors hover:text-[var(--text)]"
>
{item.label}
</a>
))}
{/* Pill-style Contact button on the right */}
<a
href="#contact"
className="inline-flex items-center gap-2 rounded-full border border-[var(--border-strong)] px-4 py-1.5 text-[var(--text)] transition-all hover:border-[var(--accent)] hover:text-[var(--accent)]"
>
Contact
<span aria-hidden="true"></span>
</a>
</nav>
{/* Mobile-only Contact pill — replaces the full nav on small screens */}
<a
href="#contact"
className="inline-flex items-center rounded-full border border-[var(--border-strong)] px-3 py-1.5 text-xs text-[var(--text)] transition-colors hover:border-[var(--accent)] hover:text-[var(--accent)] sm:hidden"
>
Contact
</a>
</div>
</header>
{/* ===================================================================
HERO
The first thing visitors see: status pill, eyebrow, big headline,
descriptive paragraph, and two call-to-action buttons.
All text comes from content.ts -> site.hero.
=================================================================== */}
<section className="relative pt-24 pb-20 sm:pt-32 sm:pb-28">
<div className="mx-auto w-full max-w-6xl px-6 sm:px-8">
<div className="max-w-4xl">
{/* Optional pulsing-dot pill. Hidden if site.hero.badge is "" */}
{site.hero.badge && (
<span className="inline-flex items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--surface)] px-3 py-1 text-xs font-medium text-[var(--text-soft)] backdrop-blur">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--accent)] opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[var(--accent)]" />
</span>
{site.hero.badge}
</span>
)}
{/* Small uppercase eyebrow */}
<p className="mt-7 text-xs font-semibold tracking-[0.18em] text-[var(--accent)] uppercase">
{site.hero.eyebrow}
</p>
{/* Main headline. The middle word ("accent") is shown in a
brand-coloured gradient. Font size scales with viewport. */}
<h1 className="mt-5 text-[clamp(2.5rem,6vw,4.8rem)] leading-[1.04] font-semibold tracking-[-0.04em] text-balance">
{site.hero.headlineBefore}{" "}
<span className="bg-gradient-to-r from-brand-500 to-brand-700 bg-clip-text text-transparent dark:from-brand-300 dark:to-brand-500">
{site.hero.headlineAccent}
</span>{" "}
{site.hero.headlineAfter}
</h1>
{/* Supporting paragraph */}
<p className="mt-7 max-w-2xl text-lg leading-relaxed text-[var(--text-soft)] sm:text-xl">
{site.hero.description}
</p>
{/* Call-to-action buttons */}
<div className="mt-10 flex flex-wrap items-center gap-3">
{/* Primary button — solid background */}
<a
href={site.hero.primaryCta.href}
className="group inline-flex h-12 items-center gap-2 rounded-xl bg-[var(--button-bg)] px-5 text-sm font-semibold text-[var(--button-fg)] shadow-sm transition-all hover:-translate-y-0.5 hover:bg-[var(--button-hover)] hover:shadow-md"
>
{site.hero.primaryCta.label}
<span
aria-hidden="true"
className="transition-transform group-hover:translate-x-0.5"
>
</span>
</a>
{/* Secondary button — outlined */}
<a
href={site.hero.secondaryCta.href}
className="inline-flex h-12 items-center rounded-xl border border-[var(--border-strong)] bg-[var(--surface)] px-5 text-sm font-semibold text-[var(--text)] backdrop-blur transition-all hover:-translate-y-0.5 hover:border-[var(--accent)] hover:text-[var(--accent)]"
>
{site.hero.secondaryCta.label}
</a>
</div>
</div>
</div>
</section>
{/* ===================================================================
SERVICES
"What we do" — section heading then a 3-column grid of cards
(1 column on mobile). The number of cards comes from
content.ts -> site.services.items, so adding a fourth card there
automatically appears here. Below the grid sits an optional
dashed-border "Capabilities" line.
=================================================================== */}
<section
id="services"
className="relative border-t border-[var(--border)] py-24 sm:py-28"
>
<div className="mx-auto w-full max-w-6xl px-6 sm:px-8">
{/* Section heading */}
<div className="max-w-3xl">
<p className="text-xs font-semibold tracking-[0.18em] text-[var(--accent)] uppercase">
{site.services.eyebrow}
</p>
<h2 className="mt-3 text-[clamp(1.9rem,3.2vw,2.75rem)] leading-tight font-semibold tracking-[-0.03em]">
{site.services.title}
</h2>
<p className="mt-5 max-w-2xl text-base leading-relaxed text-[var(--text-soft)] sm:text-lg">
{site.services.description}
</p>
</div>
{/* Service cards grid — 1 column on mobile, 3 columns on md+ */}
<div className="mt-12 grid gap-5 sm:gap-6 md:grid-cols-3">
{site.services.items.map((service) => (
<article
key={service.title}
className="group relative overflow-hidden rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-7 backdrop-blur transition-all hover:-translate-y-1 hover:border-[var(--border-strong)] hover:shadow-[0_20px_40px_-20px_oklch(0_0_0/0.25)]"
>
{/* Thin gradient hairline at the top of each card on hover */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-[var(--border-strong)] to-transparent opacity-0 transition-opacity group-hover:opacity-100"
/>
{/* Card top row: index number + arrow that appears on hover */}
<div className="flex items-center justify-between">
<span className="font-mono text-xs tracking-widest text-[var(--text-soft)]">
{service.index}
</span>
<span
aria-hidden="true"
className="text-[var(--text-soft)] opacity-0 transition-all group-hover:translate-x-0.5 group-hover:opacity-100"
>
</span>
</div>
{/* Card title + body */}
<h3 className="mt-5 text-lg font-semibold tracking-tight">
{service.title}
</h3>
<p className="mt-3 text-sm leading-relaxed text-[var(--text-soft)]">
{service.description}
</p>
</article>
))}
</div>
{/* Optional capabilities strip. Hidden if capabilities is "". */}
{site.services.capabilities && (
<div className="mt-10 rounded-2xl border border-dashed border-[var(--border)] p-5 text-sm leading-relaxed text-[var(--text-soft)] sm:p-6">
<span className="font-semibold tracking-wide text-[var(--text)] uppercase">
Capabilities ·
</span>{" "}
{site.services.capabilities}
</div>
)}
</div>
</section>
{/* ===================================================================
HOW WE ENGAGE
Three engagement-model cards. Same shape as Services but the
cards are simpler (no number, no hover arrow). Cards come from
content.ts -> site.engage.items.
=================================================================== */}
<section
id="engage"
className="relative border-t border-[var(--border)] py-24 sm:py-28"
>
<div className="mx-auto w-full max-w-6xl px-6 sm:px-8">
{/* Section heading */}
<div className="max-w-3xl">
<p className="text-xs font-semibold tracking-[0.18em] text-[var(--accent)] uppercase">
{site.engage.eyebrow}
</p>
<h2 className="mt-3 text-[clamp(1.9rem,3.2vw,2.75rem)] leading-tight font-semibold tracking-[-0.03em]">
{site.engage.title}
</h2>
<p className="mt-5 max-w-2xl text-base leading-relaxed text-[var(--text-soft)] sm:text-lg">
{site.engage.description}
</p>
</div>
{/* Engagement cards grid */}
<div className="mt-12 grid gap-5 sm:gap-6 md:grid-cols-3">
{site.engage.items.map((engagement) => (
<div
key={engagement.title}
className="rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-7 backdrop-blur"
>
<h3 className="text-lg font-semibold tracking-tight">
{engagement.title}
</h3>
<p className="mt-3 text-sm leading-relaxed text-[var(--text-soft)]">
{engagement.description}
</p>
</div>
))}
</div>
</div>
</section>
{/* ===================================================================
CONTACT
A single dark, rounded "callout" card with the contact email and
response-time note. The button uses a `mailto:` link so clicking
opens the visitor's mail app pre-addressed to us.
=================================================================== */}
<section
id="contact"
className="relative border-t border-[var(--border)] py-24 sm:py-28"
>
<div className="mx-auto w-full max-w-4xl px-6 sm:px-8">
<div className="overflow-hidden rounded-3xl border border-[var(--border)] bg-[var(--surface)] p-8 backdrop-blur sm:p-12">
{/* Section heading inside the card */}
<p className="text-xs font-semibold tracking-[0.18em] text-[var(--accent)] uppercase">
{site.contact.eyebrow}
</p>
<h2 className="mt-3 text-[clamp(1.9rem,3.2vw,2.75rem)] leading-tight font-semibold tracking-[-0.03em]">
{site.contact.title}
</h2>
<p className="mt-5 max-w-2xl text-base leading-relaxed text-[var(--text-soft)] sm:text-lg">
{site.contact.description}
</p>
{/* Email button + small response-time note */}
<div className="mt-8 flex flex-wrap items-center gap-3">
<a
href={`mailto:${site.contact.email}`}
className="group inline-flex h-12 items-center gap-2 rounded-xl bg-[var(--button-bg)] px-5 text-sm font-semibold text-[var(--button-fg)] shadow-sm transition-all hover:-translate-y-0.5 hover:bg-[var(--button-hover)] hover:shadow-md"
>
{site.contact.email}
<span
aria-hidden="true"
className="transition-transform group-hover:translate-x-0.5"
>
</span>
</a>
<span className="text-sm text-[var(--text-soft)]">
{site.contact.note}
</span>
</div>
</div>
</div>
</section>
{/* ===================================================================
FOOTER
Two columns on desktop: company details on the left, tagline +
copyright on the right. Stacks to one column on mobile.
All text comes from content.ts -> site.footer.
=================================================================== */}
<footer className="border-t border-[var(--border)] py-12">
<div className="mx-auto grid w-full max-w-6xl gap-8 px-6 sm:grid-cols-[1fr_auto] sm:px-8">
{/* Left column: company name, registration, contact email */}
<div className="text-sm leading-relaxed text-[var(--text-soft)]">
<p className="font-semibold text-[var(--text)]">
{site.footer.company}
</p>
<p className="mt-1">{site.footer.registered}</p>
<p className="mt-1">
<a
href={`mailto:${site.footer.email}`}
className="transition-colors hover:text-[var(--accent)]"
>
{site.footer.email}
</a>
</p>
</div>
{/* Right column: tagline, cookie links, copyright */}
<div className="flex flex-col gap-2 text-sm text-[var(--text-soft)] sm:items-end sm:text-right">
<span className="text-xs tracking-[0.14em] uppercase">
{site.footer.tagline}
</span>
{/* Cookie & privacy links — keep them small and unobtrusive */}
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs">
<Link
href={site.footer.cookiePolicyHref}
className="transition-colors hover:text-[var(--accent)]"
>
{site.footer.cookiePolicyLabel}
</Link>
<button
type="button"
onClick={openCookieBanner}
className="cursor-pointer text-left transition-colors hover:text-[var(--accent)]"
>
{site.footer.cookiePrefsLabel}
</button>
</div>
<span>
© {new Date().getFullYear()} {site.footer.company}
</span>
</div>
</div>
</footer>
</main>
</>
);
}
+154
View File
@@ -0,0 +1,154 @@
import Image from "next/image";
const services = [
{
title: "Internet Connectivity",
description:
"Business internet access with resilient routing, clear service boundaries, and engineering-led deployment.",
},
{
title: "Managed Network Services",
description:
"Operational support for routing, switching, firewalls, and production network infrastructure.",
},
{
title: "Network Consulting",
description:
"Architecture, troubleshooting, and design support for organisations and service providers.",
},
];
export default function HomePage() {
return (
<main id="top">
<header className="site-header">
<div className="container header-inner">
<a href="#top" className="brand-link" aria-label="Go to top">
<Image
src="/branding/novarix-wordmark-colour.png"
alt="Novarix Networks"
width={2048}
height={430}
priority
className="brand-image brand-image-light"
/>
<Image
src="/branding/novarix-wordmark-white.png"
alt="Novarix Networks"
width={2048}
height={430}
priority
className="brand-image brand-image-dark"
/>
</a>
<nav className="nav">
<a href="#services">Services</a>
<a href="#direction">Direction</a>
<a href="#contact">Contact</a>
</nav>
</div>
</header>
<section className="hero">
<div className="container">
<div className="hero-copy">
<p className="eyebrow">Connectivity Managed Services Network Engineering</p>
<h1>Network infrastructure services built for reliability and growth.</h1>
<p className="hero-text">
Novarix Networks provides internet connectivity, managed network
services, and engineering expertise for organisations that need
dependable infrastructure and clear technical ownership.
</p>
<div className="hero-actions">
<a href="#contact" className="button button-primary">
Contact
</a>
<a href="#services" className="button button-secondary">
Services
</a>
</div>
</div>
</div>
</section>
<section id="services" className="section section-border">
<div className="container">
<div className="section-heading">
<h2>Services</h2>
<p>
Focus the first version of the business on what can be delivered
credibly now.
</p>
</div>
<div className="card-grid">
{services.map((service) => (
<article key={service.title} className="card">
<h3>{service.title}</h3>
<p>{service.description}</p>
</article>
))}
</div>
</div>
</section>
<section id="direction" className="section section-border">
<div className="container narrow">
<div className="section-heading">
<h2>Platform Direction</h2>
<p>
The platform is designed to expand beyond connectivity and managed
services toward deeper interconnection capabilities, including
exchange participation and edge infrastructure where commercially
justified.
</p>
</div>
<div className="direction-list">
<div className="direction-item">
<span className="direction-phase">Phase 1</span>
<h3>Connectivity, support, consulting</h3>
<p>Lead with services that are operationally clear and saleable immediately.</p>
</div>
<div className="direction-item">
<span className="direction-phase">Phase 2</span>
<h3>Interconnect and peering</h3>
<p>Add exchange participation and partner interconnection as the footprint matures.</p>
</div>
<div className="direction-item">
<span className="direction-phase">Phase 3</span>
<h3>Edge infrastructure</h3>
<p>Introduce selective CDN edge or regional platform capability only where demand supports it.</p>
</div>
</div>
</div>
</section>
<section id="contact" className="section section-border">
<div className="container narrow">
<div className="section-heading">
<h2>Contact</h2>
<p>
For enquiries regarding connectivity, managed network support, or
consulting services:
</p>
</div>
<a className="contact-link" href="mailto:hello@novarix.network">
hello@novarix.network
</a>
</div>
</section>
<footer className="footer section-border">
<div className="container footer-inner">
<span>© {new Date().getFullYear()} Novarix Networks</span>
</div>
</footer>
</main>
);
}
+11
View File
@@ -0,0 +1,11 @@
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://novarix.uk/sitemap.xml",
};
}
+20
View File
@@ -0,0 +1,20 @@
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
const lastModified = new Date();
return [
{
url: "https://novarix.uk",
lastModified,
changeFrequency: "monthly",
priority: 1,
},
{
url: "https://novarix.uk/cookies",
lastModified,
changeFrequency: "yearly",
priority: 0.3,
},
];
}