design: apply silver gradient to section headers + nav links, add dev-only live accent switcher, slow border-beam to 11s

This commit is contained in:
2026-06-06 21:10:42 -07:00
parent 2714f8cdf3
commit eaea0dd027
4 changed files with 392 additions and 22 deletions

View File

@@ -0,0 +1,237 @@
"use client";
import { useEffect, useState } from "react";
/**
* Dev-only accent color switcher for the marketing site.
*
* Flips the homepage accent live by overriding the `--accent-h` (hue) and
* `--accent-cm` (chroma) CSS variables on `.new-site-wrapper`. Choice persists
* in localStorage so it survives reloads while you experiment.
*
* Visibility: shows only in development, or in any environment when the URL has
* `?theme`. So it's hidden in production by default — nothing to do to hide it.
* To remove entirely later, delete this file and its <AccentSwitcher /> usage
* in app/(marketing)/page.tsx.
*/
const PRESETS: { label: string; h: number; cm: number }[] = [
{ label: "Coral", h: 35, cm: 1 },
{ label: "Silver", h: 35, cm: 0 },
{ label: "Crimson", h: 25, cm: 1 },
{ label: "Amber", h: 75, cm: 1 },
{ label: "Emerald", h: 150, cm: 1 },
{ label: "Teal", h: 195, cm: 1 },
{ label: "Sky", h: 235, cm: 1 },
{ label: "Royal", h: 255, cm: 1 },
{ label: "Indigo", h: 270, cm: 1 },
{ label: "Violet", h: 300, cm: 1 },
{ label: "Pink", h: 350, cm: 1 },
];
const swatchColor = (h: number, cm: number) =>
`oklch(0.74 ${(0.175 * cm).toFixed(3)} ${h})`;
export default function AccentSwitcher() {
const [visible, setVisible] = useState(false);
const [open, setOpen] = useState(true);
const [h, setH] = useState(35);
const [cm, setCm] = useState(1);
useEffect(() => {
const isDev = process.env.NODE_ENV !== "production";
const forced =
typeof window !== "undefined" &&
new URLSearchParams(window.location.search).has("theme");
setVisible(isDev || forced);
try {
const saved = JSON.parse(localStorage.getItem("vibn:accent") || "null");
if (saved && typeof saved.h === "number") {
setH(saved.h);
setCm(typeof saved.cm === "number" ? saved.cm : 1);
}
} catch {
/* ignore */
}
}, []);
useEffect(() => {
if (!visible) return;
const el = document.querySelector(".new-site-wrapper") as HTMLElement | null;
if (el) {
el.style.setProperty("--accent-h", String(h));
el.style.setProperty("--accent-cm", String(cm));
}
try {
localStorage.setItem("vibn:accent", JSON.stringify({ h, cm }));
} catch {
/* ignore */
}
}, [visible, h, cm]);
if (!visible) return null;
const panel: React.CSSProperties = {
position: "fixed",
left: 16,
bottom: 16,
zIndex: 2147483647,
width: 232,
padding: 14,
borderRadius: 14,
background: "rgba(18,17,15,0.92)",
border: "1px solid rgba(255,255,255,0.12)",
backdropFilter: "blur(16px)",
boxShadow: "0 18px 50px rgba(0,0,0,0.5)",
color: "#f5f4f2",
fontFamily: "ui-monospace, 'SF Mono', Menlo, monospace",
fontSize: 11,
userSelect: "none",
};
if (!open) {
return (
<button
type="button"
onClick={() => setOpen(true)}
title="Theme"
style={{
position: "fixed",
left: 16,
bottom: 16,
zIndex: 2147483647,
width: 38,
height: 38,
borderRadius: "50%",
border: "1px solid rgba(255,255,255,0.18)",
background: swatchColor(h, cm),
boxShadow: "0 8px 24px rgba(0,0,0,0.45)",
cursor: "pointer",
}}
/>
);
}
return (
<div style={panel}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 10,
}}
>
<span style={{ letterSpacing: "0.1em", textTransform: "uppercase", opacity: 0.6 }}>
Accent · dev
</span>
<button
type="button"
onClick={() => setOpen(false)}
title="Collapse"
style={{
border: "none",
background: "transparent",
color: "#f5f4f2",
cursor: "pointer",
fontSize: 14,
lineHeight: 1,
opacity: 0.7,
}}
>
</button>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(6, 1fr)",
gap: 6,
marginBottom: 12,
}}
>
{PRESETS.map((p) => {
const active = p.h === h && p.cm === cm;
return (
<button
key={p.label}
type="button"
title={p.label}
onClick={() => {
setH(p.h);
setCm(p.cm);
}}
style={{
width: "100%",
aspectRatio: "1 / 1",
borderRadius: 7,
border: active
? "2px solid #fff"
: "1px solid rgba(255,255,255,0.15)",
background: swatchColor(p.h, p.cm),
cursor: "pointer",
padding: 0,
}}
/>
);
})}
</div>
<label style={{ display: "block", marginBottom: 8 }}>
<span style={{ display: "flex", justifyContent: "space-between", opacity: 0.7 }}>
<span>Hue</span>
<span>{h}</span>
</span>
<input
type="range"
min={0}
max={360}
value={h}
onChange={(e) => setH(Number(e.target.value))}
style={{ width: "100%", accentColor: swatchColor(h, cm) }}
/>
</label>
<label style={{ display: "block", marginBottom: 10 }}>
<span style={{ display: "flex", justifyContent: "space-between", opacity: 0.7 }}>
<span>Vivid</span>
<span>{cm.toFixed(2)}</span>
</span>
<input
type="range"
min={0}
max={1}
step={0.05}
value={cm}
onChange={(e) => setCm(Number(e.target.value))}
style={{ width: "100%", accentColor: swatchColor(h, cm) }}
/>
</label>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<code style={{ opacity: 0.55, fontSize: 10 }}>
h:{h} cm:{cm}
</code>
<button
type="button"
onClick={() => {
setH(35);
setCm(1);
}}
style={{
border: "1px solid rgba(255,255,255,0.18)",
background: "transparent",
color: "#f5f4f2",
borderRadius: 6,
padding: "4px 8px",
cursor: "pointer",
fontSize: 10,
}}
>
Reset
</button>
</div>
</div>
);
}

View File

@@ -915,10 +915,23 @@ function Hero({ onStart, variant = "quote" }) {
line-height: 0.98; line-height: 0.98;
text-wrap: balance; text-wrap: balance;
position: relative; position: relative;
color: var(--fg); /* Silver / brushed-metal heading: bright at the top, fading to a soft
pewter at the bottom, clipped to the text. */
background-image: linear-gradient(
180deg,
oklch(0.99 0.003 80) 0%,
oklch(0.86 0.004 80) 42%,
oklch(0.66 0.006 80) 100%
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
} }
.hero-quote .mark { .hero-quote .mark {
/* keep the accent word in full color inside the silver heading */
color: var(--accent); color: var(--accent);
-webkit-text-fill-color: var(--accent);
font-family: "Geist", serif; font-family: "Geist", serif;
font-weight: 500; font-weight: 500;
line-height: 0; line-height: 0;
@@ -963,13 +976,62 @@ function Hero({ onStart, variant = "quote" }) {
border-radius: var(--r-xl); border-radius: var(--r-xl);
padding: 1px; padding: 1px;
background: linear-gradient(180deg, background: linear-gradient(180deg,
oklch(0.50 0.06 35 / 0.6), oklch(0.50 calc(0.06 * var(--accent-cm)) var(--accent-h) / 0.6),
oklch(0.30 0.012 60 / 0.4) 40%, oklch(0.30 0.012 60 / 0.4) 40%,
oklch(0.25 0.012 60 / 0.4)); oklch(0.25 0.012 60 / 0.4));
box-shadow: box-shadow:
0 30px 80px -20px oklch(0 0 0 / 0.6), 0 30px 80px -20px oklch(0 0 0 / 0.6),
0 0 80px -20px var(--accent-glow); 0 0 80px -20px var(--accent-glow);
} }
/* Animated border beam (Magic UI style): a light comet that travels
the border, masked to a thin ring and themed to the accent. */
.prompt-beam {
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1.5px;
pointer-events: none;
z-index: 2;
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
overflow: hidden;
}
.prompt-beam::before {
content: "";
position: absolute;
width: 170px;
aspect-ratio: 1;
offset-path: rect(0 auto auto 0 round var(--r-xl));
offset-distance: 0%;
offset-anchor: 50% 50%;
background: linear-gradient(
90deg,
transparent,
var(--accent) 38%,
oklch(0.95 calc(0.05 * var(--accent-cm)) var(--accent-h)) 50%,
var(--accent) 62%,
transparent
);
filter: blur(3px);
animation: prompt-beam 11s linear infinite;
}
@keyframes prompt-beam {
to {
offset-distance: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
.prompt-beam::before {
animation: none;
opacity: 0;
}
}
.prompt-inner { .prompt-inner {
background: linear-gradient(180deg, oklch(0.19 0.009 60 / 0.92), oklch(0.17 0.008 60 / 0.92)); background: linear-gradient(180deg, oklch(0.19 0.009 60 / 0.92), oklch(0.17 0.008 60 / 0.92));
border-radius: calc(var(--r-xl) - 1px); border-radius: calc(var(--r-xl) - 1px);
@@ -1037,7 +1099,7 @@ function Hero({ onStart, variant = "quote" }) {
background: var(--accent); background: var(--accent);
color: var(--accent-fg); color: var(--accent-fg);
font-weight: 500; font-size: 14px; font-weight: 500; font-size: 14px;
box-shadow: 0 0 0 1px oklch(0.84 0.16 35 / 0.5) inset, 0 8px 28px -8px var(--accent-glow); box-shadow: 0 0 0 1px oklch(0.84 calc(0.16 * var(--accent-cm)) var(--accent-h) / 0.5) inset, 0 8px 28px -8px var(--accent-glow);
transition: transform .12s; transition: transform .12s;
} }
.prompt-send:hover { transform: translateY(-1px); } .prompt-send:hover { transform: translateY(-1px); }
@@ -1095,17 +1157,17 @@ function Hero({ onStart, variant = "quote" }) {
{/* ambient glows behind hero */} {/* ambient glows behind hero */}
<Glow <Glow
color="oklch(0.74 0.175 35 / 0.30)" color="oklch(0.74 calc(0.175 * var(--accent-cm)) var(--accent-h) / 0.30)"
size={900} size={900}
style={{ top: "-200px", left: "50%", transform: "translateX(-50%)" }} style={{ top: "-200px", left: "50%", transform: "translateX(-50%)" }}
/> />
<Glow <Glow
color="oklch(0.45 0.10 35 / 0.30)" color="oklch(0.45 calc(0.10 * var(--accent-cm)) var(--accent-h) / 0.30)"
size={600} size={600}
style={{ top: "20%", left: "-200px" }} style={{ top: "20%", left: "-200px" }}
/> />
<Glow <Glow
color="oklch(0.45 0.10 35 / 0.20)" color="oklch(0.45 calc(0.10 * var(--accent-cm)) var(--accent-h) / 0.20)"
size={500} size={500}
style={{ top: "30%", right: "-150px" }} style={{ top: "30%", right: "-150px" }}
/> />
@@ -1156,6 +1218,7 @@ function Hero({ onStart, variant = "quote" }) {
{/* Prompt */} {/* Prompt */}
<div className="prompt"> <div className="prompt">
<div className="prompt-frame"> <div className="prompt-frame">
<span className="prompt-beam" aria-hidden="true" />
<div className="prompt-inner"> <div className="prompt-inner">
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
<textarea <textarea
@@ -1860,7 +1923,7 @@ function Journey() {
background: var(--bg); background: var(--bg);
padding: 6px 12px; padding: 6px 12px;
border-radius: 999px; border-radius: 999px;
border: 1px solid oklch(0.74 0.175 35 / 0.5); border: 1px solid oklch(0.74 calc(0.175 * var(--accent-cm)) var(--accent-h) / 0.5);
white-space: nowrap; white-space: nowrap;
box-shadow: 0 0 24px var(--accent-glow); box-shadow: 0 0 24px var(--accent-glow);
transform: translateY(-1px); transform: translateY(-1px);
@@ -1878,7 +1941,7 @@ function Journey() {
margin-top: 1px; margin-top: 1px;
} }
.demo-tag.you { color: oklch(0.85 0.06 250); background: oklch(0.28 0.04 250); } .demo-tag.you { color: oklch(0.85 0.06 250); background: oklch(0.28 0.04 250); }
.demo-tag.ai { color: var(--accent); background: oklch(0.35 0.10 35 / 0.4); } .demo-tag.ai { color: var(--accent); background: oklch(0.35 calc(0.10 * var(--accent-cm)) var(--accent-h) / 0.4); }
.demo-line { color: var(--fg-dim); } .demo-line { color: var(--fg-dim); }
.demo-ok { color: var(--ok); } .demo-ok { color: var(--ok); }
.demo-host { .demo-host {
@@ -2200,8 +2263,8 @@ function Audience() {
text-transform: uppercase; text-transform: uppercase;
color: var(--accent); color: var(--accent);
padding: 3px 7px; padding: 3px 7px;
background: oklch(0.74 0.175 35 / 0.12); background: oklch(0.74 calc(0.175 * var(--accent-cm)) var(--accent-h) / 0.12);
border: 1px solid oklch(0.74 0.175 35 / 0.4); border: 1px solid oklch(0.74 calc(0.175 * var(--accent-cm)) var(--accent-h) / 0.4);
border-radius: 4px; border-radius: 4px;
margin-top: 1px; margin-top: 1px;
flex-shrink: 0; flex-shrink: 0;
@@ -2306,7 +2369,7 @@ function Closing() {
.closing-title .accent { color: var(--accent); } .closing-title .accent { color: var(--accent); }
.closing-title em { .closing-title em {
font-style: normal; font-style: normal;
background: linear-gradient(180deg, var(--accent), oklch(0.62 0.18 18)); background: linear-gradient(180deg, var(--accent), oklch(0.62 calc(0.18 * var(--accent-cm)) calc(var(--accent-h) - 17)));
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
@@ -2330,12 +2393,12 @@ function Closing() {
`}</style> `}</style>
<Glow <Glow
color="oklch(0.74 0.175 35 / 0.35)" color="oklch(0.74 calc(0.175 * var(--accent-cm)) var(--accent-h) / 0.35)"
size={1000} size={1000}
style={{ top: "20%", left: "50%", transform: "translateX(-50%)" }} style={{ top: "20%", left: "50%", transform: "translateX(-50%)" }}
/> />
<Glow <Glow
color="oklch(0.45 0.10 35 / 0.20)" color="oklch(0.45 calc(0.10 * var(--accent-cm)) var(--accent-h) / 0.20)"
size={600} size={600}
style={{ bottom: "-200px", left: "50%", transform: "translateX(-50%)" }} style={{ bottom: "-200px", left: "50%", transform: "translateX(-50%)" }}
/> />

View File

@@ -1,4 +1,7 @@
import NewSite from "./new-site"; import NewSite from "./new-site";
// Dev-only accent color switcher (auto-hidden in production). Remove these two
// lines to take it out entirely.
import AccentSwitcher from "./AccentSwitcher";
import "../styles/new-site.css"; import "../styles/new-site.css";
export const metadata = { export const metadata = {
@@ -6,5 +9,10 @@ export const metadata = {
}; };
export default function Page() { export default function Page() {
return <NewSite />; return (
<>
<NewSite />
<AccentSwitcher />
</>
);
} }

View File

@@ -9,10 +9,23 @@
--fg-mute: oklch(0.58 0.006 80); --fg-mute: oklch(0.58 0.006 80);
--fg-faint: oklch(0.42 0.006 80); --fg-faint: oklch(0.42 0.006 80);
--accent: oklch(0.74 0.175 35); /* warm coral, default */ /* ── Accent color — change these two to retheme the whole site ──────────
--accent-soft: oklch(0.74 0.175 35 / 0.18); --accent-h : hue 0-360. coral 35 · red 25 · amber 75 · green 150
--accent-glow: oklch(0.74 0.175 35 / 0.35); · teal 195 · blue 250 · indigo 270 · violet 300 · pink 350
--accent-fg: oklch(0.18 0.04 35); --accent-cm : chroma multiplier. 1 = vivid · 0 = silver / neutral gray
Every accent color on the page derives from these, so the design stays
identical and only the hue/saturation moves. */
--accent-h: 35;
--accent-cm: 1;
--accent: oklch(0.74 calc(0.175 * var(--accent-cm)) var(--accent-h));
--accent-soft: oklch(
0.74 calc(0.175 * var(--accent-cm)) var(--accent-h) / 0.18
);
--accent-glow: oklch(
0.74 calc(0.175 * var(--accent-cm)) var(--accent-h) / 0.35
);
--accent-fg: oklch(0.18 calc(0.04 * var(--accent-cm)) var(--accent-h));
--ok: oklch(0.78 0.16 155); --ok: oklch(0.78 0.16 155);
@@ -100,6 +113,34 @@ h4 {
letter-spacing: -0.02em; letter-spacing: -0.02em;
line-height: 1.05; line-height: 1.05;
} }
/* Shared Silver Metallic Gradient */
.silver-text,
.wall-title,
.crossed-title,
.journey-title,
.closing-title,
.mission-quote {
background-image: linear-gradient(
180deg,
oklch(0.99 0.003 80) 0%,
oklch(0.86 0.004 80) 42%,
oklch(0.66 0.006 80) 100%
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
}
/* Ensure highlights/em elements stay full-color inside silver headers */
.wall-title em,
.crossed-title em,
.journey-title .accent,
.closing-title .accent,
.mission-quote .highlight {
color: var(--accent);
-webkit-text-fill-color: var(--accent);
}
p { p {
margin: 0; margin: 0;
} }
@@ -172,7 +213,9 @@ button {
background: var(--accent); background: var(--accent);
color: var(--accent-fg); color: var(--accent-fg);
box-shadow: box-shadow:
0 0 0 1px oklch(0.84 0.16 35 / 0.5) inset, 0 0 0 1px
oklch(0.84 calc(0.16 * var(--accent-cm)) var(--accent-h) / 0.5)
inset,
0 10px 40px -10px var(--accent-glow), 0 10px 40px -10px var(--accent-glow),
0 0 50px -8px var(--accent-glow); 0 0 50px -8px var(--accent-glow);
} }
@@ -269,7 +312,7 @@ button {
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
var(--accent) 0%, var(--accent) 0%,
oklch(0.65 0.2 18) 100% oklch(0.65 calc(0.2 * var(--accent-cm)) calc(var(--accent-h) - 17)) 100%
); );
box-shadow: box-shadow:
0 0 22px var(--accent-glow), 0 0 22px var(--accent-glow),
@@ -293,11 +336,30 @@ button {
.nav-links { .nav-links {
display: flex; display: flex;
gap: 28px; gap: 28px;
color: var(--fg-mute);
font-size: 14px; font-size: 14px;
} }
.nav-links a {
background-image: linear-gradient(
180deg,
oklch(0.92 0.003 80) 0%,
oklch(0.74 0.004 80) 100%
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
font-weight: 500;
}
.nav-links a:hover { .nav-links a:hover {
color: var(--fg); background-image: linear-gradient(
180deg,
oklch(0.99 0.003 80) 0%,
oklch(0.88 0.004 80) 100%
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
} }
.nav-cta { .nav-cta {
display: flex; display: flex;