Initial-commit
This commit is contained in:
@@ -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 & 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'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't do
|
||||
</h2>
|
||||
<p className="mt-3">
|
||||
We don't set tracking cookies. We don't run Google
|
||||
Analytics, Plausible, Fathom, Matomo, or any equivalent. We
|
||||
don't serve advertising. We don't embed third-party
|
||||
scripts that profile you across websites. We don'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'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've mishandled your personal data, you
|
||||
have the right to complain to the UK's data protection
|
||||
authority, the Information Commissioner'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>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 548 KiB |
+338
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user