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 SiteHeader from "@/components/site-header";
|
||||||
import SiteFooter from "@/components/site-footer";
|
import SiteFooter from "@/components/site-footer";
|
||||||
import Analytics, { AnalyticsNoScript } from "@/components/analytics";
|
import Analytics, { AnalyticsNoScript } from "@/components/analytics";
|
||||||
|
import { themeScript } from "@/components/theme-toggle";
|
||||||
import { dirOf, locales, localize, ogLocale } from "@/i18n/config";
|
import { dirOf, locales, localize, ogLocale } from "@/i18n/config";
|
||||||
import { mainNav } from "@/i18n/nav";
|
import { mainNav } from "@/i18n/nav";
|
||||||
import { getUI } from "@/i18n/server";
|
import { getUI } from "@/i18n/server";
|
||||||
@@ -51,7 +52,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
|
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
themeColor: "#000000",
|
themeColor: "#000000",
|
||||||
colorScheme: "dark",
|
colorScheme: "dark light",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({ children }: LayoutProps<"/[lang]">) {
|
export default async function RootLayout({ children }: LayoutProps<"/[lang]">) {
|
||||||
@@ -63,6 +64,8 @@ export default async function RootLayout({ children }: LayoutProps<"/[lang]">) {
|
|||||||
closeMenu: t.nav.closeMenu,
|
closeMenu: t.nav.closeMenu,
|
||||||
language: t.nav.language,
|
language: t.nav.language,
|
||||||
logoHome: t.logoHome,
|
logoHome: t.logoHome,
|
||||||
|
lightTheme: t.nav.lightTheme,
|
||||||
|
darkTheme: t.nav.darkTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -71,7 +74,12 @@ export default async function RootLayout({ children }: LayoutProps<"/[lang]">) {
|
|||||||
dir={dirOf(locale)}
|
dir={dirOf(locale)}
|
||||||
className={`${inter.variable} ${arabic.variable}`}
|
className={`${inter.variable} ${arabic.variable}`}
|
||||||
data-scroll-behavior="smooth"
|
data-scroll-behavior="smooth"
|
||||||
|
// The theme script sets data-theme before React hydrates.
|
||||||
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
|
<head>
|
||||||
|
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<AnalyticsNoScript />
|
<AnalyticsNoScript />
|
||||||
<a href="#main" className="skip-link">
|
<a href="#main" className="skip-link">
|
||||||
|
|||||||
+133
-33
@@ -13,6 +13,13 @@
|
|||||||
--fumo: #8f8f8f;
|
--fumo: #8f8f8f;
|
||||||
--piombo: #666666;
|
--piombo: #666666;
|
||||||
--argento: #d2d2d2;
|
--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;
|
--font-sans: var(--font-inter), "Helvetica Neue", Arial, sans-serif;
|
||||||
|
|
||||||
@@ -28,6 +35,23 @@
|
|||||||
--dir: -1;
|
--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,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
@@ -55,7 +79,7 @@ body {
|
|||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: var(--rosso);
|
background: var(--rosso);
|
||||||
color: var(--bianco);
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -220,7 +244,7 @@ a {
|
|||||||
.btn-ghost {
|
.btn-ghost {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--bianco);
|
color: var(--bianco);
|
||||||
border-color: rgba(255, 255, 255, 0.35);
|
border-color: rgba(var(--ink-rgb), 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-ghost:hover {
|
.btn-ghost:hover {
|
||||||
@@ -261,7 +285,7 @@ a {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
border: 1px solid rgba(var(--ink-rgb), 0.28);
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -303,7 +327,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.site-header.is-scrolled {
|
.site-header.is-scrolled {
|
||||||
background: rgba(0, 0, 0, 0.82);
|
background: rgba(var(--paper-rgb), 0.82);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
-webkit-backdrop-filter: blur(14px);
|
-webkit-backdrop-filter: blur(14px);
|
||||||
border-color: var(--grafite);
|
border-color: var(--grafite);
|
||||||
@@ -350,6 +374,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo-type b {
|
.logo-type b {
|
||||||
|
white-space: nowrap;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.2em;
|
letter-spacing: 0.2em;
|
||||||
@@ -376,7 +401,7 @@ a {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0.2em;
|
letter-spacing: 0.2em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: rgba(255, 255, 255, 0.78);
|
color: rgba(var(--ink-rgb), 0.78);
|
||||||
transition: color 0.3s var(--ease);
|
transition: color 0.3s var(--ease);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,9 +537,9 @@ a {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(0, 0, 0, 0.55) 0%, transparent 22%),
|
linear-gradient(180deg, rgba(var(--paper-rgb), 0.55) 0%, transparent 22%),
|
||||||
linear-gradient(0deg, #000 4%, rgba(0, 0, 0, 0.6) 34%, transparent 62%),
|
linear-gradient(0deg, var(--nero) 4%, rgba(var(--paper-rgb), 0.6) 34%, transparent 62%),
|
||||||
linear-gradient(90deg, rgba(0, 0, 0, 0.7) 0%, transparent 55%);
|
linear-gradient(90deg, rgba(var(--paper-rgb), 0.7) 0%, transparent 55%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,13 +883,13 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cap-map line {
|
.cap-map line {
|
||||||
stroke: #262626;
|
stroke: var(--cap-line);
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
transition: stroke 0.5s var(--ease);
|
transition: stroke 0.5s var(--ease);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cap-map line.on {
|
.cap-map line.on {
|
||||||
stroke: rgba(255, 255, 255, 0.6);
|
stroke: rgba(var(--ink-rgb), 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cap-map .node {
|
.cap-map .node {
|
||||||
@@ -882,7 +907,7 @@ a {
|
|||||||
|
|
||||||
.cap-map .core {
|
.cap-map .core {
|
||||||
fill: var(--nero);
|
fill: var(--nero);
|
||||||
stroke: rgba(255, 255, 255, 0.4);
|
stroke: rgba(var(--ink-rgb), 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cap-map text {
|
.cap-map text {
|
||||||
@@ -1002,8 +1027,8 @@ a {
|
|||||||
grid-column: 1 / span 6;
|
grid-column: 1 / span 6;
|
||||||
position: relative;
|
position: relative;
|
||||||
aspect-ratio: 600 / 412;
|
aspect-ratio: 600 / 412;
|
||||||
background: #070707;
|
background: var(--panel);
|
||||||
outline: 1px solid #1c1c1c;
|
outline: 1px solid var(--panel-line);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1071,7 +1096,7 @@ a {
|
|||||||
/* journey viz */
|
/* journey viz */
|
||||||
.viz-journey .path {
|
.viz-journey .path {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: rgba(255, 255, 255, 0.16);
|
stroke: rgba(var(--ink-rgb), 0.16);
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1099,7 +1124,7 @@ a {
|
|||||||
|
|
||||||
.viz-journey .node {
|
.viz-journey .node {
|
||||||
fill: var(--nero);
|
fill: var(--nero);
|
||||||
stroke: rgba(255, 255, 255, 0.6);
|
stroke: rgba(var(--ink-rgb), 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.viz-journey .goal {
|
.viz-journey .goal {
|
||||||
@@ -1117,11 +1142,11 @@ a {
|
|||||||
/* radar viz */
|
/* radar viz */
|
||||||
.viz-radar .ring {
|
.viz-radar .ring {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: rgba(255, 255, 255, 0.12);
|
stroke: rgba(var(--ink-rgb), 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.viz-radar .axis {
|
.viz-radar .axis {
|
||||||
stroke: rgba(255, 255, 255, 0.07);
|
stroke: rgba(var(--ink-rgb), 0.07);
|
||||||
}
|
}
|
||||||
|
|
||||||
.viz-radar .sweep {
|
.viz-radar .sweep {
|
||||||
@@ -1160,12 +1185,12 @@ a {
|
|||||||
|
|
||||||
/* grid viz */
|
/* grid viz */
|
||||||
.viz-grid .cell {
|
.viz-grid .cell {
|
||||||
fill: rgba(255, 255, 255, 0.035);
|
fill: rgba(var(--ink-rgb), 0.035);
|
||||||
stroke: rgba(255, 255, 255, 0.08);
|
stroke: rgba(var(--ink-rgb), 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.viz-grid .cell.on {
|
.viz-grid .cell.on {
|
||||||
fill: rgba(255, 255, 255, 0.85);
|
fill: rgba(var(--ink-rgb), 0.85);
|
||||||
stroke: none;
|
stroke: none;
|
||||||
animation: cell 4.8s var(--ease) infinite;
|
animation: cell 4.8s var(--ease) infinite;
|
||||||
animation-delay: var(--t);
|
animation-delay: var(--t);
|
||||||
@@ -1201,7 +1226,7 @@ a {
|
|||||||
|
|
||||||
.viz-grid .frame {
|
.viz-grid .frame {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: rgba(255, 255, 255, 0.5);
|
stroke: rgba(var(--ink-rgb), 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
@@ -1290,7 +1315,7 @@ a {
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
width: 96px;
|
width: 96px;
|
||||||
height: 96px;
|
height: 96px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
border: 1px solid rgba(var(--ink-rgb), 0.35);
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -1360,7 +1385,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.journey ol::after {
|
.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: scaleX(0);
|
||||||
transform-origin: left;
|
transform-origin: left;
|
||||||
transition: transform 2.6s var(--ease) 0.2s;
|
transition: transform 2.6s var(--ease) 0.2s;
|
||||||
@@ -1434,7 +1459,7 @@ a {
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
.journey ol::after {
|
.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: scaleY(0);
|
||||||
transform-origin: top;
|
transform-origin: top;
|
||||||
}
|
}
|
||||||
@@ -1542,7 +1567,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gaps .break line {
|
.gaps .break line {
|
||||||
stroke: rgba(255, 255, 255, 0.45);
|
stroke: rgba(var(--ink-rgb), 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gaps .break .cut {
|
.gaps .break .cut {
|
||||||
@@ -1635,7 +1660,7 @@ a {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: -1;
|
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;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1758,7 +1783,7 @@ a {
|
|||||||
line-height: 0.9;
|
line-height: 0.9;
|
||||||
letter-spacing: -0.05em;
|
letter-spacing: -0.05em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: rgba(255, 255, 255, 0.07);
|
color: rgba(var(--ink-rgb), 0.07);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
@@ -1873,7 +1898,7 @@ a {
|
|||||||
|
|
||||||
.nav-panel-inner {
|
.nav-panel-inner {
|
||||||
padding: 22px 28px 26px;
|
padding: 22px 28px 26px;
|
||||||
background: rgba(0, 0, 0, 0.94);
|
background: rgba(var(--paper-rgb), 0.94);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
-webkit-backdrop-filter: blur(14px);
|
-webkit-backdrop-filter: blur(14px);
|
||||||
border: 1px solid var(--grafite);
|
border: 1px solid var(--grafite);
|
||||||
@@ -2115,7 +2140,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.link-card:hover {
|
.link-card:hover {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(var(--ink-rgb), 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-card:hover .arrow-link {
|
.link-card:hover .arrow-link {
|
||||||
@@ -2516,11 +2541,28 @@ a {
|
|||||||
resize: vertical;
|
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 input:focus,
|
||||||
.field textarea:focus {
|
.field textarea:focus {
|
||||||
border-color: var(--rosso);
|
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 {
|
.field input::placeholder {
|
||||||
color: var(--piombo);
|
color: var(--piombo);
|
||||||
}
|
}
|
||||||
@@ -2680,6 +2722,64 @@ a {
|
|||||||
color: var(--bianco);
|
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] {
|
.lang-switch [aria-current] {
|
||||||
color: var(--bianco);
|
color: var(--bianco);
|
||||||
box-shadow: inset 0 -1px 0 var(--rosso);
|
box-shadow: inset 0 -1px 0 var(--rosso);
|
||||||
@@ -2706,7 +2806,7 @@ a {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
max-width: 340px;
|
max-width: 340px;
|
||||||
padding: 14px 18px;
|
padding: 14px 18px;
|
||||||
border: 1px solid var(--grafite);
|
border: 1px solid var(--piombo);
|
||||||
transition: border-color 0.3s var(--ease);
|
transition: border-color 0.3s var(--ease);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2739,7 +2839,7 @@ a {
|
|||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
flex: none;
|
flex: none;
|
||||||
border: 1px solid var(--fumo);
|
border: 1.5px solid var(--argento);
|
||||||
transition:
|
transition:
|
||||||
border-color 0.3s var(--ease),
|
border-color 0.3s var(--ease),
|
||||||
background-color 0.3s var(--ease);
|
background-color 0.3s var(--ease);
|
||||||
@@ -2754,7 +2854,7 @@ a {
|
|||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: var(--bianco);
|
stroke: #fff;
|
||||||
stroke-width: 1.8;
|
stroke-width: 1.8;
|
||||||
stroke-dasharray: 16;
|
stroke-dasharray: 16;
|
||||||
stroke-dashoffset: 16;
|
stroke-dashoffset: 16;
|
||||||
@@ -2810,7 +2910,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[dir="rtl"] .journey ol::after {
|
[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;
|
transform-origin: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,9 +98,13 @@ export default function ContactForm({
|
|||||||
<span>{t.website}</span>
|
<span>{t.website}</span>
|
||||||
<input
|
<input
|
||||||
name="website"
|
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"
|
inputMode="url"
|
||||||
placeholder="https://"
|
autoComplete="url"
|
||||||
|
autoCapitalize="none"
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder="company.com"
|
||||||
maxLength={300}
|
maxLength={300}
|
||||||
defaultValue={values?.website}
|
defaultValue={values?.website}
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ export default function GlobeCanvas({ layout = "hero", className = "hero-canvas"
|
|||||||
|
|
||||||
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
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
|
// Fibonacci sphere
|
||||||
const COUNT = 1500;
|
const COUNT = 1500;
|
||||||
const points: Vec[] = [];
|
const points: Vec[] = [];
|
||||||
@@ -159,15 +163,15 @@ export default function GlobeCanvas({ layout = "hero", className = "hero-canvas"
|
|||||||
|
|
||||||
// soft halo
|
// soft halo
|
||||||
const halo = ctx.createRadialGradient(cx, cy, R * 0.7, cx, cy, R * 1.45);
|
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(0, `rgba(${ink},0.045)`);
|
||||||
halo.addColorStop(1, "rgba(255,255,255,0)");
|
halo.addColorStop(1, `rgba(${ink},0)`);
|
||||||
ctx.fillStyle = halo;
|
ctx.fillStyle = halo;
|
||||||
ctx.fillRect(0, 0, w, h);
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
|
||||||
// outline
|
// outline
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(cx, cy, R * 1.0, 0, Math.PI * 2);
|
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.lineWidth = 1;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
@@ -177,7 +181,7 @@ export default function GlobeCanvas({ layout = "hero", className = "hero-canvas"
|
|||||||
const front = p.z > 0;
|
const front = p.z > 0;
|
||||||
const a = front ? 0.18 + p.z * 0.62 : 0.05 + (1 + p.z) * 0.05;
|
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;
|
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);
|
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 b = proj[s];
|
||||||
const vis = (a.z + b.z) / 2;
|
const vis = (a.z + b.z) / 2;
|
||||||
if (vis < -0.15) continue;
|
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.beginPath();
|
||||||
ctx.moveTo(a.x, a.y);
|
ctx.moveTo(a.x, a.y);
|
||||||
ctx.lineTo(b.x, b.y);
|
ctx.lineTo(b.x, b.y);
|
||||||
@@ -224,7 +228,7 @@ export default function GlobeCanvas({ layout = "hero", className = "hero-canvas"
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(p.x, p.y, 3 + pulse * 14, 0, Math.PI * 2);
|
ctx.arc(p.x, p.y, 3 + pulse * 14, 0, Math.PI * 2);
|
||||||
ctx.stroke();
|
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);
|
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);
|
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 io = new IntersectionObserver(([entry]) => {
|
||||||
const visible = entry.isIntersecting;
|
const visible = entry.isIntersecting;
|
||||||
if (visible && !running) {
|
if (visible && !running) {
|
||||||
@@ -263,6 +273,7 @@ export default function GlobeCanvas({ layout = "hero", className = "hero-canvas"
|
|||||||
running = false;
|
running = false;
|
||||||
cancelAnimationFrame(raf);
|
cancelAnimationFrame(raf);
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
|
themeObserver.disconnect();
|
||||||
io.disconnect();
|
io.disconnect();
|
||||||
window.removeEventListener("pointermove", onMove);
|
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 }) {
|
export function LogoStacked({ href, label }: { href: string; label: string }) {
|
||||||
return (
|
return (
|
||||||
<Link href={href} className="logo-stacked" aria-label={label}>
|
<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>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function JourneyViz({ labels }: { labels: VisualLabels }) {
|
|||||||
return (
|
return (
|
||||||
<svg className="viz viz-journey" viewBox={`0 0 ${W} ${H}`} aria-hidden="true">
|
<svg className="viz viz-journey" viewBox={`0 0 ${W} ${H}`} aria-hidden="true">
|
||||||
{[180, 280, 380].map((x) => (
|
{[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) => {
|
{sources.map((s, i) => {
|
||||||
const y = top + i * gap;
|
const y = top + i * gap;
|
||||||
@@ -98,7 +98,7 @@ function RadarViz({ labels }: { labels: VisualLabels }) {
|
|||||||
))}
|
))}
|
||||||
<g className="sweep">
|
<g className="sweep">
|
||||||
<path d={`M${cx} ${cy} L${cx + sweepR} ${cy} A${sweepR} ${sweepR} 0 0 0 ${ex} ${ey} Z`} fill="url(#sweepGrad)" />
|
<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>
|
</g>
|
||||||
{dots.map((d, i) => {
|
{dots.map((d, i) => {
|
||||||
const x = cx + Math.cos(rad(d.a)) * d.r;
|
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 { usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Logo from "./logo";
|
import Logo from "./logo";
|
||||||
|
import ThemeToggle from "./theme-toggle";
|
||||||
import { localeNames, localeShort, locales, localize, splitLocale, type Locale } from "@/i18n/config";
|
import { localeNames, localeShort, locales, localize, splitLocale, type Locale } from "@/i18n/config";
|
||||||
import type { NavItem } from "@/i18n/nav";
|
import type { NavItem } from "@/i18n/nav";
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ export type HeaderLabels = {
|
|||||||
closeMenu: string;
|
closeMenu: string;
|
||||||
language: string;
|
language: string;
|
||||||
logoHome: string;
|
logoHome: string;
|
||||||
|
lightTheme: string;
|
||||||
|
darkTheme: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function LanguageSwitch({ locale, path, label }: { locale: Locale; path: string; label: 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>
|
</nav>
|
||||||
<LanguageSwitch locale={locale} path={path} label={t.language} />
|
<LanguageSwitch locale={locale} path={path} label={t.language} />
|
||||||
|
<ThemeToggle t={t} />
|
||||||
<Link href={contactHref} className="btn btn-solid header-cta">
|
<Link href={contactHref} className="btn btn-solid header-cta">
|
||||||
{t.bookCall} <span className="arrow">→</span>
|
{t.bookCall} <span className="arrow">→</span>
|
||||||
</Link>
|
</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": "مكتب شريك",
|
"subtitle": "مكتب شريك",
|
||||||
"body": [
|
"body": [
|
||||||
"نقطة تواصل في الولايات المتحدة للشركات في تكساس وخارجها.",
|
"نقطة تواصل في الولايات المتحدة للشركات في تكساس وخارجها.",
|
||||||
"Downtown Houston, TX, USA\nPhone: +1 832-350-0465"
|
"Downtown Houston, TX, USA\nPhone: +1 832-350-0465\nEmail: info@aigrowthtechnologies.com"
|
||||||
],
|
],
|
||||||
"list": [],
|
"list": [],
|
||||||
"cta": null
|
"cta": null
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
"subtitle": "مكتب شريك",
|
"subtitle": "مكتب شريك",
|
||||||
"body": [
|
"body": [
|
||||||
"نقطة تواصل في الولايات المتحدة مقرها جنوب فلوريدا.",
|
"نقطة تواصل في الولايات المتحدة مقرها جنوب فلوريدا.",
|
||||||
"Brickell Avenue, FL, USA\nPhone: +1 954-235-4974"
|
"Brickell Avenue, FL, USA\nPhone: +1 954-235-4974\nEmail: info@aigrowthtechnologies.com"
|
||||||
],
|
],
|
||||||
"list": [],
|
"list": [],
|
||||||
"cta": null
|
"cta": null
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
"subtitle": "مكتب شريك",
|
"subtitle": "مكتب شريك",
|
||||||
"body": [
|
"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": [],
|
"list": [],
|
||||||
"cta": null
|
"cta": null
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
"subtitle": "Partner Office",
|
"subtitle": "Partner Office",
|
||||||
"body": [
|
"body": [
|
||||||
"A US point of contact for businesses across Texas and beyond.",
|
"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": [],
|
"list": [],
|
||||||
"cta": null
|
"cta": null
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
"subtitle": "Partner Office",
|
"subtitle": "Partner Office",
|
||||||
"body": [
|
"body": [
|
||||||
"A US point of contact based in South Florida.",
|
"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": [],
|
"list": [],
|
||||||
"cta": null
|
"cta": null
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
"subtitle": "Partner Office",
|
"subtitle": "Partner Office",
|
||||||
"body": [
|
"body": [
|
||||||
"A US point of contact in one of the world’s leading business markets.",
|
"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": [],
|
"list": [],
|
||||||
"cta": null
|
"cta": null
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
"subtitle": "Oficina asociada",
|
"subtitle": "Oficina asociada",
|
||||||
"body": [
|
"body": [
|
||||||
"Un punto de contacto en EE. UU. para empresas de Texas y más allá.",
|
"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": [],
|
"list": [],
|
||||||
"cta": null
|
"cta": null
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
"subtitle": "Oficina asociada",
|
"subtitle": "Oficina asociada",
|
||||||
"body": [
|
"body": [
|
||||||
"Un punto de contacto en EE. UU. ubicado en el sur de Florida.",
|
"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": [],
|
"list": [],
|
||||||
"cta": null
|
"cta": null
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
"subtitle": "Oficina asociada",
|
"subtitle": "Oficina asociada",
|
||||||
"body": [
|
"body": [
|
||||||
"Un punto de contacto en EE. UU. en uno de los mercados de negocios más importantes del mundo.",
|
"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": [],
|
"list": [],
|
||||||
"cta": null
|
"cta": null
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export const ar: UI = {
|
|||||||
openMenu: "فتح القائمة",
|
openMenu: "فتح القائمة",
|
||||||
closeMenu: "إغلاق القائمة",
|
closeMenu: "إغلاق القائمة",
|
||||||
language: "اللغة",
|
language: "اللغة",
|
||||||
|
lightTheme: "التبديل إلى الوضع الفاتح",
|
||||||
|
darkTheme: "التبديل إلى الوضع الداكن",
|
||||||
},
|
},
|
||||||
bookCall: "احجز مكالمة استراتيجية",
|
bookCall: "احجز مكالمة استراتيجية",
|
||||||
services: {
|
services: {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export const en = {
|
|||||||
openMenu: "Open menu",
|
openMenu: "Open menu",
|
||||||
closeMenu: "Close menu",
|
closeMenu: "Close menu",
|
||||||
language: "Language",
|
language: "Language",
|
||||||
|
lightTheme: "Switch to light theme",
|
||||||
|
darkTheme: "Switch to dark theme",
|
||||||
},
|
},
|
||||||
bookCall: "Book a Strategy Call",
|
bookCall: "Book a Strategy Call",
|
||||||
services: {
|
services: {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export const es: UI = {
|
|||||||
openMenu: "Abrir menú",
|
openMenu: "Abrir menú",
|
||||||
closeMenu: "Cerrar menú",
|
closeMenu: "Cerrar menú",
|
||||||
language: "Idioma",
|
language: "Idioma",
|
||||||
|
lightTheme: "Cambiar a tema claro",
|
||||||
|
darkTheme: "Cambiar a tema oscuro",
|
||||||
},
|
},
|
||||||
bookCall: "Agenda una llamada estratégica",
|
bookCall: "Agenda una llamada estratégica",
|
||||||
services: {
|
services: {
|
||||||
|
|||||||
Reference in New Issue
Block a user