ELF-457: add slot 3 rotating tagline above hero sub
Some checks are pending
CI / go (push) Waiting to run
CI / docker (push) Waiting to run
CI / lint-go (push) Waiting to run
CI / lint-docs (push) Waiting to run
CI / check-paperclip (push) Waiting to run

Three taglines rotate every 3.5s with a subtle 700ms opacity crossfade:
  1. Inspect any request. Mock any response. (default)
  2. Your HTTP bin, on your machine.
  3. Send. Capture. Mock. No signup.

Implementation notes:
- Client component, default tagline in initial SSR HTML for SEO/no-JS
- ARIA: aria-live=polite on container, aria-hidden on inactive items,
  aria-atomic=true; prefers-reduced-motion suppresses both the timer
  and the fade (via motion-reduce:transition-none + early-return guard)
- No CLS: container has fixed h-7 md:h-8 height; absolute children
  crossfade in place
- Hierarchy: tagline below H1 (mb-6), above hero sub (mb-6 from tagline
  container, mb-10 from sub to CTAs)

Tailwind v4 fixes that were needed to make this visible:
- app/globals.css: switch to @import 'tailwindcss' with explicit
  @source globs (auto-detection misses components when Turbopack
  workspace root is ambiguous)
- app/globals.css: drop redundant  reset;
  Tailwind v4 Preflight handles it, and an unlayered  selector
  beats @layer utilities so it was silently killing mb-*, mx-*, etc.
- app/globals.css: wrap  in @layer base
  so text-white / text-zinc-* utilities can override it
- next.config.ts: pin turbopack.root to project dir so the build
  works regardless of shell cwd (was running into the paperclip
  workspace root during agent sessions)
This commit is contained in:
Senior Web Designer 2026-06-26 19:30:57 +02:00
parent 475fd95a59
commit bcb4f1688e
3 changed files with 83 additions and 17 deletions

View File

@ -1,6 +1,10 @@
@tailwind base; @import "tailwindcss";
@tailwind components;
@tailwind utilities; /* Explicit source roots for Tailwind v4. Auto-detection misses files when
Turbopack's workspace root is ambiguous (e.g. shell cwd differs from the
web project root), so we pin the scan paths here. */
@source "../components/**/*.{ts,tsx}";
@source "../app/**/*.{ts,tsx}";
:root { :root {
--surface-0: #09090b; --surface-0: #09090b;
@ -17,11 +21,11 @@
--code-border: #27272a; --code-border: #27272a;
} }
* { /* Note: no `* { margin: 0; padding: 0; box-sizing: border-box; }` here.
margin: 0; Tailwind v4's Preflight (included by `@import "tailwindcss"`) already
padding: 0; resets margins on block elements and sets border-box globally. An
box-sizing: border-box; unlayered `*` selector would beat Tailwind's `@layer utilities` and
} silently break every margin utility (mb-*, mt-*, mx-*, etc.). */
html { html {
font-size: 16px; font-size: 16px;
@ -37,14 +41,19 @@ body {
overflow-x: hidden; overflow-x: hidden;
} }
a { /* Default link color. Wrapped in @layer base so Tailwind utilities
color: var(--brand); (text-white, text-zinc-*, etc.) in @layer utilities still win. An
text-decoration: none; unlayered `a { color: ... }` would silently override utilities. */
transition: color 150ms cubic-bezier(0.16, 1, 0.3, 1); @layer base {
} a {
color: var(--brand);
text-decoration: none;
transition: color 150ms cubic-bezier(0.16, 1, 0.3, 1);
}
a:hover { a:hover {
color: var(--text-primary); color: var(--text-primary);
}
} }
code { code {

View File

@ -1,16 +1,65 @@
"use client";
import { useEffect, useState } from "react";
// Rotating taglines — Slot 3 above the hero sub (ELF-457, brief ELF-456).
// Order matters: index 0 is the default and the initial server-rendered string.
const TAGLINES = [
"Inspect any request. Mock any response.",
"Your HTTP bin, on your machine.",
"Send. Capture. Mock. No signup.",
] as const;
// Per-tagline dwell. 3.5s gives ~2.8s solid visibility after the 700ms crossfade.
const ROTATE_MS = 3500;
export default function Hero() { export default function Hero() {
const [index, setIndex] = useState(0);
useEffect(() => {
// Respect prefers-reduced-motion at the JS layer too: don't start a timer
// for users who opt out. The fade is independently suppressed via the
// motion-reduce: variant on the <p> class below.
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const id = window.setInterval(() => {
setIndex((i) => (i + 1) % TAGLINES.length);
}, ROTATE_MS);
return () => window.clearInterval(id);
}, []);
return ( return (
<section className="pt-32 pb-20 px-6"> <section className="pt-32 pb-20 px-6">
<div className="max-w-4xl mx-auto text-center"> <div className="max-w-4xl mx-auto text-center">
<div className="inline-block mb-6 px-5 py-2 rounded-full bg-blue-500/10 border border-blue-500/30 text-blue-400 text-sm font-medium"> <div className="inline-block mb-6 px-5 py-2 rounded-full bg-blue-500/10 border border-blue-500/30 text-blue-400 text-sm font-medium">
Open source · MIT licensed Open source · MIT licensed
</div> </div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold tracking-tight mb-6 leading-tight bg-gradient-to-r from-white to-zinc-400 bg-clip-text text-transparent"> <h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold tracking-tight mb-6 leading-tight bg-gradient-to-r from-white to-zinc-400 bg-clip-text text-transparent">
Self-hostable HTTP request<br /> Self-hostable HTTP request<br />
inspector&nbsp;+&nbsp;mocker inspector&nbsp;+&nbsp;mocker
</h1> </h1>
{/* Slot 3: rotating tagline. Fixed-height container prevents CLS; subtle
opacity crossfade so it doesn't compete with the terminal CTA below.
a11y: aria-live polite + aria-hidden on inactive items. */}
<div
className="relative h-7 md:h-8 mb-6 mx-auto max-w-2xl"
aria-live="polite"
aria-atomic="true"
>
{TAGLINES.map((t, i) => (
<p
key={t}
aria-hidden={i !== index}
className={`absolute inset-0 font-mono text-sm md:text-lg font-semibold text-zinc-300 leading-7 md:leading-8 transition-opacity duration-700 ease-out motion-reduce:transition-none ${
i === index ? "opacity-100" : "opacity-0"
}`}
>
{t}
</p>
))}
</div>
<p className="text-lg md:text-xl text-zinc-400 mb-10 max-w-2xl mx-auto leading-relaxed"> <p className="text-lg md:text-xl text-zinc-400 mb-10 max-w-2xl mx-auto leading-relaxed">
One Go binary. SQLite under the hood. One Go binary. SQLite under the hood.
<code className="mx-1 px-2 py-0.5 bg-zinc-800 border border-zinc-700 rounded text-fuchsia-400 text-base"> <code className="mx-1 px-2 py-0.5 bg-zinc-800 border border-zinc-700 rounded text-fuchsia-400 text-base">

View File

@ -1,9 +1,17 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import path from "node:path";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
images: { images: {
unoptimized: true, unoptimized: true,
}, },
// Pin the Turbopack workspace root to this directory. Without this, Turbopack
// infers it from the shell's cwd, which during Paperclip agent runs is the
// paperclip project root — that misroutes CSS/source scanning and silently
// strips Tailwind utility classes from the production bundle.
turbopack: {
root: path.resolve(import.meta.dirname),
},
}; };
export default nextConfig; export default nextConfig;