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:
237
vibn-frontend/app/(marketing)/AccentSwitcher.tsx
Normal file
237
vibn-frontend/app/(marketing)/AccentSwitcher.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -915,10 +915,23 @@ function Hero({ onStart, variant = "quote" }) {
|
||||
line-height: 0.98;
|
||||
text-wrap: balance;
|
||||
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 {
|
||||
/* keep the accent word in full color inside the silver heading */
|
||||
color: var(--accent);
|
||||
-webkit-text-fill-color: var(--accent);
|
||||
font-family: "Geist", serif;
|
||||
font-weight: 500;
|
||||
line-height: 0;
|
||||
@@ -963,13 +976,62 @@ function Hero({ onStart, variant = "quote" }) {
|
||||
border-radius: var(--r-xl);
|
||||
padding: 1px;
|
||||
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.25 0.012 60 / 0.4));
|
||||
box-shadow:
|
||||
0 30px 80px -20px oklch(0 0 0 / 0.6),
|
||||
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 {
|
||||
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);
|
||||
@@ -1037,7 +1099,7 @@ function Hero({ onStart, variant = "quote" }) {
|
||||
background: var(--accent);
|
||||
color: var(--accent-fg);
|
||||
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;
|
||||
}
|
||||
.prompt-send:hover { transform: translateY(-1px); }
|
||||
@@ -1095,17 +1157,17 @@ function Hero({ onStart, variant = "quote" }) {
|
||||
|
||||
{/* ambient glows behind hero */}
|
||||
<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}
|
||||
style={{ top: "-200px", left: "50%", transform: "translateX(-50%)" }}
|
||||
/>
|
||||
<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}
|
||||
style={{ top: "20%", left: "-200px" }}
|
||||
/>
|
||||
<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}
|
||||
style={{ top: "30%", right: "-150px" }}
|
||||
/>
|
||||
@@ -1156,6 +1218,7 @@ function Hero({ onStart, variant = "quote" }) {
|
||||
{/* Prompt */}
|
||||
<div className="prompt">
|
||||
<div className="prompt-frame">
|
||||
<span className="prompt-beam" aria-hidden="true" />
|
||||
<div className="prompt-inner">
|
||||
<div style={{ position: "relative" }}>
|
||||
<textarea
|
||||
@@ -1860,7 +1923,7 @@ function Journey() {
|
||||
background: var(--bg);
|
||||
padding: 6px 12px;
|
||||
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;
|
||||
box-shadow: 0 0 24px var(--accent-glow);
|
||||
transform: translateY(-1px);
|
||||
@@ -1878,7 +1941,7 @@ function Journey() {
|
||||
margin-top: 1px;
|
||||
}
|
||||
.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-ok { color: var(--ok); }
|
||||
.demo-host {
|
||||
@@ -2200,8 +2263,8 @@ function Audience() {
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
padding: 3px 7px;
|
||||
background: oklch(0.74 0.175 35 / 0.12);
|
||||
border: 1px solid oklch(0.74 0.175 35 / 0.4);
|
||||
background: oklch(0.74 calc(0.175 * var(--accent-cm)) var(--accent-h) / 0.12);
|
||||
border: 1px solid oklch(0.74 calc(0.175 * var(--accent-cm)) var(--accent-h) / 0.4);
|
||||
border-radius: 4px;
|
||||
margin-top: 1px;
|
||||
flex-shrink: 0;
|
||||
@@ -2306,7 +2369,7 @@ function Closing() {
|
||||
.closing-title .accent { color: var(--accent); }
|
||||
.closing-title em {
|
||||
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;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
@@ -2330,12 +2393,12 @@ function Closing() {
|
||||
`}</style>
|
||||
|
||||
<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}
|
||||
style={{ top: "20%", left: "50%", transform: "translateX(-50%)" }}
|
||||
/>
|
||||
<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}
|
||||
style={{ bottom: "-200px", left: "50%", transform: "translateX(-50%)" }}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
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";
|
||||
|
||||
export const metadata = {
|
||||
@@ -6,5 +9,10 @@ export const metadata = {
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <NewSite />;
|
||||
return (
|
||||
<>
|
||||
<NewSite />
|
||||
<AccentSwitcher />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,23 @@
|
||||
--fg-mute: oklch(0.58 0.006 80);
|
||||
--fg-faint: oklch(0.42 0.006 80);
|
||||
|
||||
--accent: oklch(0.74 0.175 35); /* warm coral, default */
|
||||
--accent-soft: oklch(0.74 0.175 35 / 0.18);
|
||||
--accent-glow: oklch(0.74 0.175 35 / 0.35);
|
||||
--accent-fg: oklch(0.18 0.04 35);
|
||||
/* ── Accent color — change these two to retheme the whole site ──────────
|
||||
--accent-h : hue 0-360. coral 35 · red 25 · amber 75 · green 150
|
||||
· teal 195 · blue 250 · indigo 270 · violet 300 · pink 350
|
||||
--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);
|
||||
|
||||
@@ -100,6 +113,34 @@ h4 {
|
||||
letter-spacing: -0.02em;
|
||||
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 {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -172,7 +213,9 @@ button {
|
||||
background: var(--accent);
|
||||
color: var(--accent-fg);
|
||||
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 0 50px -8px var(--accent-glow);
|
||||
}
|
||||
@@ -269,7 +312,7 @@ button {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
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:
|
||||
0 0 22px var(--accent-glow),
|
||||
@@ -293,11 +336,30 @@ button {
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 28px;
|
||||
color: var(--fg-mute);
|
||||
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 {
|
||||
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 {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user