diff --git a/apps/edr-landing/public/brand/edr-logo-green.png b/apps/edr-landing/public/brand/edr-logo-green.png new file mode 100644 index 000000000..434c51296 Binary files /dev/null and b/apps/edr-landing/public/brand/edr-logo-green.png differ diff --git a/apps/edr-landing/public/brand/edr-logo-white.png b/apps/edr-landing/public/brand/edr-logo-white.png new file mode 100644 index 000000000..0797d9982 Binary files /dev/null and b/apps/edr-landing/public/brand/edr-logo-white.png differ diff --git a/apps/edr-landing/public/brand/edr-mark-green.png b/apps/edr-landing/public/brand/edr-mark-green.png new file mode 100644 index 000000000..e1140b046 Binary files /dev/null and b/apps/edr-landing/public/brand/edr-mark-green.png differ diff --git a/apps/edr-landing/public/brand/edr-mark-white.png b/apps/edr-landing/public/brand/edr-mark-white.png new file mode 100644 index 000000000..15abd079f Binary files /dev/null and b/apps/edr-landing/public/brand/edr-mark-white.png differ diff --git a/apps/edr-landing/public/photos/freight-train.jpg b/apps/edr-landing/public/photos/freight-train.jpg new file mode 100644 index 000000000..da2f3a4b9 Binary files /dev/null and b/apps/edr-landing/public/photos/freight-train.jpg differ diff --git a/apps/edr-landing/public/photos/station.jpg b/apps/edr-landing/public/photos/station.jpg new file mode 100644 index 000000000..865e733ee Binary files /dev/null and b/apps/edr-landing/public/photos/station.jpg differ diff --git a/apps/edr-landing/src/app/globals.css b/apps/edr-landing/src/app/globals.css index 6aa66fed6..6075d287e 100644 --- a/apps/edr-landing/src/app/globals.css +++ b/apps/edr-landing/src/app/globals.css @@ -4,24 +4,53 @@ @layer base { body { - @apply bg-white text-slate-900 antialiased; + @apply bg-white text-edr-ink antialiased; } } /* ------------------------------------------------------------------ */ -/* Motion — lifted from the freight portal's landing page so the two */ -/* public surfaces share one visual language. */ +/* Motion */ +/* */ +/* Every hidden-by-default state is scoped to `html.js`, which an */ +/* inline script in the layout sets. With JavaScript off the page */ +/* renders plainly instead of staying invisible — fatal for a front */ +/* door whose whole job is two links. */ /* ------------------------------------------------------------------ */ +/* One-shot page-load choreography for the hero copy. */ +.js .edr-rise { + opacity: 0; + transform: translateY(18px); + animation: edr-rise 0.9s cubic-bezier(0.16, 1, 0.3, 1) both; + animation-delay: var(--rise-delay, 0ms); +} +@keyframes edr-rise { + to { + opacity: 1; + transform: none; + } +} + +/* Hero photos drift slowly while they are on screen. */ +.js .edr-kenburns { + animation: edr-kenburns 16s ease-in-out infinite alternate; + will-change: transform; +} +@keyframes edr-kenburns { + from { + transform: scale(1); + } + to { + transform: scale(1.08); + } +} + +/* Scroll reveal for the sections below the fold. */ .edr-reveal { transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1), transform 0.7s cubic-bezier(0.16, 1, 0.3, 1); } - -/* The hidden state is scoped to `html.js`, set by an inline script in the - layout. Without it a reader with JavaScript off would get a blank page — - fatal for a front door whose whole job is two links. */ .js .edr-reveal { opacity: 0; transform: translateY(24px); @@ -31,45 +60,6 @@ transform: none; } -/* Hero: rail streaks racing toward the horizon */ -.edr-rails { - position: absolute; - inset: 0; - background-image: repeating-linear-gradient( - 100deg, - transparent 0 46px, - rgba(52, 211, 153, 0.16) 46px 48px - ); - mask-image: radial-gradient(120% 90% at 78% 40%, #000 0%, transparent 70%); - -webkit-mask-image: radial-gradient(120% 90% at 78% 40%, #000 0%, transparent 70%); - animation: edr-rails 9s linear infinite; -} -@keyframes edr-rails { - to { - background-position: 480px 0; - } -} - -.edr-hero-glow { - position: absolute; - inset: 0; - background: - radial-gradient(46% 60% at 12% 22%, rgba(10, 138, 95, 0.45), transparent 70%), - radial-gradient(40% 55% at 88% 82%, rgba(242, 165, 22, 0.22), transparent 70%); - animation: edr-breathe 11s ease-in-out infinite; -} -@keyframes edr-breathe { - 0%, - 100% { - opacity: 0.75; - transform: scale(1); - } - 50% { - opacity: 1; - transform: scale(1.06); - } -} - /* Pulsing live dot */ .edr-pulse-dot { position: relative; @@ -79,6 +69,9 @@ border-radius: 9999px; background: #34d399; } +.edr-pulse-dot--green { + background: #008550; +} .edr-pulse-dot::after { content: ""; position: absolute; @@ -90,6 +83,18 @@ opacity: 0.55; animation: edr-ping 1.8s cubic-bezier(0, 0, 0.2, 1) infinite; } +.edr-ping { + position: relative; +} +.edr-ping::after { + content: ""; + position: absolute; + inset: -4px; + border-radius: 9999px; + background: currentColor; + opacity: 0.35; + animation: edr-ping 1.8s cubic-bezier(0, 0, 0.2, 1) infinite; +} @keyframes edr-ping { 0% { transform: scale(0.6); @@ -103,23 +108,6 @@ } /* The corridor, with a train light running its length */ -.edr-corridor { - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - height: 18px; -} -.edr-corridor::before { - content: ""; - position: absolute; - left: 0; - right: 0; - top: 50%; - height: 2px; - transform: translateY(-50%); - background: rgba(52, 211, 153, 0.28); -} .edr-corridor-node { position: relative; z-index: 1; @@ -159,16 +147,90 @@ } } +/* Mockups ported from the freight portal's landing page */ +.edr-float { + animation: edr-float 7s ease-in-out infinite; +} +@keyframes edr-float { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-8px); + } +} + +.edr-minitrack { + position: relative; + height: 14px; +} +.edr-minitrack::before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 50%; + height: 2px; + transform: translateY(-50%); + background: #c9e9db; + border-radius: 9999px; +} +.edr-minitrack-dot { + position: absolute; + top: 50%; + width: 9px; + height: 9px; + margin-top: -4.5px; + border-radius: 9999px; + background: #008550; + box-shadow: + 0 0 0 4px rgba(0, 133, 80, 0.18), + 0 0 14px 2px rgba(0, 133, 80, 0.5); + animation: edr-run-dot 6s ease-in-out infinite; +} +@keyframes edr-run-dot { + 0%, + 8% { + left: 0%; + } + 92%, + 100% { + left: calc(100% - 9px); + } +} + +.edr-grow { + transform-origin: bottom; + animation: edr-grow 0.8s cubic-bezier(0.16, 1, 0.3, 1) both; +} +@keyframes edr-grow { + from { + transform: scaleY(0); + } + to { + transform: scaleY(1); + } +} + @media (prefers-reduced-motion: reduce) { + .js .edr-rise, + .js .edr-kenburns { + opacity: 1; + transform: none; + animation: none; + } .js .edr-reveal { opacity: 1; transform: none; transition: none; } - .edr-rails, - .edr-hero-glow, .edr-pulse-dot::after, - .edr-corridor-train { + .edr-ping::after, + .edr-corridor-train, + .edr-float, + .edr-minitrack-dot, + .edr-grow { animation: none !important; } } diff --git a/apps/edr-landing/src/app/layout.tsx b/apps/edr-landing/src/app/layout.tsx index 60510d9f7..84e808b94 100644 --- a/apps/edr-landing/src/app/layout.tsx +++ b/apps/edr-landing/src/app/layout.tsx @@ -27,17 +27,26 @@ export const metadata: Metadata = { title: "Ethio-Djibouti Railway — Passenger & Freight", description: "Book a seat or ship a container on the same railway. Pick the service you need.", + images: [ + { + url: "/photos/station.jpg", + width: 1280, + height: 720, + alt: "Furi-Lebu station of the Ethio-Djibouti Railway", + }, + ], }, twitter: { card: "summary_large_image", title: "Ethio-Djibouti Railway — Passenger & Freight", description: "Book a seat or ship a container on the same railway. Pick the service you need.", + images: ["/photos/station.jpg"], }, icons: { - icon: "/edr-logo.png", - shortcut: "/edr-logo.png", - apple: "/edr-logo.png", + icon: "/brand/edr-mark-green.png", + shortcut: "/brand/edr-mark-green.png", + apple: "/brand/edr-mark-green.png", }, }; @@ -52,7 +61,7 @@ const organizationSchema = { "@type": "Organization", name: "Ethio-Djibouti Railway (EDR)", url: siteUrl, - logo: `${siteUrl}/edr-logo.png`, + logo: `${siteUrl}/brand/edr-logo-green.png`, contactPoint: { "@type": "ContactPoint", telephone: "9546", @@ -85,7 +94,7 @@ export default function RootLayout({ crossOrigin="anonymous" /> {/* Gates the reveal animation's hidden state (see globals.css) so the diff --git a/apps/edr-landing/src/app/page.tsx b/apps/edr-landing/src/app/page.tsx index 4ac3e506f..25da2acfb 100644 --- a/apps/edr-landing/src/app/page.tsx +++ b/apps/edr-landing/src/app/page.tsx @@ -1,70 +1,9 @@ -import { - ArrowRight, - Container, - Mail, - MapPin, - Phone, - TrainFront, -} from "lucide-react"; - -import { CountUp, Reveal } from "@/components/motion"; -import { FREIGHT_URL, PASSENGER_URL } from "@/lib/apps"; - -/* ------------------------------------------------------------------ */ -/* Content */ -/* ------------------------------------------------------------------ */ - -const doors = [ - { - key: "passenger", - icon: TrainFront, - name: "Passenger", - lede: "Travelling yourself.", - href: PASSENGER_URL, - cta: "Open Passenger", - points: [ - "Search trains and book a seat", - "Choose your class and seat", - "Pay with Telebirr or CBE Birr", - "Manage or reschedule a trip", - ], - }, - { - key: "freight", - icon: Container, - name: "Freight", - lede: "Shipping goods.", - href: FREIGHT_URL, - cta: "Open Freight", - points: [ - "Book containers or bulk cargo", - "Track every wagon live", - "Customs-ready documents", - "Invoices and settlement", - ], - }, -] as const; - -const stations = [ - { name: "Addis Ababa", kind: "hub" }, - { name: "Mojo", kind: "stop" }, - { name: "Adama", kind: "stop" }, - { name: "Awash", kind: "stop" }, - { name: "Dire Dawa", kind: "hub" }, - { name: "Dewele", kind: "border" }, - { name: "Djibouti Port", kind: "hub" }, -] as const; - -const stats = [ - { value: 752, suffix: " km", label: "Addis Ababa to Djibouti, electrified standard gauge" }, - { value: 16, prefix: "~", suffix: " h", label: "Mojo Dry Port to Djibouti Port transit" }, - { value: 2, label: "Countries connected by one railway" }, - { text: "24/7", label: "Booking and tracking online" }, -] as const; - -/* ------------------------------------------------------------------ */ -/* Page */ -/* ------------------------------------------------------------------ */ +import { Corridor } from "@/components/corridor"; +import { Footer } from "@/components/footer"; +import { Header } from "@/components/header"; +import { Hero } from "@/components/hero"; +import { Portals } from "@/components/portals"; +import { Stats } from "@/components/stats"; export default function Home() { return ( @@ -78,9 +17,10 @@ export default function Home() {
-
+
+
@@ -88,308 +28,3 @@ export default function Home() { ); } - -function Wordmark({ subtitle = true }: { subtitle?: boolean }) { - return ( - - - - - - - EDR - {subtitle ? ( - - Ethio-Djibouti Railway - - ) : null} - - - ); -} - -function Header() { - return ( -
-
- - - - - -
-
- ); -} - -function Hero() { - return ( -
-
-
- -
-
- - - - Addis Ababa – Djibouti · 752 km electrified corridor - - - - -

- The Ethio-Djibouti Railway, - online. -

-
- - -

- Book a seat or ship a container on the same railway. Pick the - service you need. -

-
-
- - {/* The two doors — the reason this page exists. */} -
- {doors.map((door, index) => ( - - - - ))} -
- - -

- Travelling yourself?{" "} - Passenger.{" "} - Shipping goods?{" "} - Freight. -

-
-
-
- ); -} - -function Door({ - icon: Icon, - name, - lede, - href, - cta, - points, -}: (typeof doors)[number]) { - return ( - - - - - -

- {name} -

-

{lede}

- -
    - {points.map((point) => ( -
  • - - {point} -
  • - ))} -
- - - {cta} - - -
- ); -} - -function Corridor() { - return ( -
-
- -
- - - -
    - {stations.map((station, index) => { - const isFirst = index === 0; - const isLast = index === stations.length - 1; - - return ( -
  1. - - - {/* Absolute so a long label never widens its column — the - nodes have to stay evenly spaced along the rail. */} - - {station.name} - {station.kind === "border" ? " (border)" : ""} - -
  2. - ); - })} -
-
-
-
-
- ); -} - -function Stats() { - return ( -
-
- -
- {stats.map((stat) => ( -
-
- {"text" in stat ? ( - stat.text - ) : ( - - )} -
-
- {stat.label} -
-
- ))} -
-
-
-
- ); -} - -function Footer() { - return ( - - ); -} diff --git a/apps/edr-landing/src/components/brand.tsx b/apps/edr-landing/src/components/brand.tsx new file mode 100644 index 000000000..c450ecc2d --- /dev/null +++ b/apps/edr-landing/src/components/brand.tsx @@ -0,0 +1,42 @@ +// The official EDR logo, keyed from the brand JPEGs into transparent PNGs +// (see public/brand). `lockup` is the mark plus wordmark; `mark` is the swoosh. +const assets = { + lockup: { + white: "/brand/edr-logo-white.png", + green: "/brand/edr-logo-green.png", + width: 1600, + height: 668, + }, + mark: { + white: "/brand/edr-mark-white.png", + green: "/brand/edr-mark-green.png", + width: 1234, + height: 386, + }, +} as const; + +export function Logo({ + tone = "white", + variant = "lockup", + decorative = false, + className = "", +}: { + tone?: "white" | "green"; + variant?: "lockup" | "mark"; + decorative?: boolean; + className?: string; +}) { + const asset = assets[variant]; + + return ( + // eslint-disable-next-line @next/next/no-img-element -- static export, no optimizer + {decorative + ); +} diff --git a/apps/edr-landing/src/components/corridor.tsx b/apps/edr-landing/src/components/corridor.tsx new file mode 100644 index 000000000..1e129e881 --- /dev/null +++ b/apps/edr-landing/src/components/corridor.tsx @@ -0,0 +1,77 @@ +const stations = [ + { name: "Addis Ababa", kind: "hub", note: "Km 0" }, + { name: "Mojo", kind: "stop", note: "Dry port" }, + { name: "Adama", kind: "stop" }, + { name: "Awash", kind: "stop" }, + { name: "Dire Dawa", kind: "hub" }, + { name: "Dewele", kind: "border", note: "Customs" }, + { name: "Djibouti Port", kind: "hub", note: "Km 752" }, +] as const; + +/** The line itself: seven stations on one rail, with a train light running it. */ +export function Corridor() { + return ( +
+
+
+ + + +
    + {stations.map((station, index) => { + const isFirst = index === 0; + const isLast = index === stations.length - 1; + + return ( +
  1. + + + {/* Absolute so a long label never widens its column — the + nodes have to stay evenly spaced along the rail. */} + + + {station.name} + + {"note" in station ? ( + + {station.note} + + ) : null} + +
  2. + ); + })} +
+
+
+
+ ); +} diff --git a/apps/edr-landing/src/components/footer.tsx b/apps/edr-landing/src/components/footer.tsx new file mode 100644 index 000000000..cb55f2ca7 --- /dev/null +++ b/apps/edr-landing/src/components/footer.tsx @@ -0,0 +1,65 @@ +import { Mail, MapPin, Phone } from "lucide-react"; + +import { FREIGHT_URL, PASSENGER_URL } from "@/lib/apps"; + +import { Logo } from "./brand"; + +export function Footer() { + return ( + + ); +} diff --git a/apps/edr-landing/src/components/header.tsx b/apps/edr-landing/src/components/header.tsx new file mode 100644 index 000000000..06f71ad78 --- /dev/null +++ b/apps/edr-landing/src/components/header.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { FREIGHT_URL, PASSENGER_URL, PUBLICATIONS_URL } from "@/lib/apps"; + +import { Logo } from "./brand"; + +const link = + "rounded-md text-sm font-semibold text-white/80 transition hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-edr-light"; + +/** + * Sits transparently over the hero photos, then turns solid once the reader + * scrolls so it stays legible over the light sections. + */ +export function Header() { + const [scrolled, setScrolled] = useState(false); + + useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 40); + onScroll(); + window.addEventListener("scroll", onScroll, { passive: true }); + return () => window.removeEventListener("scroll", onScroll); + }, []); + + return ( +
+
+ + + + + +
+
+ ); +} diff --git a/apps/edr-landing/src/components/hero.tsx b/apps/edr-landing/src/components/hero.tsx new file mode 100644 index 000000000..e6bffdf99 --- /dev/null +++ b/apps/edr-landing/src/components/hero.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { ArrowRight, Container, TrainFront } from "lucide-react"; +import { useEffect, useState } from "react"; + +import { FREIGHT_URL, PASSENGER_URL } from "@/lib/apps"; + +const doors = [ + { + key: "passenger", + icon: TrainFront, + title: "Passenger", + body: "Book a seat, choose your class, pay with Telebirr or CBE Birr.", + href: PASSENGER_URL, + cta: "Open Passenger", + photo: { + src: "/photos/station.jpg", + width: 1280, + height: 720, + alt: "Furi-Lebu station in Addis Ababa, the Ethio-Djibouti Railway's passenger terminal", + position: "center 30%", + }, + }, + { + key: "freight", + icon: Container, + title: "Freight", + body: "Book wagons and containers, track every one of them live.", + href: FREIGHT_URL, + cta: "Open Freight", + photo: { + src: "/photos/freight-train.jpg", + width: 1280, + height: 1134, + alt: "An Ethio-Djibouti Railway locomotive hauling container wagons", + position: "45% 45%", + }, + }, +] as const; + +const ROTATE_MS = 7000; + +const rise = (ms: number) => + ({ "--rise-delay": `${ms}ms` }) as React.CSSProperties; + +/** + * One photograph at a time behind one message. The background alternates + * between the two portals' worlds on its own, and settles on whichever card + * the reader is hovering. + */ +export function Hero() { + const [active, setActive] = useState(0); + const [pinned, setPinned] = useState(null); + + useEffect(() => { + if (pinned !== null) return; + if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return; + + const id = window.setInterval( + () => setActive((index) => (index + 1) % doors.length), + ROTATE_MS, + ); + return () => window.clearInterval(id); + }, [pinned]); + + const shown = pinned ?? active; + + return ( +
+ {doors.map((door, index) => ( + // eslint-disable-next-line @next/next/no-img-element -- static export + {door.photo.alt} + ))} + + {/* Backdrop: a heavy ink wash so the message and cards always read. */} +
+
+ +
+

+ Addis Ababa to Djibouti, by rail +

+

+ One railway. +
+ Two ways to move. +

+

+ Book a seat or ship a container on the Ethio-Djibouti Railway. Pick + the service you need. +

+ + +
+
+ ); +} diff --git a/apps/edr-landing/src/components/portals.tsx b/apps/edr-landing/src/components/portals.tsx new file mode 100644 index 000000000..08632b203 --- /dev/null +++ b/apps/edr-landing/src/components/portals.tsx @@ -0,0 +1,253 @@ +import { + ArrowLeftRight, + ArrowRight, + Armchair, + CalendarDays, + FileCheck, + MapPin, + Radar, + Receipt, + Smartphone, + Ticket, + Users, +} from "lucide-react"; + +import { FREIGHT_URL, PASSENGER_URL } from "@/lib/apps"; + +import { Reveal } from "./motion"; + +/* ------------------------------------------------------------------ */ +/* Passenger — the trip search, as it looks in the portal */ +/* ------------------------------------------------------------------ */ + +function Field({ + label, + value, + icon: Icon, +}: { + label: string; + value: string; + icon: React.ElementType; +}) { + return ( +
+ + + {label} + {value} + +
+ ); +} + +function SearchMock() { + return ( +
+
+ + One way + + Round trip +
+ +
+ + + + + +
+ +
+ + +
+ + + Search trains + +
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Freight — the live tracking card from the freight portal */ +/* ------------------------------------------------------------------ */ + +const trackingStops = [ + { name: "Mojo Dry Port", time: "Departed 06:10", state: "done" }, + { name: "Adama", time: "08:05", state: "done" }, + { name: "Awash", time: "Now · 64 km/h", state: "current" }, + { name: "Dire Dawa", time: "ETA 14:30", state: "idle" }, + { name: "Dewele customs", time: "ETA 17:50", state: "idle" }, + { name: "Djibouti Port", time: "ETA 22:10", state: "idle" }, +] as const; + +function TrackingMock() { + return ( +
+
+
+

CN-2026-08417

+

+ Mojo Dry Port → Djibouti Port · 40ft × 12 +

+
+ + + In transit + +
+ +
    + {trackingStops.map((stop, index) => { + const isLast = index === trackingStops.length - 1; + const done = stop.state === "done"; + const current = stop.state === "current"; + const nextDone = !isLast && trackingStops[index + 1].state === "done"; + + return ( +
  1. + + + {!isLast ? ( + + ) : null} + + + + + {stop.name} + + {stop.time} + +
  2. + ); + })} +
+ +
+ {[ + ["Train", "TS-14 · 2 locos"], + ["Wagons", "12 / 12 loaded"], + ["Next gate", "Dewele customs"], + ].map(([key, value]) => ( +
+

{key}

+

{value}

+
+ ))} +
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Section */ +/* ------------------------------------------------------------------ */ + +const portals = [ + { + key: "passenger", + title: "Passenger", + lede: "Search a train, choose your class and seat, and pay with Telebirr or CBE Birr. Your e-ticket comes to your phone.", + points: [ + { icon: Armchair, text: "Economy, standard and VIP coaches" }, + { icon: Smartphone, text: "Pay with Telebirr, CBE Birr or card" }, + { icon: Ticket, text: "QR e-ticket by SMS, shown at the gate" }, + ], + href: PASSENGER_URL, + cta: "Open Passenger", + Mock: SearchMock, + }, + { + key: "freight", + title: "Freight", + lede: "Book wagons and containers, watch every train move along the line, and keep customs documents and invoices in one place.", + points: [ + { icon: Radar, text: "Live GPS position of every wagon" }, + { icon: FileCheck, text: "Interchange and customs documents, signed online" }, + { icon: Receipt, text: "Invoices settled with Telebirr, CBE Birr or eBirr" }, + ], + href: FREIGHT_URL, + cta: "Open Freight", + Mock: TrackingMock, + }, +] as const; + +export function Portals() { + return ( +
+
+ {portals.map(({ key, title, lede, points, href, cta, Mock }, index) => { + const flip = index % 2 === 1; + + return ( + 0 ? "border-t border-edr-border" : "" + }`} + > +
+

+ {title} +

+

+ {lede} +

+ +
    + {points.map(({ icon: Icon, text }) => ( +
  • + + + + {text} +
  • + ))} +
+ + + {cta} + + +
+ +
+ +
+
+ ); + })} +
+
+ ); +} diff --git a/apps/edr-landing/src/components/stats.tsx b/apps/edr-landing/src/components/stats.tsx new file mode 100644 index 000000000..92e644052 --- /dev/null +++ b/apps/edr-landing/src/components/stats.tsx @@ -0,0 +1,37 @@ +import { CountUp, Reveal } from "./motion"; + +const stats = [ + { value: 752, suffix: " km", label: "Addis Ababa to Djibouti, electrified standard gauge" }, + { value: 16, prefix: "~", suffix: " h", label: "Mojo Dry Port to Djibouti Port transit" }, + { value: 2, label: "Countries connected by one railway" }, + { text: "24/7", label: "Booking and tracking online" }, +] as const; + +export function Stats() { + return ( +
+
+ +
+ {stats.map((stat) => ( +
+
+ {"text" in stat ? ( + stat.text + ) : ( + + )} +
+
{stat.label}
+
+ ))} +
+
+
+
+ ); +} diff --git a/apps/edr-landing/src/lib/apps.ts b/apps/edr-landing/src/lib/apps.ts index 06d34a67f..371863708 100644 --- a/apps/edr-landing/src/lib/apps.ts +++ b/apps/edr-landing/src/lib/apps.ts @@ -11,6 +11,10 @@ export const PASSENGER_URL = origin( // The freight app serves its own marketing landing at `/`, so link past it: // `/portal` is the app itself, and redirects to /login when signed out. -export const FREIGHT_URL = `${origin( +const FREIGHT_ORIGIN = origin( process.env.NEXT_PUBLIC_FREIGHT_URL ?? "https://freight.edrsc.com", -)}/portal`; +); +export const FREIGHT_URL = `${FREIGHT_ORIGIN}/portal`; + +// Public notices and tariffs, published from the freight app. +export const PUBLICATIONS_URL = `${FREIGHT_ORIGIN}/publications`; diff --git a/apps/edr-landing/tailwind.config.js b/apps/edr-landing/tailwind.config.js index cf24e4a93..47e69aca7 100644 --- a/apps/edr-landing/tailwind.config.js +++ b/apps/edr-landing/tailwind.config.js @@ -4,19 +4,31 @@ export default { theme: { extend: { colors: { - // Palette lifted from the freight landing page so the two public - // surfaces read as one railway. + // One palette for the whole railway: the green is sampled from the + // official logo, the dark inks and light surfaces come from the + // freight portal so the two public surfaces read as one brand. edr: { ink: "#0c1a2b", "ink-2": "#10202f", - primary: "#0a8a5f", - "primary-dark": "#087049", + primary: "#008550", + "primary-dark": "#006b41", light: "#34d399", amber: "#f2a516", + surface: "#f7fafc", + border: "#e6ecf2", + muted: "#6b7c8e", }, }, fontFamily: { - sans: ["Inter", "ui-sans-serif", "system-ui", "sans-serif"], + sans: [ + '"Plus Jakarta Sans"', + "ui-sans-serif", + "system-ui", + "-apple-system", + "Segoe UI", + "Roboto", + "sans-serif", + ], }, }, },