Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions app/[locale]/certification/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { getTranslations, setRequestLocale } from "next-intl/server";
import { faBuilding, faBuildingShield, faUser, faUserGraduate } from "@fortawesome/free-solid-svg-icons";
import CertifiedIntegrators from "./CertifiedIntegrators";
import CertificationPath from "@/app/components/CertificationPath";
import { ButtonLink, Card, CardTitle, CtaBand, Feature, PageHero, Section, Steps } from "@/app/components/ui";
import CertifiedProof from "@/app/components/CertifiedProof";
import HeroMark from "@/app/components/HeroMark";
import { ButtonLink, Card, CardTitle, CtaBand, Eyebrow, Feature, Section, Steps } from "@/app/components/ui";
import { alternates, pageLocale, type PageParams } from "@/app/lib/seo";
import { LINKS } from "@/app/lib/site";

Expand All @@ -20,9 +22,22 @@ export default async function Page({ params }: { params: Promise<PageParams> })
const tracks = t.raw("curriculum.tracks") as { title: string; body: string }[];
return (
<>
<PageHero eyebrow={t("hero.eyebrow")} title={t("hero.title")} lead={t("hero.lead")}>
<ButtonLink href="/contact?topic=certification" variant="primary">{t("hero.cta")}</ButtonLink>
</PageHero>
<div className="ground">
<HeroMark />
<section className="hero container-x">
<div className="hero-grid">
<div>
<Eyebrow>{t("hero.eyebrow")}</Eyebrow>
<h1 className="mt-3">{t("hero.title")}</h1>
<p className="lead">{t("hero.lead")}</p>
<div className="ctas">
<ButtonLink href="/contact?topic=certification" variant="primary">{t("hero.cta")}</ButtonLink>
</div>
</div>
<CertifiedProof />
</div>
</section>
</div>

<Section eyebrow={t("why.eyebrow")} lead={t("why.lead")} />

Expand Down
9 changes: 7 additions & 2 deletions app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ export async function generateMetadata({ params }: { params: Promise<PageParams>
};
}

/** Sets data-theme before first paint: stored choice, else OS preference, else dark. */
/**
* Sets data-theme before first paint: stored choice, else OS preference, else
* dark. The server never renders the attribute itself: a client-side
* navigation between locales re-renders <html> and would reset a server value
* to "dark" without running this script again.
*/
const themeInitScript = `
(function(){try{var s=localStorage.getItem('mobiera-theme');var t=(s==='light'||s==='dark')?s:(window.matchMedia('(prefers-color-scheme: light)').matches?'light':'dark');document.documentElement.setAttribute('data-theme',t);}catch(e){document.documentElement.setAttribute('data-theme','dark');}})();
`;
Expand All @@ -57,7 +62,7 @@ export default async function RootLayout({ children, params }: { children: React
setRequestLocale(locale);
const t = await getTranslations("common");
return (
<html lang={HTML_LANG[locale]} data-theme="dark" suppressHydrationWarning className={`${sora.variable} ${manrope.variable} ${plexMono.variable} ${inter.variable} ${spaceGrotesk.variable}`}>
<html lang={HTML_LANG[locale]} suppressHydrationWarning className={`${sora.variable} ${manrope.variable} ${plexMono.variable} ${inter.variable} ${spaceGrotesk.variable}`}>
<head>
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
</head>
Expand Down
4 changes: 2 additions & 2 deletions app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Metadata } from "next";
import { getTranslations, setRequestLocale } from "next-intl/server";
import JsonLd from "@/app/components/JsonLd";
import HeroMark from "@/app/components/HeroMark";
import ProofOfTrust from "@/app/components/ProofOfTrust";
import CredentialCards from "@/app/components/CredentialCards";
import LogoWall from "@/app/components/LogoWall";
import NewsList from "@/app/components/NewsList";
import { ButtonLink, Card, CardTitle, Chips, Eyebrow, Feature, MoreLink, ProofStrip, Section } from "@/app/components/ui";
Expand Down Expand Up @@ -48,7 +48,7 @@ export default async function Home({ params }: { params: Promise<PageParams> })
<ButtonLink href="/certification">{t("hero.ctaCertification")}</ButtonLink>
</div>
</div>
<ProofOfTrust />
<CredentialCards />
</div>
</section>
</div>
Expand Down
25 changes: 20 additions & 5 deletions app/[locale]/trust/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import type { Metadata } from "next";
import { getTranslations, setRequestLocale } from "next-intl/server";
import { faCertificate, faDiagramProject, faHandshake } from "@fortawesome/free-solid-svg-icons";
import EcosystemTree from "@/app/components/EcosystemTree";
import HeroMark from "@/app/components/HeroMark";
import WalletPhone from "@/app/components/WalletPhone";
import VeranaLockup from "@/app/components/VeranaLockup";
import { ButtonLink, Card, CardTitle, Chips, CtaBand, MoreLink, PageHero, Section, UseCase } from "@/app/components/ui";
import { ButtonLink, Card, CardTitle, Chips, CtaBand, Eyebrow, MoreLink, Section, UseCase } from "@/app/components/ui";
import { TRUST_STANDARDS } from "@/app/lib/content";
import { alternates, pageLocale, type PageParams } from "@/app/lib/seo";
import { LINKS } from "@/app/lib/site";
Expand All @@ -20,10 +22,23 @@ export default async function Page({ params }: { params: Promise<PageParams> })
const t = await getTranslations("trust");
return (
<>
<PageHero eyebrow={t("hero.eyebrow")} title={t("hero.title")} lead={t("hero.lead")}>
<ButtonLink href="/trust/services" variant="primary">{t("hero.services")}</ButtonLink>
<ButtonLink href="/certification">{t("hero.certified")}</ButtonLink>
</PageHero>
<div className="ground">
<HeroMark />
<section className="hero container-x">
<div className="hero-grid">
<div>
<Eyebrow>{t("hero.eyebrow")}</Eyebrow>
<h1 className="mt-3">{t("hero.title")}</h1>
<p className="lead">{t("hero.lead")}</p>
<div className="ctas">
<ButtonLink href="/trust/services" variant="primary">{t("hero.services")}</ButtonLink>
<ButtonLink href="/certification">{t("hero.certified")}</ButtonLink>
</div>
</div>
<WalletPhone />
</div>
</section>
</div>

<Section eyebrow={t("roles.eyebrow")}>
<div className="grid-3">
Expand Down
51 changes: 51 additions & 0 deletions app/components/CertifiedProof.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getTranslations } from "next-intl/server";

/**
* Certification hero: what a customer's wallet shows when it meets a
* certified integrator. The company's DID resolving, its organization and
* service credentials, and the Verana Certified Integrator line highlighted,
* issued by Mobiera and valid; a chip ties it to the directory generated from
* the Trust Graph. Example data, labelled as such. Reuses the Proof-of-Trust
* panel styles (.panel, .pot-*) plus .cert-proof in globals.css. Design C of
* 12 September 2026.
*/
export default async function CertifiedProof() {
const t = await getTranslations("certification.proof");
const rows = t.raw("rows") as { label: string; detail: string; hi?: boolean }[];
return (
<figure className="proof-stage">
<div className="panel cert-proof" role="img" aria-label={t("ariaLabel")}>
<div className="panel-head">
<b>{t("title")}</b>
<span className="eyebrow">{t("tag")}</span>
</div>
<div className="pot-did">
<span className="eyebrow">{t("resolving")}</span>
<code>did:webvh:…:acme-integrators.example</code>
<span className="pot-org">{t("org")}</span>
</div>
<ul className="pot-rows">
{rows.map((r) => (
<li key={r.label} className={r.hi ? "hi" : undefined}>
<span className="pot-check" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
</span>
<span className="pot-label">{r.label}</span>
<span className="pot-detail">{r.detail}</span>
</li>
))}
</ul>
<div className="pot-foot">
<span className="eyebrow">{t("score")}</span>
<span className="pot-score" aria-label={t("scoreAria")}><i /><i /><i /><i /><i className="off" /></span>
<span className="pot-verdict">{t("verdict")}</span>
</div>
<div className="pot-dir" aria-hidden="true">
<i />
<div><b>{t("dir.title")}</b><small>{t("dir.body")}</small></div>
</div>
</div>
<figcaption className="eyebrow proof-cap">{t("caption")}</figcaption>
</figure>
);
}
84 changes: 84 additions & 0 deletions app/components/CredentialCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { getTranslations } from "next-intl/server";

/**
* Home hero: three example verifiable credentials as wallet cards, fanned
* like the boletos of the Verana Playground's events demo. A citizen
* identity, a business registration and the Verana Certified Integrator
* credential Mobiera issues. Every name is fictional and the caption says so.
* Text comes from home.cards in messages/*; the scenes are line art in
* currentColor so one drawing works on any of the gradients.
*/

function SceneId() {
return (
<svg viewBox="0 0 200 150" className="vc-scene" aria-hidden="true">
<path d="M100 14 l58 20 v38 c0 38 -32 60 -58 70 c-26 -10 -58 -32 -58 -70 v-38 z" fill="currentColor" opacity=".22" />
<path d="M100 30 l42 15 v28 c0 28 -23 45 -42 53 c-19 -8 -42 -25 -42 -53 v-28 z" fill="none" stroke="currentColor" strokeWidth="3" opacity=".55" />
<circle cx="100" cy="66" r="13" fill="currentColor" opacity=".7" />
<path d="M74 106 c4 -18 14 -24 26 -24 s22 6 26 24" fill="currentColor" opacity=".55" />
<path d="M20 138 H180" stroke="currentColor" strokeWidth="4" strokeLinecap="round" opacity=".5" />
</svg>
);
}

function SceneBusiness() {
return (
<svg viewBox="0 0 200 150" className="vc-scene" aria-hidden="true">
<rect x="22" y="70" width="38" height="68" fill="currentColor" opacity=".28" />
<rect x="70" y="36" width="52" height="102" fill="currentColor" opacity=".4" />
<rect x="132" y="58" width="44" height="80" fill="currentColor" opacity=".3" />
<path d="M80 52 h32 M80 68 h32 M80 84 h32 M80 100 h32 M80 116 h32 M140 74 h28 M140 90 h28 M140 106 h28 M30 86 h22 M30 102 h22 M30 118 h22" stroke="currentColor" strokeWidth="2.5" opacity=".5" />
<path d="M12 138 H188" stroke="currentColor" strokeWidth="4" strokeLinecap="round" opacity=".5" />
<circle cx="160" cy="26" r="12" fill="currentColor" opacity=".7" />
</svg>
);
}

function SceneIntegrator() {
return (
<svg viewBox="11 24 78 52" className="vc-scene" aria-hidden="true">
<path
d="M85.0299 42.5693L69.4759 27.0142C68.5204 26.0585 67.386 25.3004 66.1375 24.7832C64.8889 24.266 63.5508 24 62.1994 24C60.848 24 59.5099 24.266 58.2614 24.7832C57.0128 25.3004 55.8784 26.0585 54.9229 27.0142L49.5229 32.4141L44.1229 27.0142C42.1928 25.0841 39.575 24 36.8454 24C34.1158 24 31.498 25.0841 29.5679 27.0142L14.0139 42.5693C12.0841 44.4994 11 47.1168 11 49.8462C11 52.5755 12.0841 55.193 14.0139 57.123L29.5679 72.6782C30.5235 73.634 31.6581 74.3924 32.9068 74.9097C34.1555 75.427 35.4938 75.6929 36.8454 75.6929C38.197 75.6929 39.5353 75.427 40.784 74.9097C42.0327 74.3924 43.1673 73.634 44.1229 72.6782L49.5229 67.2783L54.9229 72.6782C55.8784 73.634 57.0127 74.3919 58.2613 74.9092C59.5098 75.4264 60.848 75.6929 62.1994 75.6929C63.5508 75.6929 64.889 75.4264 66.1375 74.9092C67.3861 74.3919 68.5205 73.634 69.4759 72.6782L85.0299 57.123C86.9597 55.193 88.0438 52.5755 88.0438 49.8462C88.0438 47.1168 86.9597 44.4994 85.0299 42.5693ZM38.8529 67.415C38.5885 67.6797 38.2746 67.89 37.929 68.0332C37.5834 68.1764 37.213 68.25 36.8389 68.25C36.4648 68.25 36.0944 68.1764 35.7488 68.0332C35.4033 67.89 35.0893 67.6797 34.8249 67.415L19.2709 51.8604C19.0063 51.596 18.7964 51.282 18.6532 50.9365C18.51 50.591 18.4362 50.2207 18.4362 49.8467C18.4362 49.4727 18.51 49.1023 18.6532 48.7568C18.7964 48.4113 19.0063 48.0973 19.2709 47.833L34.8249 32.2783C35.0893 32.0136 35.4032 31.8035 35.7487 31.6602C36.0943 31.5169 36.4648 31.4429 36.8389 31.4429C37.213 31.4429 37.5835 31.5169 37.929 31.6602C38.2746 31.8035 38.5886 32.0136 38.8529 32.2783L44.2529 37.6782L39.3639 42.5674C37.4361 44.4985 36.3533 47.1155 36.3533 49.8442C36.3533 52.5729 37.4361 55.19 39.3639 57.1211L44.2529 62.0103L38.8529 67.415ZM54.4069 47.832C54.6715 48.0963 54.8814 48.4104 55.0246 48.7559C55.1678 49.1014 55.2415 49.4717 55.2415 49.8457C55.2415 50.2197 55.1678 50.5901 55.0246 50.9355C54.8814 51.281 54.6715 51.5951 54.4069 51.8594L49.5179 56.748L44.6289 51.8594C44.0955 51.325 43.7959 50.6007 43.7959 49.8457C43.7959 49.0907 44.0955 48.3664 44.6289 47.832L49.5179 42.9414L54.4069 47.832ZM59.6269 42.5264L54.7789 37.6792L60.1789 32.2793C60.4434 32.0145 60.7575 31.8045 61.1033 31.6611C61.449 31.5178 61.8196 31.4438 62.1939 31.4438C62.5682 31.4438 62.9388 31.5178 63.2845 31.6611C63.6303 31.8045 63.9444 32.0145 64.2089 32.2793L79.7629 47.834C80.0275 48.0983 80.2374 48.4123 80.3806 48.7578C80.5238 49.1033 80.5976 49.4737 80.5976 49.8477C80.5976 50.2217 80.5238 50.592 80.3806 50.9375C80.2374 51.283 80.0275 51.597 79.7629 51.8613L64.2089 67.416C63.9443 67.6807 63.6302 67.8909 63.2845 68.0342C62.9387 68.1774 62.5682 68.251 62.1939 68.251C61.8197 68.251 61.4491 68.1774 61.1033 68.0342C60.7576 67.8909 60.4435 67.6807 60.1789 67.416L54.7789 62.0161L59.6699 57.1274C61.5843 55.2129 62.6671 52.6205 62.6836 49.9131C62.7001 47.2056 61.6489 44.6008 59.7579 42.6631"
fill="currentColor"
opacity=".35"
/>
</svg>
);
}

const SCENES = { id: SceneId, business: SceneBusiness, integrator: SceneIntegrator } as const;
type CardKey = keyof typeof SCENES;
const ORDER: CardKey[] = ["id", "business", "integrator"];

export default async function CredentialCards() {
const t = await getTranslations("home.cards");
return (
<figure className="vc-figure">
<div className="vc-stack" role="group" aria-label={t("ariaLabel")}>
{ORDER.map((key) => {
const Scene = SCENES[key];
return (
<article key={key} className={`vc-card vc-${key}`}>
<span className="vc-tab">{t(`${key}.tab`)}</span>
<Scene />
<div className="vc-body">
<b className="vc-holder">{t(`${key}.holder`)}</b>
<span className="vc-type">{t(`${key}.type`)}</span>
<span className="vc-issuer">{t(`${key}.issuer`)}</span>
<span className="vc-meta">
<span className="vc-check" aria-hidden="true">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
</span>
<span>{t(`${key}.status`)}</span>
<span className="vc-dot" aria-hidden="true">·</span>
<span>{t(`${key}.valid`)}</span>
</span>
</div>
</article>
);
})}
</div>
<figcaption className="eyebrow vc-caption">{t("caption")}</figcaption>
</figure>
);
}
5 changes: 3 additions & 2 deletions app/components/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ export default function Nav() {
))}
</nav>
<div className="flex items-center gap-2">
<Link href={pathname} locale={other} className="lang-switch" lang={other === "es" ? "es-419" : "en"} aria-label={t("switchLocale")} title={t("switchLocale")}>
{/* A full navigation, not a client-side one: the proxy records the choice in the NEXT_LOCALE cookie and redirects to the canonical path. */}
<a href={`/${other}${pathname === "/" ? "" : pathname}`} className="lang-switch" lang={other === "es" ? "es-419" : "en"} aria-label={t("switchLocale")} title={t("switchLocale")}>
{t("switchLocaleShort")}
</Link>
</a>
<button type="button" className="theme-toggle" aria-label={mounted && theme === "light" ? t("toDark") : t("toLight")} onClick={toggleTheme}>
{mounted && theme === "light" ? (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" /></svg>
Expand Down
53 changes: 53 additions & 0 deletions app/components/WalletPhone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { getTranslations } from "next-intl/server";

const MARK = "M85 42.6 69.5 27A9.7 9.7 0 0 0 55 27l-5.5 5.4L44 27a9.7 9.7 0 0 0-14.5 0L14 42.6a9.7 9.7 0 0 0 0 14.5L29.6 72.7a9.7 9.7 0 0 0 14.5 0l5.4-5.4 5.4 5.4a9.7 9.7 0 0 0 14.6 0L85 57.1a9.7 9.7 0 0 0 0-14.5ZM38.9 67.4a2.9 2.9 0 0 1-4 0L19.3 51.9a2.9 2.9 0 0 1 0-4l15.6-15.6a2.9 2.9 0 0 1 4 0l5.4 5.4-4.9 4.9a10.3 10.3 0 0 0 0 14.6l4.9 4.9-5.4 5.4Zm15.5-19.6a2.9 2.9 0 0 1 0 4l-4.9 4.9-4.9-4.9a2.9 2.9 0 0 1 0-4l4.9-4.9 4.9 4.9Zm5.2-5.3-4.8-4.8 5.4-5.4a2.9 2.9 0 0 1 4 0l15.6 15.6a2.9 2.9 0 0 1 0 4L64.2 67.4a2.9 2.9 0 0 1-4 0l-5.4-5.4 4.9-4.9a10.3 10.3 0 0 0 0-14.6";

/**
* Verifiable Credentials hero: a phone running a fictional wallet at rest.
* The home screen with the three example credentials stacked as cards, the
* recent activity and a tab bar. Pure markup and CSS (.wp-* in globals.css),
* so it follows the theme; the phone is drawn at 296x612 and scaled to the
* hero height. Text in trust.phone (messages/*). Design A of 12 September
* 2026.
*/
export default async function WalletPhone() {
const t = await getTranslations("trust.phone");
const cards = ["id", "business", "cert"] as const;
const activity = t.raw("activity") as { text: string; when: string }[];
const tabs = t.raw("tabs") as string[];
return (
<div className="wp-stage" role="img" aria-label={t("ariaLabel")}>
<div className="wp-scale" aria-hidden="true">
<div className="wp-phone">
<div className="wp-screen">
<div className="wp-status"><span>9:41</span><span>●●● ▮</span></div>
<div className="wp-apphead"><b>{t("app")}</b><span className="wp-avatar" /></div>
<p className="wp-sub">{t("sub")}</p>
<div className="wp-list">
{cards.map((c) => (
<div key={c} className={`wp-vc wp-${c}`}>
<span className="wp-tab">{t(`cards.${c}.tab`)}</span>
<b>{t(`cards.${c}.holder`)}</b>
<span>{t(`cards.${c}.type`)}</span>
<span>{t(`cards.${c}.issuer`)}</span>
{c === "cert" && <svg className="wp-mark" viewBox="11 24 78 52"><path d={MARK} fill="currentColor" /></svg>}
</div>
))}
</div>
<div className="wp-act">
<span className="wp-act-h">{t("activityTitle")}</span>
{activity.map((a) => (
<div key={a.text} className="wp-act-i"><i />{a.text}<small>{a.when}</small></div>
))}
</div>
<div className="wp-tabbar">
{tabs.map((label, i) => (
<span key={label} className={i === 0 ? "on" : undefined}><i />{label}</span>
))}
</div>
</div>
</div>
</div>
</div>
);
}
Loading
Loading