fix(website): clear WCAG AA across both themes, and stop the hero shifting
Contrast was measured, not eyeballed: 41 foreground/background pairs across both themes and the terminal surface, each against the darkest (light) or lightest (dark) surface it actually sits on. Five failed. Links on manila read 3.64:1 and the footer's "No telemetry. Ever." — a claim the site goes out of its way to make — sat at 2.86:1. The accent needed splitting rather than moving. #c1502e clears 4.5:1 as a button fill behind white but only 3.99:1 as an 11px eyebrow on vinyl, so --accent keeps the brand hue for fills and a new --accent-text carries the readable cut. The terminal gets the same treatment dark emulators give their palettes: alarm and oxide brighten inside .tui-content so "Failed" is legible next to its glyph. Reduced motion no longer nukes everything. A blanket 0.01ms also kills the transitions that answer a click, which is feedback rather than movement; looping and travelling animation stops, the rest caps at 80ms. The TUI already skipped its opening run and still does. The hero reserves its box with aspect-ratio from the first paint. It was prerendered at full unscaled height and collapsed once JS measured, moving everything below it by ~145px. Measured shift is now 0. Also: the webfont loads non-blocking (display=swap meant the first frame never waited for it anyway), footer link groups nest under a heading instead of sitting level with page sections, and eight tokens nothing referenced stop shipping.
This commit is contained in:
@@ -13,9 +13,10 @@ const ASPECT = 186.407 / 211
|
||||
* Two assets rather than one recoloured file — the ink and light cuts differ
|
||||
* in more than colour — swapped by CSS.
|
||||
*
|
||||
* Background images, not <img>: a hidden <img> is still downloaded, so the
|
||||
* two-element version fetched both cuts on every visit (~12-23 kB gzip wasted
|
||||
* above the fold). A background on a non-painted rule is never requested.
|
||||
* Painted as CSS background images rather than image elements: a hidden one
|
||||
* is still downloaded, so the two-element version fetched both cuts on every
|
||||
* visit (~12-23 kB gzip wasted above the fold). A background on a rule that
|
||||
* never paints is never requested.
|
||||
*/
|
||||
export const Logo = ({ size = 180, className }: LogoProps) => (
|
||||
<div
|
||||
|
||||
@@ -61,6 +61,10 @@ const FooterAnchor = ({ label, href }: FooterLink) =>
|
||||
export const Footer = () => (
|
||||
<footer className="border-t border-hairline bg-base pt-7 pb-6 sm:pt-11 sm:pb-10">
|
||||
<Wrap>
|
||||
{/* Gives the three link groups a heading to sit under, so the document
|
||||
outline stays nested instead of putting "Packages" on the same
|
||||
level as the page's own sections. */}
|
||||
<h2 className="sr-only">Site footer</h2>
|
||||
{/* Two columns even on the narrowest phone, with the brand spanning
|
||||
both — one column per link group makes the footer taller than the
|
||||
page it closes. */}
|
||||
@@ -77,9 +81,9 @@ export const Footer = () => (
|
||||
|
||||
{COLUMNS.map((column) => (
|
||||
<div key={column.heading}>
|
||||
<h2 className="mb-2 text-label uppercase tracking-[var(--tracking-label)] text-secondary sm:mb-3.5">
|
||||
<h3 className="mb-2 text-label uppercase tracking-[var(--tracking-label)] text-secondary sm:mb-3.5">
|
||||
{column.heading}
|
||||
</h2>
|
||||
</h3>
|
||||
<ul>
|
||||
{column.links.map((link) => (
|
||||
<li key={link.label} className="mb-1.5 sm:mb-2.5">
|
||||
|
||||
@@ -37,7 +37,7 @@ export const Nav = () => (
|
||||
cn(
|
||||
'relative inline-flex shrink-0 items-center rounded-md px-3.5 py-1.5 text-label uppercase tracking-[var(--tracking-label)] max-sm:min-h-11 sm:px-3',
|
||||
isActive
|
||||
? "bg-raised text-primary before:text-accent before:content-['›_'] after:absolute after:inset-x-3 after:bottom-px after:h-0.5 after:bg-accent"
|
||||
? "bg-raised text-primary before:text-accent-text before:content-['›_'] after:absolute after:inset-x-3 after:bottom-px after:h-0.5 after:bg-accent"
|
||||
: 'text-secondary hover:bg-[var(--bg-selection)] hover:text-primary',
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,15 @@ import { InfoPane, ResultsPane, SearchesPane, TransfersPane } from './panes'
|
||||
import { useFitScale } from './use-fit-scale'
|
||||
|
||||
const DESIGN_WIDTH = 1360
|
||||
/*
|
||||
* Natural height of the terminal at DESIGN_WIDTH, measured in the browser.
|
||||
*
|
||||
* The layout is fixed — no content-driven heights — so the wrapper can hold
|
||||
* this ratio in CSS from the very first paint. Without it the prerendered
|
||||
* hero occupied its full unscaled height, then collapsed to the scaled one
|
||||
* once JS measured, shifting everything below it by ~145px.
|
||||
*/
|
||||
const DESIGN_HEIGHT = 863
|
||||
const TICK_MS = 480
|
||||
const MAX_CONCURRENT = 2
|
||||
/** Where the scripted run leaves the cursor. */
|
||||
@@ -374,8 +383,10 @@ export const HeroTerminal = () => {
|
||||
return (
|
||||
<div
|
||||
ref={wrapRef}
|
||||
className="w-full overflow-x-auto overflow-y-hidden"
|
||||
style={ready ? { height } : undefined}
|
||||
className="w-full overflow-hidden"
|
||||
// aspect-ratio reserves the right box before JS runs; once measured the
|
||||
// explicit height takes over and the two agree, so nothing moves.
|
||||
style={ready ? { height } : { aspectRatio: `${String(DESIGN_WIDTH)} / ${String(DESIGN_HEIGHT)}` }}
|
||||
>
|
||||
<div style={ready ? { width: DESIGN_WIDTH * scale, height } : undefined}>
|
||||
<div
|
||||
|
||||
@@ -22,7 +22,7 @@ export const CommandPill = ({ text, className }: CommandPillProps) => {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span aria-hidden="true" className="text-accent">
|
||||
<span aria-hidden="true" className="text-accent-text">
|
||||
$
|
||||
</span>
|
||||
<code className="font-mono">{text}</code>
|
||||
|
||||
@@ -45,7 +45,7 @@ interface EyebrowProps {
|
||||
export const Eyebrow = ({ children, className }: EyebrowProps) => (
|
||||
<span
|
||||
className={cn(
|
||||
'text-label uppercase tracking-[var(--tracking-label)] text-accent',
|
||||
'text-label uppercase tracking-[var(--tracking-label)] text-accent-text',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -27,7 +27,7 @@ export const OsIcon = ({ name, className }: OsIconProps) => (
|
||||
role="img"
|
||||
aria-label={name}
|
||||
className={cn(
|
||||
'block size-[22px] shrink-0 text-primary transition-colors group-hover:text-accent',
|
||||
'block size-[22px] shrink-0 text-primary transition-colors group-hover:text-accent-text',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -51,7 +51,7 @@ export const Callout = ({ title, tone = 'accent', children, className }: Callout
|
||||
<div
|
||||
className={cn(
|
||||
'mb-2 text-label uppercase tracking-[var(--tracking-label)]',
|
||||
tone === 'warn' ? 'text-warning' : 'text-accent',
|
||||
tone === 'warn' ? 'text-warning' : 'text-accent-text',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
|
||||
Reference in New Issue
Block a user