Apply website QA: light theme, clearer contact form, office emails
- Add a light theme with a header toggle; the choice is remembered and applied before paint - Make the globe, product visuals and footer logo follow the theme - Give the message field a full outline, strengthen the captcha box border and keep autofilled fields readable - Accept website addresses without https:// - List the info email under every office - Keep the phone header on one row: languages move into the menu below 480px
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
@@ -5,6 +5,7 @@ import "../globals.css";
|
||||
import SiteHeader from "@/components/site-header";
|
||||
import SiteFooter from "@/components/site-footer";
|
||||
import Analytics, { AnalyticsNoScript } from "@/components/analytics";
|
||||
import { themeScript } from "@/components/theme-toggle";
|
||||
import { dirOf, locales, localize, ogLocale } from "@/i18n/config";
|
||||
import { mainNav } from "@/i18n/nav";
|
||||
import { getUI } from "@/i18n/server";
|
||||
@@ -51,7 +52,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: "#000000",
|
||||
colorScheme: "dark",
|
||||
colorScheme: "dark light",
|
||||
};
|
||||
|
||||
export default async function RootLayout({ children }: LayoutProps<"/[lang]">) {
|
||||
@@ -63,6 +64,8 @@ export default async function RootLayout({ children }: LayoutProps<"/[lang]">) {
|
||||
closeMenu: t.nav.closeMenu,
|
||||
language: t.nav.language,
|
||||
logoHome: t.logoHome,
|
||||
lightTheme: t.nav.lightTheme,
|
||||
darkTheme: t.nav.darkTheme,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -71,7 +74,12 @@ export default async function RootLayout({ children }: LayoutProps<"/[lang]">) {
|
||||
dir={dirOf(locale)}
|
||||
className={`${inter.variable} ${arabic.variable}`}
|
||||
data-scroll-behavior="smooth"
|
||||
// The theme script sets data-theme before React hydrates.
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||
</head>
|
||||
<body>
|
||||
<AnalyticsNoScript />
|
||||
<a href="#main" className="skip-link">
|
||||
|
||||
+133
-33
@@ -13,6 +13,13 @@
|
||||
--fumo: #8f8f8f;
|
||||
--piombo: #666666;
|
||||
--argento: #d2d2d2;
|
||||
/* The names keep their roles in both themes: bianco is ink, nero is paper. */
|
||||
--ink-rgb: 255, 255, 255;
|
||||
--paper-rgb: 0, 0, 0;
|
||||
--cap-line: #262626;
|
||||
--panel: #070707;
|
||||
--panel-line: #1c1c1c;
|
||||
color-scheme: dark;
|
||||
|
||||
--font-sans: var(--font-inter), "Helvetica Neue", Arial, sans-serif;
|
||||
|
||||
@@ -28,6 +35,23 @@
|
||||
--dir: -1;
|
||||
}
|
||||
|
||||
/* Light theme, chosen with the header toggle. */
|
||||
:root[data-theme="light"] {
|
||||
--bianco: #0a0a0a;
|
||||
--nero: #ffffff;
|
||||
--notte: #f4f4f2;
|
||||
--grafite: #d9d9d6;
|
||||
--fumo: #5c5c5c;
|
||||
--piombo: #808080;
|
||||
--argento: #333333;
|
||||
--ink-rgb: 10, 10, 10;
|
||||
--paper-rgb: 255, 255, 255;
|
||||
--cap-line: #e2e2e2;
|
||||
--panel: #f7f7f5;
|
||||
--panel-line: #e4e4e1;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
@@ -55,7 +79,7 @@ body {
|
||||
|
||||
::selection {
|
||||
background: var(--rosso);
|
||||
color: var(--bianco);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -220,7 +244,7 @@ a {
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--bianco);
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
border-color: rgba(var(--ink-rgb), 0.35);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
@@ -261,7 +285,7 @@ a {
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
border: 1px solid rgba(var(--ink-rgb), 0.28);
|
||||
border-radius: 9999px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
@@ -303,7 +327,7 @@ a {
|
||||
}
|
||||
|
||||
.site-header.is-scrolled {
|
||||
background: rgba(0, 0, 0, 0.82);
|
||||
background: rgba(var(--paper-rgb), 0.82);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
border-color: var(--grafite);
|
||||
@@ -350,6 +374,7 @@ a {
|
||||
}
|
||||
|
||||
.logo-type b {
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2em;
|
||||
@@ -376,7 +401,7 @@ a {
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
color: rgba(var(--ink-rgb), 0.78);
|
||||
transition: color 0.3s var(--ease);
|
||||
}
|
||||
|
||||
@@ -512,9 +537,9 @@ a {
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0, 0, 0, 0.55) 0%, transparent 22%),
|
||||
linear-gradient(0deg, #000 4%, rgba(0, 0, 0, 0.6) 34%, transparent 62%),
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0.7) 0%, transparent 55%);
|
||||
linear-gradient(180deg, rgba(var(--paper-rgb), 0.55) 0%, transparent 22%),
|
||||
linear-gradient(0deg, var(--nero) 4%, rgba(var(--paper-rgb), 0.6) 34%, transparent 62%),
|
||||
linear-gradient(90deg, rgba(var(--paper-rgb), 0.7) 0%, transparent 55%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -858,13 +883,13 @@ a {
|
||||
}
|
||||
|
||||
.cap-map line {
|
||||
stroke: #262626;
|
||||
stroke: var(--cap-line);
|
||||
stroke-width: 1;
|
||||
transition: stroke 0.5s var(--ease);
|
||||
}
|
||||
|
||||
.cap-map line.on {
|
||||
stroke: rgba(255, 255, 255, 0.6);
|
||||
stroke: rgba(var(--ink-rgb), 0.6);
|
||||
}
|
||||
|
||||
.cap-map .node {
|
||||
@@ -882,7 +907,7 @@ a {
|
||||
|
||||
.cap-map .core {
|
||||
fill: var(--nero);
|
||||
stroke: rgba(255, 255, 255, 0.4);
|
||||
stroke: rgba(var(--ink-rgb), 0.4);
|
||||
}
|
||||
|
||||
.cap-map text {
|
||||
@@ -1002,8 +1027,8 @@ a {
|
||||
grid-column: 1 / span 6;
|
||||
position: relative;
|
||||
aspect-ratio: 600 / 412;
|
||||
background: #070707;
|
||||
outline: 1px solid #1c1c1c;
|
||||
background: var(--panel);
|
||||
outline: 1px solid var(--panel-line);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1071,7 +1096,7 @@ a {
|
||||
/* journey viz */
|
||||
.viz-journey .path {
|
||||
fill: none;
|
||||
stroke: rgba(255, 255, 255, 0.16);
|
||||
stroke: rgba(var(--ink-rgb), 0.16);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
@@ -1099,7 +1124,7 @@ a {
|
||||
|
||||
.viz-journey .node {
|
||||
fill: var(--nero);
|
||||
stroke: rgba(255, 255, 255, 0.6);
|
||||
stroke: rgba(var(--ink-rgb), 0.6);
|
||||
}
|
||||
|
||||
.viz-journey .goal {
|
||||
@@ -1117,11 +1142,11 @@ a {
|
||||
/* radar viz */
|
||||
.viz-radar .ring {
|
||||
fill: none;
|
||||
stroke: rgba(255, 255, 255, 0.12);
|
||||
stroke: rgba(var(--ink-rgb), 0.12);
|
||||
}
|
||||
|
||||
.viz-radar .axis {
|
||||
stroke: rgba(255, 255, 255, 0.07);
|
||||
stroke: rgba(var(--ink-rgb), 0.07);
|
||||
}
|
||||
|
||||
.viz-radar .sweep {
|
||||
@@ -1160,12 +1185,12 @@ a {
|
||||
|
||||
/* grid viz */
|
||||
.viz-grid .cell {
|
||||
fill: rgba(255, 255, 255, 0.035);
|
||||
stroke: rgba(255, 255, 255, 0.08);
|
||||
fill: rgba(var(--ink-rgb), 0.035);
|
||||
stroke: rgba(var(--ink-rgb), 0.08);
|
||||
}
|
||||
|
||||
.viz-grid .cell.on {
|
||||
fill: rgba(255, 255, 255, 0.85);
|
||||
fill: rgba(var(--ink-rgb), 0.85);
|
||||
stroke: none;
|
||||
animation: cell 4.8s var(--ease) infinite;
|
||||
animation-delay: var(--t);
|
||||
@@ -1201,7 +1226,7 @@ a {
|
||||
|
||||
.viz-grid .frame {
|
||||
fill: none;
|
||||
stroke: rgba(255, 255, 255, 0.5);
|
||||
stroke: rgba(var(--ink-rgb), 0.5);
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
@@ -1290,7 +1315,7 @@ a {
|
||||
place-items: center;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
border: 1px solid rgba(var(--ink-rgb), 0.35);
|
||||
border-radius: 9999px;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
@@ -1360,7 +1385,7 @@ a {
|
||||
}
|
||||
|
||||
.journey ol::after {
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.7), var(--rosso));
|
||||
background: linear-gradient(90deg, rgba(var(--ink-rgb), 0.7), var(--rosso));
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 2.6s var(--ease) 0.2s;
|
||||
@@ -1434,7 +1459,7 @@ a {
|
||||
height: auto;
|
||||
}
|
||||
.journey ol::after {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.7), var(--rosso));
|
||||
background: linear-gradient(180deg, rgba(var(--ink-rgb), 0.7), var(--rosso));
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
}
|
||||
@@ -1542,7 +1567,7 @@ a {
|
||||
}
|
||||
|
||||
.gaps .break line {
|
||||
stroke: rgba(255, 255, 255, 0.45);
|
||||
stroke: rgba(var(--ink-rgb), 0.45);
|
||||
}
|
||||
|
||||
.gaps .break .cut {
|
||||
@@ -1635,7 +1660,7 @@ a {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.9) 55%, #000 80%);
|
||||
background: radial-gradient(ellipse at center, rgba(var(--paper-rgb), 0.6) 0%, rgba(var(--paper-rgb), 0.9) 55%, var(--nero) 80%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -1758,7 +1783,7 @@ a {
|
||||
line-height: 0.9;
|
||||
letter-spacing: -0.05em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.07);
|
||||
color: rgba(var(--ink-rgb), 0.07);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -1873,7 +1898,7 @@ a {
|
||||
|
||||
.nav-panel-inner {
|
||||
padding: 22px 28px 26px;
|
||||
background: rgba(0, 0, 0, 0.94);
|
||||
background: rgba(var(--paper-rgb), 0.94);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
border: 1px solid var(--grafite);
|
||||
@@ -2115,7 +2140,7 @@ a {
|
||||
}
|
||||
|
||||
.link-card:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
background: rgba(var(--ink-rgb), 0.03);
|
||||
}
|
||||
|
||||
.link-card:hover .arrow-link {
|
||||
@@ -2516,11 +2541,28 @@ a {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* The message gets a full outline so it reads as a box to type in. */
|
||||
.field textarea {
|
||||
min-height: 132px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--grafite);
|
||||
}
|
||||
|
||||
.field input:focus,
|
||||
.field textarea:focus {
|
||||
border-color: var(--rosso);
|
||||
}
|
||||
|
||||
/* Browser autofill paints its own pale background; keep the field on-theme and readable. */
|
||||
.field input:-webkit-autofill,
|
||||
.field input:-webkit-autofill:hover,
|
||||
.field input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: var(--bianco);
|
||||
caret-color: var(--bianco);
|
||||
-webkit-box-shadow: 0 0 0 1000px var(--nero) inset;
|
||||
transition: background-color 9999s;
|
||||
}
|
||||
|
||||
.field input::placeholder {
|
||||
color: var(--piombo);
|
||||
}
|
||||
@@ -2680,6 +2722,64 @@ a {
|
||||
color: var(--bianco);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex: none;
|
||||
padding: 0;
|
||||
border: 1px solid var(--grafite);
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: var(--bianco);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s var(--ease);
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
border-color: var(--bianco);
|
||||
}
|
||||
|
||||
.theme-toggle svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
/* Shows the theme you'd switch to: a sun on dark pages, a moon on light ones. */
|
||||
.theme-toggle .moon,
|
||||
:root[data-theme="light"] .theme-toggle .sun {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .theme-toggle .moon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Phones: the languages move into the menu (it carries its own switch) so the logo has room. */
|
||||
@media (max-width: 480px) {
|
||||
.site-header .lang-switch {
|
||||
display: none;
|
||||
}
|
||||
.site-header .theme-toggle {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* The stacked logo's wordmark is white; the light theme swaps in the dark-lettered file. */
|
||||
.logo-stacked .on-light,
|
||||
:root[data-theme="light"] .logo-stacked .on-dark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .logo-stacked .on-light {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lang-switch [aria-current] {
|
||||
color: var(--bianco);
|
||||
box-shadow: inset 0 -1px 0 var(--rosso);
|
||||
@@ -2706,7 +2806,7 @@ a {
|
||||
gap: 16px;
|
||||
max-width: 340px;
|
||||
padding: 14px 18px;
|
||||
border: 1px solid var(--grafite);
|
||||
border: 1px solid var(--piombo);
|
||||
transition: border-color 0.3s var(--ease);
|
||||
}
|
||||
|
||||
@@ -2739,7 +2839,7 @@ a {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex: none;
|
||||
border: 1px solid var(--fumo);
|
||||
border: 1.5px solid var(--argento);
|
||||
transition:
|
||||
border-color 0.3s var(--ease),
|
||||
background-color 0.3s var(--ease);
|
||||
@@ -2754,7 +2854,7 @@ a {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: none;
|
||||
stroke: var(--bianco);
|
||||
stroke: #fff;
|
||||
stroke-width: 1.8;
|
||||
stroke-dasharray: 16;
|
||||
stroke-dashoffset: 16;
|
||||
@@ -2810,7 +2910,7 @@ a {
|
||||
}
|
||||
|
||||
[dir="rtl"] .journey ol::after {
|
||||
background: linear-gradient(270deg, rgba(255, 255, 255, 0.7), var(--rosso));
|
||||
background: linear-gradient(270deg, rgba(var(--ink-rgb), 0.7), var(--rosso));
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
|
||||
@@ -98,9 +98,13 @@ export default function ContactForm({
|
||||
<span>{t.website}</span>
|
||||
<input
|
||||
name="website"
|
||||
type="url"
|
||||
// Plain text, not type="url": people write "company.com" and shouldn't be made to add https://.
|
||||
type="text"
|
||||
inputMode="url"
|
||||
placeholder="https://"
|
||||
autoComplete="url"
|
||||
autoCapitalize="none"
|
||||
spellCheck={false}
|
||||
placeholder="company.com"
|
||||
maxLength={300}
|
||||
defaultValue={values?.website}
|
||||
dir="ltr"
|
||||
|
||||
@@ -59,6 +59,10 @@ export default function GlobeCanvas({ layout = "hero", className = "hero-canvas"
|
||||
|
||||
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
// Dots and lines use the theme's ink colour, re-read whenever the theme changes.
|
||||
const readInk = () => getComputedStyle(document.documentElement).getPropertyValue("--ink-rgb").trim() || "255, 255, 255";
|
||||
let ink = readInk();
|
||||
|
||||
// Fibonacci sphere
|
||||
const COUNT = 1500;
|
||||
const points: Vec[] = [];
|
||||
@@ -159,15 +163,15 @@ export default function GlobeCanvas({ layout = "hero", className = "hero-canvas"
|
||||
|
||||
// soft halo
|
||||
const halo = ctx.createRadialGradient(cx, cy, R * 0.7, cx, cy, R * 1.45);
|
||||
halo.addColorStop(0, "rgba(255,255,255,0.045)");
|
||||
halo.addColorStop(1, "rgba(255,255,255,0)");
|
||||
halo.addColorStop(0, `rgba(${ink},0.045)`);
|
||||
halo.addColorStop(1, `rgba(${ink},0)`);
|
||||
ctx.fillStyle = halo;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// outline
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, R * 1.0, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.08)";
|
||||
ctx.strokeStyle = `rgba(${ink},0.08)`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
@@ -177,7 +181,7 @@ export default function GlobeCanvas({ layout = "hero", className = "hero-canvas"
|
||||
const front = p.z > 0;
|
||||
const a = front ? 0.18 + p.z * 0.62 : 0.05 + (1 + p.z) * 0.05;
|
||||
const size = (front ? 1.15 : 0.8) * p.s;
|
||||
ctx.fillStyle = `rgba(255,255,255,${a.toFixed(3)})`;
|
||||
ctx.fillStyle = `rgba(${ink},${a.toFixed(3)})`;
|
||||
ctx.fillRect(p.x - size / 2, p.y - size / 2, size, size);
|
||||
}
|
||||
|
||||
@@ -190,7 +194,7 @@ export default function GlobeCanvas({ layout = "hero", className = "hero-canvas"
|
||||
const b = proj[s];
|
||||
const vis = (a.z + b.z) / 2;
|
||||
if (vis < -0.15) continue;
|
||||
ctx.strokeStyle = `rgba(255,255,255,${(0.08 + Math.max(0, vis) * 0.22).toFixed(3)})`;
|
||||
ctx.strokeStyle = `rgba(${ink},${(0.08 + Math.max(0, vis) * 0.22).toFixed(3)})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(a.x, a.y);
|
||||
ctx.lineTo(b.x, b.y);
|
||||
@@ -224,7 +228,7 @@ export default function GlobeCanvas({ layout = "hero", className = "hero-canvas"
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, 3 + pulse * 14, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = `rgba(255,255,255,${(0.4 + p.z * 0.6).toFixed(3)})`;
|
||||
ctx.fillStyle = `rgba(${ink},${(0.4 + p.z * 0.6).toFixed(3)})`;
|
||||
ctx.fillRect(p.x - 2.5, p.y - 2.5, 5, 5);
|
||||
}
|
||||
|
||||
@@ -243,6 +247,12 @@ export default function GlobeCanvas({ layout = "hero", className = "hero-canvas"
|
||||
});
|
||||
ro.observe(canvas);
|
||||
|
||||
const themeObserver = new MutationObserver(() => {
|
||||
ink = readInk();
|
||||
if (reduced) draw(performance.now());
|
||||
});
|
||||
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
||||
|
||||
const io = new IntersectionObserver(([entry]) => {
|
||||
const visible = entry.isIntersecting;
|
||||
if (visible && !running) {
|
||||
@@ -263,6 +273,7 @@ export default function GlobeCanvas({ layout = "hero", className = "hero-canvas"
|
||||
running = false;
|
||||
cancelAnimationFrame(raf);
|
||||
ro.disconnect();
|
||||
themeObserver.disconnect();
|
||||
io.disconnect();
|
||||
window.removeEventListener("pointermove", onMove);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,8 @@ export default function Logo({ href, label }: { href: string; label: string }) {
|
||||
export function LogoStacked({ href, label }: { href: string; label: string }) {
|
||||
return (
|
||||
<Link href={href} className="logo-stacked" aria-label={label}>
|
||||
<Image src="/logo-stacked.png" alt="AI Growth Technologies" width={480} height={547} />
|
||||
<Image className="on-dark" src="/logo-stacked.png" alt="AI Growth Technologies" width={480} height={547} />
|
||||
<Image className="on-light" src="/logo-stacked-dark.png" alt="AI Growth Technologies" width={480} height={547} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ function JourneyViz({ labels }: { labels: VisualLabels }) {
|
||||
return (
|
||||
<svg className="viz viz-journey" viewBox={`0 0 ${W} ${H}`} aria-hidden="true">
|
||||
{[180, 280, 380].map((x) => (
|
||||
<line key={x} x1={x} y1={28} x2={x} y2={H - 28} stroke="rgba(255,255,255,0.05)" />
|
||||
<line key={x} x1={x} y1={28} x2={x} y2={H - 28} style={{ stroke: "rgba(var(--ink-rgb), 0.05)" }} />
|
||||
))}
|
||||
{sources.map((s, i) => {
|
||||
const y = top + i * gap;
|
||||
@@ -98,7 +98,7 @@ function RadarViz({ labels }: { labels: VisualLabels }) {
|
||||
))}
|
||||
<g className="sweep">
|
||||
<path d={`M${cx} ${cy} L${cx + sweepR} ${cy} A${sweepR} ${sweepR} 0 0 0 ${ex} ${ey} Z`} fill="url(#sweepGrad)" />
|
||||
<line x1={cx} y1={cy} x2={cx + sweepR} y2={cy} stroke="rgba(255,255,255,0.6)" />
|
||||
<line x1={cx} y1={cy} x2={cx + sweepR} y2={cy} style={{ stroke: "rgba(var(--ink-rgb), 0.6)" }} />
|
||||
</g>
|
||||
{dots.map((d, i) => {
|
||||
const x = cx + Math.cos(rad(d.a)) * d.r;
|
||||
|
||||
@@ -4,6 +4,7 @@ import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import Logo from "./logo";
|
||||
import ThemeToggle from "./theme-toggle";
|
||||
import { localeNames, localeShort, locales, localize, splitLocale, type Locale } from "@/i18n/config";
|
||||
import type { NavItem } from "@/i18n/nav";
|
||||
|
||||
@@ -14,6 +15,8 @@ export type HeaderLabels = {
|
||||
closeMenu: string;
|
||||
language: string;
|
||||
logoHome: string;
|
||||
lightTheme: string;
|
||||
darkTheme: string;
|
||||
};
|
||||
|
||||
function LanguageSwitch({ locale, path, label }: { locale: Locale; path: string; label: string }) {
|
||||
@@ -120,6 +123,7 @@ export default function SiteHeader({ locale, nav, t }: { locale: Locale; nav: Na
|
||||
)}
|
||||
</nav>
|
||||
<LanguageSwitch locale={locale} path={path} label={t.language} />
|
||||
<ThemeToggle t={t} />
|
||||
<Link href={contactHref} className="btn btn-solid header-cta">
|
||||
{t.bookCall} <span className="arrow">→</span>
|
||||
</Link>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
export type Theme = "dark" | "light";
|
||||
|
||||
// The site is dark by default; a visitor's choice is remembered in localStorage under this key.
|
||||
const KEY = "theme";
|
||||
|
||||
/** Runs in <head> before the page paints, so a saved light theme never flashes dark first. */
|
||||
export const themeScript = `try{if(localStorage.getItem("${KEY}")==="light")document.documentElement.dataset.theme="light"}catch(e){}`;
|
||||
|
||||
const read = (): Theme => (document.documentElement.dataset.theme === "light" ? "light" : "dark");
|
||||
|
||||
function subscribe(onChange: () => void) {
|
||||
const observer = new MutationObserver(onChange);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
/** The current theme, kept in sync with <html data-theme>. */
|
||||
export function useTheme(): Theme {
|
||||
return useSyncExternalStore(subscribe, read, () => "dark");
|
||||
}
|
||||
|
||||
export default function ThemeToggle({ t }: { t: { lightTheme: string; darkTheme: string } }) {
|
||||
const theme = useTheme();
|
||||
const label = theme === "dark" ? t.lightTheme : t.darkTheme;
|
||||
|
||||
function toggle() {
|
||||
const next: Theme = theme === "dark" ? "light" : "dark";
|
||||
if (next === "light") document.documentElement.dataset.theme = "light";
|
||||
else delete document.documentElement.dataset.theme;
|
||||
try {
|
||||
localStorage.setItem(KEY, next);
|
||||
} catch {
|
||||
// Storage can be blocked (private mode); the theme still switches for this visit.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" className="theme-toggle" onClick={toggle} aria-label={label} title={label}>
|
||||
<svg className="sun" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4.2" />
|
||||
<path d="M12 2.5v2.2M12 19.3v2.2M2.5 12h2.2M19.3 12h2.2M5.3 5.3l1.6 1.6M17.1 17.1l1.6 1.6M5.3 18.7l1.6-1.6M17.1 6.9l1.6-1.6" />
|
||||
</svg>
|
||||
<svg className="moon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M20 14.6A8.5 8.5 0 0 1 9.4 4a8.5 8.5 0 1 0 10.6 10.6z" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -128,7 +128,7 @@
|
||||
"subtitle": "مكتب شريك",
|
||||
"body": [
|
||||
"نقطة تواصل في الولايات المتحدة للشركات في تكساس وخارجها.",
|
||||
"Downtown Houston, TX, USA\nPhone: +1 832-350-0465"
|
||||
"Downtown Houston, TX, USA\nPhone: +1 832-350-0465\nEmail: info@aigrowthtechnologies.com"
|
||||
],
|
||||
"list": [],
|
||||
"cta": null
|
||||
@@ -139,7 +139,7 @@
|
||||
"subtitle": "مكتب شريك",
|
||||
"body": [
|
||||
"نقطة تواصل في الولايات المتحدة مقرها جنوب فلوريدا.",
|
||||
"Brickell Avenue, FL, USA\nPhone: +1 954-235-4974"
|
||||
"Brickell Avenue, FL, USA\nPhone: +1 954-235-4974\nEmail: info@aigrowthtechnologies.com"
|
||||
],
|
||||
"list": [],
|
||||
"cta": null
|
||||
@@ -150,7 +150,7 @@
|
||||
"subtitle": "مكتب شريك",
|
||||
"body": [
|
||||
"نقطة تواصل في الولايات المتحدة في أحد أبرز أسواق الأعمال في العالم.",
|
||||
"Manhattan Financial District, NY, USA\nPhone: +1 713-289-4577"
|
||||
"Manhattan Financial District, NY, USA\nPhone: +1 713-289-4577\nEmail: info@aigrowthtechnologies.com"
|
||||
],
|
||||
"list": [],
|
||||
"cta": null
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
"subtitle": "Partner Office",
|
||||
"body": [
|
||||
"A US point of contact for businesses across Texas and beyond.",
|
||||
"Downtown Houston, TX, USA\nPhone: +1 832-350-0465"
|
||||
"Downtown Houston, TX, USA\nPhone: +1 832-350-0465\nEmail: info@aigrowthtechnologies.com"
|
||||
],
|
||||
"list": [],
|
||||
"cta": null
|
||||
@@ -139,7 +139,7 @@
|
||||
"subtitle": "Partner Office",
|
||||
"body": [
|
||||
"A US point of contact based in South Florida.",
|
||||
"Brickell Avenue, FL, USA\nPhone: +1 954-235-4974"
|
||||
"Brickell Avenue, FL, USA\nPhone: +1 954-235-4974\nEmail: info@aigrowthtechnologies.com"
|
||||
],
|
||||
"list": [],
|
||||
"cta": null
|
||||
@@ -150,7 +150,7 @@
|
||||
"subtitle": "Partner Office",
|
||||
"body": [
|
||||
"A US point of contact in one of the world’s leading business markets.",
|
||||
"Manhattan Financial District, NY, USA\nPhone: +1 713-289-4577"
|
||||
"Manhattan Financial District, NY, USA\nPhone: +1 713-289-4577\nEmail: info@aigrowthtechnologies.com"
|
||||
],
|
||||
"list": [],
|
||||
"cta": null
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
"subtitle": "Oficina asociada",
|
||||
"body": [
|
||||
"Un punto de contacto en EE. UU. para empresas de Texas y más allá.",
|
||||
"Downtown Houston, TX, USA\nPhone: +1 832-350-0465"
|
||||
"Downtown Houston, TX, USA\nPhone: +1 832-350-0465\nEmail: info@aigrowthtechnologies.com"
|
||||
],
|
||||
"list": [],
|
||||
"cta": null
|
||||
@@ -139,7 +139,7 @@
|
||||
"subtitle": "Oficina asociada",
|
||||
"body": [
|
||||
"Un punto de contacto en EE. UU. ubicado en el sur de Florida.",
|
||||
"Brickell Avenue, FL, USA\nPhone: +1 954-235-4974"
|
||||
"Brickell Avenue, FL, USA\nPhone: +1 954-235-4974\nEmail: info@aigrowthtechnologies.com"
|
||||
],
|
||||
"list": [],
|
||||
"cta": null
|
||||
@@ -150,7 +150,7 @@
|
||||
"subtitle": "Oficina asociada",
|
||||
"body": [
|
||||
"Un punto de contacto en EE. UU. en uno de los mercados de negocios más importantes del mundo.",
|
||||
"Manhattan Financial District, NY, USA\nPhone: +1 713-289-4577"
|
||||
"Manhattan Financial District, NY, USA\nPhone: +1 713-289-4577\nEmail: info@aigrowthtechnologies.com"
|
||||
],
|
||||
"list": [],
|
||||
"cta": null
|
||||
|
||||
@@ -19,6 +19,8 @@ export const ar: UI = {
|
||||
openMenu: "فتح القائمة",
|
||||
closeMenu: "إغلاق القائمة",
|
||||
language: "اللغة",
|
||||
lightTheme: "التبديل إلى الوضع الفاتح",
|
||||
darkTheme: "التبديل إلى الوضع الداكن",
|
||||
},
|
||||
bookCall: "احجز مكالمة استراتيجية",
|
||||
services: {
|
||||
|
||||
@@ -19,6 +19,8 @@ export const en = {
|
||||
openMenu: "Open menu",
|
||||
closeMenu: "Close menu",
|
||||
language: "Language",
|
||||
lightTheme: "Switch to light theme",
|
||||
darkTheme: "Switch to dark theme",
|
||||
},
|
||||
bookCall: "Book a Strategy Call",
|
||||
services: {
|
||||
|
||||
@@ -19,6 +19,8 @@ export const es: UI = {
|
||||
openMenu: "Abrir menú",
|
||||
closeMenu: "Cerrar menú",
|
||||
language: "Idioma",
|
||||
lightTheme: "Cambiar a tema claro",
|
||||
darkTheme: "Cambiar a tema oscuro",
|
||||
},
|
||||
bookCall: "Agenda una llamada estratégica",
|
||||
services: {
|
||||
|
||||
Reference in New Issue
Block a user