diff --git a/app/src/components/HeaderNav.tsx b/app/src/components/HeaderNav.tsx
index 051151d..8f871f5 100644
--- a/app/src/components/HeaderNav.tsx
+++ b/app/src/components/HeaderNav.tsx
@@ -3,8 +3,8 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
-import { MotionConfig, motion, useReducedMotion } from "motion/react";
-import { Menu, Search, X } from "lucide-react";
+import { AnimatePresence, MotionConfig, motion, useReducedMotion } from "motion/react";
+import { ArrowUpRight, BookOpen, Bot, ChartCandlestick, ChevronRight, Menu, MessagesSquare, Rocket, Search, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { isTypingTarget } from "@/lib/command-palette";
import { heroCtaOnScreen, subscribeHeroCta } from "@/lib/hero-cta";
@@ -19,15 +19,18 @@ import LivePulse from "./launchpad/LivePulse";
import { BridgeButton, BridgeProvider } from "./bridge/BridgeProvider";
const NAV = [
- { href: "/", label: "Launchpad" },
- { href: "/feed", label: "Posts" },
- { href: "/rules", label: "How it works" },
- { href: "/agents", label: "Agents" },
+ { href: "/", label: "Launchpad", hint: "Live launches, charts and trades", icon: ChartCandlestick },
+ { href: "/feed", label: "Posts", hint: "What people say about tokens", icon: MessagesSquare },
+ { href: "/rules", label: "How it works", hint: "What a launch does and costs", icon: BookOpen },
+ { href: "/agents", label: "Agents", hint: "Launch and trade from an agent", icon: Bot },
// /me is reached from the wallet menu ("Your workspace") and the footer ("Your dashboard")
];
type Pulse = { visits: number; online: number };
+/** The glass the strip turns into while it floats: translucent card, a hairline of light on top, a soft drop shadow. */
+const FLOATING = "bg-card/80 shadow-[inset_0_1px_0_rgb(255_255_255/0.06),var(--shadow-card-hover)] dark:bg-card/75";
+
/**
* Site header: a floating instrument strip. Full-width hairline row at rest;
* past 100px of scroll the navigation shell contracts it into a
@@ -80,15 +83,27 @@ function useShortcutLabel(): string {
}
/**
- * The header's way into search: one icon button that names its shortcut. The strip is already full at
- * rest (pulse, four links, five controls), so a labelled field would push it past the page column;
- * the home redesign gives search a proper field.
+ * The header's tools in one capsule: search, bridge, notifications and theme read as one group beside the wallet and
+ * the launch button, instead of four separate frames. Each tool inside is a borderless 30px button.
*/
-function SearchTrigger({ onClick }: { onClick: () => void }) {
+function Rail({ children, className }: { children: React.ReactNode; className?: string }) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * The header's way into search: the first tool in the rail, naming its shortcut beside the icon while the strip
+ * rests at full width (xl and up). The home redesign gives search a proper field.
+ */
+function SearchTrigger({ onClick, compact = false }: { onClick: () => void; compact?: boolean }) {
const shortcut = useShortcutLabel();
return (
-
+
+ {compact ? null : {shortcut} }
);
}
@@ -114,13 +129,15 @@ function Desktop({ visible = false, pulse, isActive, quietCta, onSearch }: { vis
className={cn(
"border border-line transition-colors",
// At rest: a full-width hairline ROW, content padded to the page column
- // (max-w-6xl minus its px-4). Floating: a bordered card pill over the
- // content. The shell animates the width and corner radius together.
+ // (--page-max and --page-pad in globals.css). Floating: a glass pill
+ // over the content. The shell animates the width and corner radius together.
visible
- ? "max-w-6xl px-3 bg-card/90 dark:bg-card/90"
- : "max-w-none border-x-0 border-t-0 px-[max(16px,calc((100vw-1120px)/2))] bg-paper dark:bg-paper",
+ ? `max-w-6xl px-2.5 ${FLOATING}`
+ : "max-w-none border-x-0 border-t-0 px-[max(var(--page-pad),calc((100vw_-_var(--page-max))/2_+_var(--page-pad)))] bg-paper dark:bg-paper",
)}
>
+ {/* at rest, the bottom hairline catches a little brand light in the middle */}
+ {!visible ? : null}
{/* the wordmark and pulse are the first things to give way when the strip contracts */}
@@ -135,11 +152,13 @@ function Desktop({ visible = false, pulse, isActive, quietCta, onSearch }: { vis
-
- {/* the floating pill is 800px wide: search costs a button, so the bridge gives up its label */}
-
-
-
+
+
+ {/* the floating pill is 800px wide: the bridge gives up its label there */}
+
+
+
+
{/* while the hero's CTA is on screen it owns the one filled blue; this one fills in once that has scrolled away */}
@@ -149,14 +168,15 @@ function Desktop({ visible = false, pulse, isActive, quietCta, onSearch }: { vis
}
/**
- * Nav links in normal flex flow between the brand and the utilities. All three
- * groups share the available width when the strip contracts to 800px.
- * One shared hover pill slides
- * between items via layoutId; under reduced motion it simply appears.
+ * Nav links in normal flex flow between the brand and the utilities. The page you
+ * are on sits in a filled pill with a small lamp of brand light on its top edge;
+ * it glides to the new link when you navigate. A fainter pill follows the pointer
+ * between the other links. Under reduced motion both simply appear.
*/
function NavLinks({ isActive, compact = false }: { isActive: (href: string) => boolean; compact?: boolean }) {
const [hovered, setHovered] = useState
(null);
const reduced = useReducedMotion();
+ const glide = reduced ? { duration: 0 } : ({ type: "spring", stiffness: 420, damping: 34 } as const);
return (
setHovered(null)} className="mx-auto hidden items-center gap-0.5 px-2 lg:flex">
{NAV.map((n, i) => {
@@ -169,16 +189,14 @@ function NavLinks({ isActive, compact = false }: { isActive: (href: string) => b
onFocus={() => setHovered(i)}
onBlur={() => setHovered(null)}
aria-current={active ? "page" : undefined}
- className={cn("relative h-9 inline-flex items-center rounded-full text-sm font-medium whitespace-nowrap transition-colors", compact ? "px-2" : "px-3.5", active ? "text-ink" : "text-body hover:text-ink")}
+ className={cn("relative h-9 inline-flex items-center rounded-full text-sm font-medium whitespace-nowrap transition-colors", compact ? "px-2.5" : "px-3.5", active ? "text-ink" : "text-body hover:text-ink")}
>
- {hovered === i ? (
- reduced ? (
-
- ) : (
-
- )
- ) : active ? (
-
+ {hovered === i && !active ? : null}
+ {active ? (
+
+
+
+
) : null}
{n.label}
@@ -199,19 +217,22 @@ function LaunchCta({ compact = false, block = false, quiet = false, onNavigate }
href="/launch"
onClick={onNavigate}
className={cn(
- "inline-flex items-center justify-center gap-1.5 font-semibold whitespace-nowrap transition-colors",
- quiet ? "border border-line bg-card text-body hover:text-ink hover:border-line-strong" : "bg-brand text-inverse hover:bg-brand-strong",
+ "group relative inline-flex items-center justify-center gap-1.5 overflow-hidden font-semibold whitespace-nowrap transition-[color,background-color,border-color,box-shadow,transform] duration-150 active:scale-[0.97] motion-reduce:active:scale-100",
+ // filled: the same rocket and sheen as the hero button it takes over from
+ quiet ? "border border-line bg-card text-body hover:text-ink hover:border-line-strong" : "bb-sheen bg-brand text-inverse shadow-[inset_0_1px_0_rgb(255_255_255/0.2)] hover:bg-brand-strong",
block ? "w-full min-h-12 rounded-xl text-[15px]" : "h-9 rounded-full text-[13px]",
block ? "" : compact ? "px-3.5" : "px-4",
)}
>
- {/* at lg (1024–1279) the rest-state strip has no room for the long label beside five links */}
+
+ {/* at lg (1024–1279) the rest-state strip has no room for the long label beside five links; the phone menu has the whole width */}
{compact ? (
"Launch"
+ ) : block ? (
+ "Launch a token"
) : (
- <>
- Launch a token
- >
+ // one flex item, so the button's icon gap never lands between "Launch" and "a token"
+ Launch a token
)}
);
@@ -226,10 +247,12 @@ function XLink({ block = false }: { block?: boolean }) {
);
if (block) {
+ // a settings row like its neighbours: mark in the 17px icon column, label, then the arrow that says it leaves the site
return (
-
+
+ {icon}
@{BRAND_X} on X
- {icon}
+
);
}
@@ -242,6 +265,21 @@ function XLink({ block = false }: { block?: boolean }) {
// ── mobile ───────────────────────────────────────────────────────────────────
+/** Holds the page still while the phone menu is open, so a swipe scrolls the menu, not what is under it. */
+function useScrollLock(locked: boolean) {
+ useEffect(() => {
+ if (!locked) return;
+ const root = document.documentElement;
+ const before = root.style.overflow;
+ root.style.overflow = "hidden";
+ return () => {
+ root.style.overflow = before;
+ };
+ }, [locked]);
+}
+
+const mobileTool = "h-9 w-9 inline-flex items-center justify-center rounded-full text-ink transition-colors hover:bg-line motion-reduce:transition-none";
+
function Mobile({ visible = false, pulse, isActive, onSearch }: { visible?: boolean; pulse: Pulse; isActive: (href: string) => boolean; onSearch: () => void }) {
const [open, setOpen] = useState(false);
const menuToggle = useRef(null);
@@ -249,76 +287,96 @@ function Mobile({ visible = false, pulse, isActive, onSearch }: { visible?: bool
setOpen(false);
menuToggle.current?.focus();
}, []);
+ useScrollLock(open);
return (
-
-
-
- setOpen(false)}>
-
-
-
- {/* self-hides below sm; on tablets it rides beside the wordmark as on desktop */}
-
-
-
- {
- setOpen(false);
- onSearch();
- }}
- aria-label="Search tokens and pages"
- className="h-10 w-10 inline-flex items-center justify-center rounded-full border border-line bg-card text-ink hover:border-line-strong"
- >
-
-
- setOpen((v) => !v)}
- aria-label={open ? "Close menu" : "Open menu"}
- aria-expanded={open}
- aria-controls="mobile-menu"
- className="h-10 w-10 inline-flex items-center justify-center rounded-full border border-line bg-card text-ink hover:border-line-strong"
- >
- {open ? : }
-
-
-
+ <>
+ {/* the page dims behind the open menu; a tap on it closes the menu */}
+
+ {open ? : null}
+
+
+
+
+ setOpen(false)}>
+
+
+
+ {/* self-hides below sm; on tablets it rides beside the wordmark as on desktop */}
+
+
+
+ {
+ setOpen(false);
+ onSearch();
+ }}
+ aria-label="Search tokens and pages"
+ className={mobileTool}
+ >
+
+
+ setOpen((v) => !v)}
+ aria-label={open ? "Close menu" : "Open menu"}
+ aria-expanded={open}
+ aria-controls="mobile-menu"
+ className={cn(mobileTool, open ? "bg-line" : "")}
+ >
+ {open ? : }
+
+
+
-
-
-
-
+ {/* a card of its own just under the strip, so it reads the same whether the strip rests or floats */}
+
+
+
+
+ >
);
}
diff --git a/app/src/components/NotificationSettings.module.css b/app/src/components/NotificationSettings.module.css
index 9d36330..81c4ce7 100644
--- a/app/src/components/NotificationSettings.module.css
+++ b/app/src/components/NotificationSettings.module.css
@@ -4,22 +4,20 @@
flex-shrink: 0;
align-items: center;
justify-content: center;
- width: 36px;
- height: 36px;
- border: 1px solid var(--color-line);
+ /* one borderless tool in the header's rail (HeaderNav), which draws the frame */
+ width: 30px;
+ height: 30px;
+ border: 1px solid transparent;
border-radius: 999px;
- background: var(--color-card);
+ background: transparent;
color: var(--color-body);
cursor: pointer;
transition: background 160ms ease, border-color 160ms ease, color 160ms ease;
}
-/* Larger hit area without widening the desktop instrument strip. */
-.trigger::before { content: ""; position: absolute; inset: -4px; border-radius: inherit; }
-.trigger:hover, .trigger[data-popup-open] { background: var(--color-line); border-color: var(--color-line-strong); color: var(--color-ink); }
+.trigger:hover, .trigger[data-popup-open] { background: var(--color-line); color: var(--color-ink); }
.bell { position: relative; display: flex; }
.dot { position: absolute; top: -2px; right: -2px; width: 6px; height: 6px; border-radius: 50%; background: var(--color-brand); box-shadow: 0 0 0 2px var(--color-card); }
-.block { width: 100%; min-height: 48px; height: auto; padding: 12px; justify-content: start; gap: 12px; border-color: transparent; border-radius: 12px; font-size: 14px; font-weight: 500; }
-.block::before { display: none; }
+.block { width: 100%; min-height: 48px; height: auto; padding: 12px; justify-content: start; gap: 12px; border-color: transparent; border-radius: 12px; color: var(--color-ink); font-size: 15px; font-weight: 500; }
.triggerStatus { margin-left: auto; color: var(--color-muted); font-size: 12px; font-weight: 400; }
.positioner { z-index: 70; }
.backdrop { position: fixed; inset: 0; z-index: 69; background: color-mix(in srgb, var(--color-scrim) 55%, transparent); }
diff --git a/app/src/components/ThemeToggle.tsx b/app/src/components/ThemeToggle.tsx
index 2bc917e..9714500 100644
--- a/app/src/components/ThemeToggle.tsx
+++ b/app/src/components/ThemeToggle.tsx
@@ -2,6 +2,7 @@
import { useSyncExternalStore } from "react";
import { useTheme } from "next-themes";
+import { SunMoon } from "lucide-react";
import { AnimatedThemeToggler } from "@/components/vendor/animated-theme-toggler";
/**
@@ -20,7 +21,10 @@ export default function ThemeToggle({ block = false }: { block?: boolean }) {
() => false,
);
- const control = "h-9 w-9 inline-flex items-center justify-center rounded-full border border-line bg-card text-body hover:text-ink hover:border-line-strong [&_svg]:size-4";
+ // in the header it is one borderless tool in the rail; in the phone menu's settings row it keeps its own frame
+ const control = block
+ ? "h-9 w-9 inline-flex items-center justify-center rounded-full border border-line bg-card text-body hover:text-ink hover:border-line-strong [&_svg]:size-4"
+ : "h-7.5 w-7.5 inline-flex shrink-0 items-center justify-center rounded-full text-body transition-colors hover:bg-line hover:text-ink motion-reduce:transition-none [&_svg]:size-4";
if (!mounted) {
return block ?
:
;
@@ -35,13 +39,14 @@ export default function ThemeToggle({ block = false }: { block?: boolean }) {
if (!block) return toggler;
// In the mobile menu the toggler sits in a list of labelled rows, so it gets
- // a visible label too — a settings row, label left, control right.
+ // a visible label too — a settings row: icon and label left, control right.
return (
-
+
+
Theme · {theme === "dark" ? "Dark" : "Light"}
- {toggler}
+ {toggler}
);
}
diff --git a/app/src/components/bridge/BridgeDialog.module.css b/app/src/components/bridge/BridgeDialog.module.css
index 82161cd..6f498bf 100644
--- a/app/src/components/bridge/BridgeDialog.module.css
+++ b/app/src/components/bridge/BridgeDialog.module.css
@@ -1,7 +1,8 @@
-.trigger { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 36px; padding: 0 11px; border: 1px solid var(--color-line); border-radius: 999px; color: var(--color-body); background: var(--color-card); font-size: 13px; font-weight: 500; cursor: pointer; transition: background 160ms, color 160ms, transform 160ms; }
-.compactTrigger { width: 36px; padding: 0; }
+/* in the header the trigger is one borderless tool in the rail (HeaderNav), which draws the frame */
+.trigger { display: inline-flex; flex-shrink: 0; align-items: center; justify-content: center; gap: 7px; min-height: 30px; padding: 0 10px; border: 1px solid transparent; border-radius: 999px; color: var(--color-body); background: transparent; font-size: 13px; font-weight: 500; cursor: pointer; transition: background 160ms, color 160ms, transform 160ms; }
+.compactTrigger { width: 30px; padding: 0; }
.compactTrigger .triggerLabel { display: none; }
-.blockTrigger { width: 100%; justify-content: flex-start; min-height: 48px; padding: 0 12px; border: 0; border-radius: 12px; color: var(--color-ink); font-size: 16px; }
+.blockTrigger { width: 100%; justify-content: flex-start; gap: 12px; min-height: 48px; padding: 0 12px; border: 0; border-radius: 12px; color: var(--color-ink); font-size: 15px; }
.triggerHint { margin-left: auto; font-size: 12px; color: var(--color-muted); }
.backdrop { position: fixed; inset: 0; z-index: 80; background: color-mix(in srgb, var(--color-scrim) 45%, transparent); opacity: 1; transition: opacity 180ms ease-out; }
.backdrop[data-starting-style], .backdrop[data-ending-style] { opacity: 0; }
@@ -143,14 +144,15 @@
@media (hover: hover) and (pointer: fine) {
.networkTrigger:not(:disabled):hover { background: var(--color-paper); border-color: var(--color-line); }
.assetTrigger:not(:disabled):hover { border-color: var(--color-brand); }
- .trigger:hover, .close:hover, .reverse:hover, .externalAction:hover { color: var(--color-ink); background: var(--color-paper); }
+ .close:hover, .reverse:hover, .externalAction:hover { color: var(--color-ink); background: var(--color-paper); }
+ .trigger:hover { color: var(--color-ink); background: var(--color-line); }
.primary:not(:disabled):hover { background: var(--color-brand-strong); }
.footer a:hover, .explorerLinks a:hover { text-decoration: underline; }
}
.primary:not(:disabled):active, .reverse:not(:disabled):active, .trigger:active { transform: scale(.97); }
.networkTrigger:not(:disabled):active:not(:focus-visible) { transform: scale(.98); }
.networkTrigger:focus-visible { transition: none; }
-@media (min-width: 1024px) and (max-width: 1279px) { .trigger:not(.blockTrigger) { width: 36px; padding: 0; } .trigger:not(.blockTrigger) .triggerLabel { display: none; } }
+@media (min-width: 1024px) and (max-width: 1279px) { .trigger:not(.blockTrigger) { width: 30px; padding: 0; } .trigger:not(.blockTrigger) .triggerLabel { display: none; } }
@media (max-width: 520px) {
.panel { width: 100%; top: auto; bottom: 0; left: 0; max-height: calc(100dvh - 16px); transform: none; padding: 24px 22px calc(20px + env(safe-area-inset-bottom)); border-radius: 22px 22px 0 0; border-bottom: 0; box-shadow: var(--shadow-sheet); }
.panel[data-starting-style], .panel[data-ending-style] { transform: translateY(16px); }
diff --git a/app/src/components/launchpad/LivePulse.tsx b/app/src/components/launchpad/LivePulse.tsx
index f5c62ad..2be29e7 100644
--- a/app/src/components/launchpad/LivePulse.tsx
+++ b/app/src/components/launchpad/LivePulse.tsx
@@ -49,12 +49,13 @@ export default function LivePulse({ initial, block = false }: { initial: Pulse;
);
if (block) {
+ // the phone menu's footer line: quiet, under the buttons
return (
-
+
- visits
+ visits
- {online}
+ {online}
);
}
diff --git a/app/src/components/navigation-shell.tsx b/app/src/components/navigation-shell.tsx
index 85c2f73..de007b9 100644
--- a/app/src/components/navigation-shell.tsx
+++ b/app/src/components/navigation-shell.tsx
@@ -39,7 +39,7 @@ function NavigationSurface({ children, className, visible = false, mobile = fals
width,
y: visible ? 20 : 0,
borderRadius: visible ? 28 : 0,
- backdropFilter: visible ? "blur(10px)" : "blur(0px)",
+ backdropFilter: visible ? "blur(16px) saturate(1.5)" : "blur(0px) saturate(1)",
...(mobile ? { paddingLeft: visible ? 12 : 16, paddingRight: visible ? 12 : 16 } : {}),
}}
transition={{ duration: reduced ? 0 : 0.28, ease: [0.22, 1, 0.36, 1] }}
@@ -87,9 +87,9 @@ export function MobileNavMenu({ children, className, isOpen, onClose }: SlotProp
{isOpen ? (