diff --git a/app/components/marketing.css b/app/components/marketing.css index 0c69695..351560d 100644 --- a/app/components/marketing.css +++ b/app/components/marketing.css @@ -89,6 +89,63 @@ } } +/* Release banner */ +.release-banner { + border-bottom: 1px solid hsl(var(--border)); + background: color-mix(in srgb, hsl(var(--warning)) 10%, hsl(var(--card))); +} + +.release-banner__row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 1rem; + padding-block: 0.75rem; + border-inline-start: 4px solid hsl(var(--warning)); +} + +.release-banner__badge { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: hsl(var(--foreground)); + white-space: nowrap; +} + +.release-banner__dot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: hsl(var(--warning)); + flex-shrink: 0; +} + +.release-banner__text { + margin: 0; + font-size: 0.875rem; + color: hsl(var(--muted-foreground)); +} + +.release-banner__text strong { + color: hsl(var(--foreground)); +} + +.release-banner__link { + color: hsl(var(--primary)); + text-decoration: underline; + text-underline-offset: 2px; +} + +.release-banner__link:hover, +.release-banner__link:focus-visible { + color: hsl(var(--foreground)); +} + /* Hero */ .hero { position: relative; @@ -138,6 +195,14 @@ grid-template-columns: 1.1fr 0.9fr; } +.hero__copy { + /* The headline's available width is this column's width, not the + viewport's -- at the two-column desktop breakpoint this column is well + under half the viewport. A container query (not a vw-based clamp) lets + the headline size track its actual column. */ + container-type: inline-size; +} + .hero__eyebrow { font-family: var(--font-mono); font-size: 0.875rem; @@ -148,18 +213,33 @@ } .hero__headline { - font-size: 3.25rem; - line-height: 1.1; + /* A floor for the smallest phones, a cqw-driven middle so it scales with + this column's own width (see .hero__copy's container-type), a ceiling + so it never grows past the original design size. Tuned so the two-line + headline stays at or under 3 wrapped lines at every width this site is + checked at, including the narrower column the two-column desktop + layout leaves it. */ + font-size: clamp(1.25rem, 0.7rem + 3.3cqw, 2.6rem); + line-height: 1.15; font-weight: 700; display: flex; flex-direction: column; margin-bottom: 1.5rem; } +.hero__headline span { + text-wrap: balance; +} + .hero__headline-accent { color: hsl(var(--primary)); } +.hero__headline-icon { + display: inline-block; + white-space: nowrap; +} + .hero__lede { font-size: 1.125rem; color: hsl(var(--muted-foreground)); @@ -184,10 +264,6 @@ .hero__grid { grid-template-columns: 1fr; } - - .hero__headline { - font-size: 2.5rem; - } } /* Terminal card */ @@ -282,6 +358,28 @@ margin-bottom: 2rem; } +/* Split layout: head and body side by side in the same column ratio as the + hero's grid, so a section whose body is a single block of prose still + uses the section's full width instead of sitting in a narrow column with + empty space beside it. The prose itself still wraps at a readable width + (its own max-width, below) inside that column. */ +.section__grid { + display: grid; + gap: 3rem; + grid-template-columns: 1.1fr 0.9fr; + align-items: start; +} + +.section__grid .section__head { + margin-bottom: 0; +} + +@media (max-width: 860px) { + .section__grid { + grid-template-columns: 1fr; + } +} + .section__eyebrow { font-family: var(--font-mono); font-size: 0.875rem; @@ -297,6 +395,11 @@ line-height: 1.1; } +.section__heading-icon { + display: inline-block; + white-space: nowrap; +} + .section__lede { margin-top: 0.75rem; color: hsl(var(--muted-foreground)); @@ -371,6 +474,7 @@ .footer__links { list-style: none; display: flex; + flex-wrap: wrap; gap: 1.5rem; margin: 0; padding: 0; diff --git a/app/components/release-banner.tsx b/app/components/release-banner.tsx new file mode 100644 index 0000000..3be15fc --- /dev/null +++ b/app/components/release-banner.tsx @@ -0,0 +1,53 @@ +/* +Copyright The CryptOS Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import "./marketing.css"; + +interface ReleaseBannerCta { + label: string; + href: string; +} + +interface ReleaseBannerProps { + badge: string; + heading: string; + body: string; + cta?: ReleaseBannerCta; +} + +// Base-layer component: a pre-release status strip shown above the hero. +// Text stays in the foreground/muted-foreground pair against the card +// background (both exceed WCAG AA contrast in light and dark) -- the +// warning token is used only as a left accent border and the badge icon, +// never as a text/background text pair, because `--warning-foreground` on +// `--warning` falls short of AA for normal text in the light theme. +export const ReleaseBanner = ({ badge, heading, body, cta }: ReleaseBannerProps) => ( +
+
+ + +

+ {heading} {body}{" "} + {cta ? ( + + {cta.label} + + ) : null} +

+
+
+); diff --git a/app/components/section.tsx b/app/components/section.tsx index 267778a..9309c90 100644 --- a/app/components/section.tsx +++ b/app/components/section.tsx @@ -15,36 +15,61 @@ limitations under the License. */ import type { ReactNode } from "react"; +import { splitTrailingWord } from "~/lib/text"; + import "./marketing.css"; interface SectionProps { id?: string; eyebrow?: string; + icon?: string; heading: string; lede?: string; tone?: "default" | "card"; + // "stack": head above body, full container width (what-it-is's feature + // grid already fills that width on its own). "split": head and body sit + // side by side in the hero's own column ratio, for a section whose body + // is a single block of prose that would otherwise sit in a narrow column + // on the left with the rest of the wide container empty. + layout?: "stack" | "split"; children: ReactNode; } // Base-layer component: a content section with an optional eyebrow/heading, // consistent width and vertical rhythm. Mirrors the Astro proof's -// Section.astro. +// Section.astro. `icon` is a single decorative character (an emoji escape +// from the content file, never a literal emoji in this source) shown after +// the heading's last word, wrapped aria-hidden so screen readers only read +// the heading itself, and kept in a `white-space: nowrap` unit with that +// word so the two can never land on separate lines. export const Section = ({ id, eyebrow, + icon, heading, lede, tone = "default", + layout = "stack", children, -}: SectionProps) => ( -
-
-
- {eyebrow ?

{eyebrow}

: null} -

{heading}

- {lede ?

{lede}

: null} +}: SectionProps) => { + const [headingLead, headingTail] = splitTrailingWord(heading); + + return ( +
+
+
+ {eyebrow ?

{eyebrow}

: null} +

+ {headingLead} + + {headingTail} + {icon ? : null} + +

+ {lede ?

{lede}

: null} +
+
{children}
-
{children}
-
-
-); + + ); +}; diff --git a/app/components/split-hero.tsx b/app/components/split-hero.tsx index 464bf48..55f33c3 100644 --- a/app/components/split-hero.tsx +++ b/app/components/split-hero.tsx @@ -17,6 +17,7 @@ import { Button } from "@cryptos-pki/ui"; import type { ReactNode } from "react"; import type { LedeSegment } from "~/content/cryptos"; +import { splitTrailingWord } from "~/lib/text"; import "./marketing.css"; @@ -27,6 +28,7 @@ interface Cta { interface SplitHeroProps { eyebrow: string; + headlineIcon?: string; headlineLine1: string; headlineLine2: string; lede: LedeSegment[]; @@ -39,44 +41,60 @@ interface SplitHeroProps { // blurred radial glow, top-right. Same recipe as the Astro proof's // SplitHero.astro, so the two builds render identically; the CTA buttons are // the kit's own - +}: SplitHeroProps) => { + const [line2Lead, line2Tail] = splitTrailingWord(headlineLine2); + + return ( +
+ -
-); + + ); +}; diff --git a/app/content/cryptos.ts b/app/content/cryptos.ts index ccc7a8c..b4dd8ae 100644 --- a/app/content/cryptos.ts +++ b/app/content/cryptos.ts @@ -37,8 +37,19 @@ export const nav = { ], }; +export const releaseStatus = { + badge: "Pre-release", + heading: "First release coming soon.", + body: "v0.1.0-rc.1 is in final testing. There are no downloads yet -- check back soon.", + cta: { label: "Follow progress on GitHub", href: "https://github.com/CryptOS-PKI/website" }, +}; + export const hero = { eyebrow: "CryptOS-PKI", + // Decorative only (rendered aria-hidden next to the headline); written as a + // Unicode escape rather than the literal character, consistent with the + // no-emoji-in-source-files rule everywhere else in this repo. + headlineIcon: "\u{1F512}", // lock headlineLine1: "A PKI operating system", headlineLine2: "with no shell to break into.", lede: [ @@ -66,6 +77,7 @@ export const hero = { export const whatItIs = { eyebrow: "What it is", + icon: "\u{2693}", // anchor -- matches "trust anchor" in the heading heading: "An operating system built around one job: being a trust anchor.", lede: "No login prompt, no package manager, no general-purpose shell -- just the PKI API.", features: [ @@ -90,6 +102,7 @@ export const whatItIs = { export const trustModel = { eyebrow: "Trust model", + icon: "\u{1F511}", // key -- matches "your keys" in the heading heading: "Tested source, unsigned images, your keys.", lede: "We publish and test the source. You decide who signs what runs.", body: "CryptOS ships unsigned images built from source you can read and test yourself. Secure Boot enrollment, image signing and the keys behind them stay with you: nothing in the chain of trust for your fleet is signed by us.", @@ -97,6 +110,7 @@ export const trustModel = { export const fleetosManager = { eyebrow: "FleetOS Manager", + icon: "\u{1F6F0}\u{FE0F}", // satellite -- fleet of linked nodes heading: "One view across every linked node.", lede: "An optional web console for day-to-day operations -- the nodes keep working without it.", body: "Link a node to the FleetOS Manager for topology, certificate inventory and ceremony history across your fleet. The manager never holds issuance authority -- its own certificate can't sign.", diff --git a/app/lib/text.ts b/app/lib/text.ts new file mode 100644 index 0000000..ece198e --- /dev/null +++ b/app/lib/text.ts @@ -0,0 +1,24 @@ +/* +Copyright The CryptOS Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Splits the trailing word off a line of text so a caller can pair it with a +// trailing decorative mark (an emoji) inside a `white-space: nowrap` span, +// keeping the two from ever landing on separate lines. +export function splitTrailingWord(text: string): [lead: string, tail: string] { + const trimmed = text.trimEnd(); + const lastSpace = trimmed.lastIndexOf(" "); + if (lastSpace === -1) return ["", trimmed]; + return [trimmed.slice(0, lastSpace + 1), trimmed.slice(lastSpace + 1)]; +} diff --git a/app/routes/home.tsx b/app/routes/home.tsx index a80100c..d08e4a2 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -19,6 +19,7 @@ import type { MetaFunction } from "react-router"; import { FeatureGrid } from "~/components/feature-grid"; import { Footer } from "~/components/footer"; import { Nav } from "~/components/nav"; +import { ReleaseBanner } from "~/components/release-banner"; import { Section } from "~/components/section"; import { SplitHero } from "~/components/split-hero"; import { TerminalCard } from "~/components/terminal-card"; @@ -28,6 +29,7 @@ import { hero, meta as pageMeta, nav, + releaseStatus, trustModel, whatItIs, } from "~/content/cryptos"; @@ -44,9 +46,16 @@ export default function Home() { Skip to content