diff --git a/frontend/src/components/react/Search.tsx b/frontend/src/components/react/Search.tsx
index 0d1e767..cbb350f 100644
--- a/frontend/src/components/react/Search.tsx
+++ b/frontend/src/components/react/Search.tsx
@@ -129,8 +129,7 @@ export default function Search() {
type="button"
onClick={() => setOpen(true)}
aria-label={`Search the catalogue (${isMac ? '⌘' : 'Ctrl'}+K)`}
- title={`Search (${isMac ? '⌘' : 'Ctrl'}+K)`}
- className="topbar-control tc-collapse-md"
+ className="topbar-control tc-collapse-md kbd-tip-host"
>
Search
+
+ {isMac ? '⌘' : 'Ctrl'}
+ K
+
{open && (
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index e9aee35..b936ecf 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -829,6 +829,19 @@ code, pre, kbd, samp {
gap: 2px;
position: relative;
}
+.nameplate::after {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: -6px;
+ height: 2px;
+ background: linear-gradient(to right,
+ var(--mauve) 0%,
+ var(--mauve) 35%,
+ var(--surface2) 35%,
+ var(--surface2) 100%);
+}
.nameplate-title {
font-family: var(--font-display);
font-weight: 600;
@@ -1098,6 +1111,67 @@ code, pre, kbd, samp {
.topbar-control svg { width: 15px; height: 15px; flex-shrink: 0; }
/* Exact-square icon-only variant — keeps the row aligned. */
.topbar-control--icon { width: 2rem; padding: 0; }
+
+/* Keyboard-shortcut hover/focus tooltip — kept out of the button label,
+ * surfaced only on hover or keyboard focus. */
+.kbd-tip-host { position: relative; }
+.kbd-tip {
+ position: absolute;
+ top: calc(100% + 8px);
+ left: 50%;
+ display: flex;
+ align-items: center;
+ gap: 0.3rem;
+ white-space: nowrap;
+ padding: 4px 8px;
+ font-family: var(--font-sans);
+ font-size: 0.6rem;
+ font-style: normal;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: var(--subtext1);
+ background: color-mix(in srgb, var(--crust) 90%, transparent);
+ border: 1px solid color-mix(in srgb, var(--surface2) 70%, transparent);
+ border-radius: 4px;
+ box-shadow: 0 8px 20px -10px rgba(0, 0, 0, 0.5);
+ opacity: 0;
+ transform: translate(-50%, 4px);
+ pointer-events: none;
+ transition: opacity 0.16s ease, transform 0.16s ease;
+ z-index: 60;
+}
+.kbd-tip kbd {
+ font-family: var(--font-mono);
+ font-size: 0.62rem;
+ line-height: 1;
+ padding: 2px 5px;
+ color: var(--text);
+ background: color-mix(in srgb, var(--surface0) 70%, transparent);
+ border: 1px solid color-mix(in srgb, var(--surface2) 80%, transparent);
+ border-radius: 3px;
+}
+.kbd-tip-host:hover .kbd-tip,
+.kbd-tip-host:focus-visible .kbd-tip {
+ opacity: 1;
+ transform: translate(-50%, 0);
+}
+/* Breakcore: hard neon tooltip — matches the layer's offset-shadow chrome. */
+.breakcore .kbd-tip {
+ background: var(--crust);
+ border-color: var(--mauve);
+ border-radius: 0;
+ color: var(--green);
+ box-shadow: 2px 2px 0 var(--mauve);
+}
+.breakcore .kbd-tip kbd {
+ color: var(--rosewater);
+ background: var(--surface0);
+ border-color: var(--mauve);
+ border-radius: 0;
+}
+@media (prefers-reduced-motion: reduce) {
+ .kbd-tip { transition: opacity 0.16s ease; transform: translate(-50%, 0); }
+}
.topbar-control--danger:hover {
color: var(--red);
border-color: color-mix(in srgb, var(--red) 55%, var(--surface2));
@@ -1283,7 +1357,17 @@ input[type="date"] { color-scheme: light; }
);
}
-/* Nameplate — glitch-shear burst on hover (underline removed site-wide). */
+/* Nameplate — breakcore reworks the underline: hard cyan offset + magenta
+ * neon glow (the layer's hard-offset chrome language) instead of the
+ * default two-tone rule. Plus a glitch-shear burst on hover. */
+.breakcore .nameplate::after {
+ height: 2px;
+ bottom: -7px;
+ background: var(--mauve);
+ box-shadow:
+ 2px 2px 0 var(--blue),
+ 0 0 10px color-mix(in srgb, var(--mauve) 70%, transparent);
+}
@keyframes bc-shear {
0% { clip-path: inset(0 0 0 0); transform: translateX(0);
text-shadow: -1px 0 0 var(--teal), 1px 0 0 var(--mauve); }
@@ -1310,11 +1394,15 @@ input[type="date"] { color-scheme: light; }
40% { clip-path: inset(68% 0 8% 0); transform: translateX(-5px); }
60% { clip-path: inset(24% 0 36% 0); transform: translateX(3px); }
80% { clip-path: inset(4% 0 84% 0); transform: translateX(-2px); }
- 100% { opacity: 1; clip-path: inset(0 0 0 0); transform: translateX(0); }
+ /* End unclipped (none, not inset(0)) so italic-Fraunces descenders
+ * (g, y, p) aren't sliced at the box edge once the glitch settles. */
+ 100% { opacity: 1; clip-path: none; transform: translateX(0); }
}
.breakcore .prose h1,
.breakcore h1.font-display {
- animation: bc-load-glitch 460ms steps(5, jump-none) both;
+ /* `backwards` (not `both`): after the one-shot, props revert to base —
+ * clip-path: none — instead of persisting the final inset clip. */
+ animation: bc-load-glitch 460ms steps(5, jump-none) backwards;
}
/* Plate — hard hover (no soft lift), RGB-split image, scanline sweep. */