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
116 changes: 110 additions & 6 deletions app/components/marketing.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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));
Expand All @@ -184,10 +264,6 @@
.hero__grid {
grid-template-columns: 1fr;
}

.hero__headline {
font-size: 2.5rem;
}
}

/* Terminal card */
Expand Down Expand Up @@ -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;
Expand All @@ -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));
Expand Down Expand Up @@ -371,6 +474,7 @@
.footer__links {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
margin: 0;
padding: 0;
Expand Down
53 changes: 53 additions & 0 deletions app/components/release-banner.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<div className="release-banner" role="note" aria-label="Release status">
<div className="container release-banner__row">
<span className="release-banner__badge">
<span className="release-banner__dot" aria-hidden="true" />
{badge}
</span>
<p className="release-banner__text">
<strong>{heading}</strong> {body}{" "}
{cta ? (
<a className="release-banner__link" href={cta.href}>
{cta.label}
</a>
) : null}
</p>
</div>
</div>
);
49 changes: 37 additions & 12 deletions app/components/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => (
<section id={id} className={tone === "card" ? "section section--card" : "section"}>
<div className="container">
<div className="section__head">
{eyebrow ? <p className="section__eyebrow">{eyebrow}</p> : null}
<h2 className="section__heading">{heading}</h2>
{lede ? <p className="section__lede">{lede}</p> : null}
}: SectionProps) => {
const [headingLead, headingTail] = splitTrailingWord(heading);

return (
<section id={id} className={tone === "card" ? "section section--card" : "section"}>
<div className={layout === "split" ? "container section__grid" : "container"}>
<div className="section__head">
{eyebrow ? <p className="section__eyebrow">{eyebrow}</p> : null}
<h2 className="section__heading">
{headingLead}
<span className="section__heading-icon">
{headingTail}
{icon ? <span aria-hidden="true"> {icon}</span> : null}
</span>
</h2>
{lede ? <p className="section__lede">{lede}</p> : null}
</div>
<div className="section__body">{children}</div>
</div>
<div className="section__body">{children}</div>
</div>
</section>
);
</section>
);
};
Loading
Loading