You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iggy.apache.org by pi...@apache.org on 2026/04/20 20:46:06 UTC

(iggy) 01/01: feat(bench): improve UI, add new mode and landing page

This is an automated email from the ASF dual-hosted git repository.

spetz pushed a commit to branch bench_ui_refactor
in repository https://gitbox.apache.org/repos/asf/iggy.git

commit 05273f2ab814b186fc81bd3ae20e269b1ed0e5be
Author: spetz <pi...@gmail.com>
AuthorDate: Mon Apr 20 22:45:52 2026 +0200

    feat(bench): improve UI, add new mode and landing page
---
 core/bench/dashboard/frontend/assets/style.css     | 1104 +++++++++++++++++++-
 .../frontend/src/components/app_content.rs         |   21 +-
 .../dashboard/frontend/src/components/chart/mod.rs |    1 +
 .../frontend/src/components/chart/single_chart.rs  |    2 +-
 .../frontend/src/components/chart/tail_chart.rs    |  420 ++++++++
 .../frontend/src/components/chart/trend_chart.rs   |    6 +-
 .../frontend/src/components/embed_modal.rs         |    1 +
 .../frontend/src/components/layout/hero.rs         |  391 +++++++
 .../frontend/src/components/layout/logo.rs         |   35 +-
 .../frontend/src/components/layout/main_content.rs |  220 +++-
 .../frontend/src/components/layout/mod.rs          |    5 +
 .../src/components/layout/percentile_card.rs       |   93 ++
 .../frontend/src/components/layout/sidebar.rs      |   16 +-
 .../src/components/layout/sidebar_toggle.rs        |   57 +
 .../frontend/src/components/layout/sweep_view.rs   |  290 +++++
 .../src/components/layout/theme_toggle_floating.rs |   66 ++
 .../frontend/src/components/layout/topbar.rs       |   14 +-
 .../src/components/selectors/benchmark_selector.rs |  249 +++--
 .../selectors/measurement_type_selector.rs         |    9 +
 .../frontend/src/components/selectors/mod.rs       |    1 +
 .../components/selectors/param_filters_panel.rs    |  211 ++++
 .../selectors/recent_benchmarks_selector.rs        |   78 +-
 .../dashboard/frontend/src/components/theme/mod.rs |    1 -
 .../frontend/src/components/theme/theme_toggle.rs  |   57 -
 .../dashboard/frontend/src/state/benchmark.rs      |   74 +-
 core/bench/dashboard/frontend/src/state/ui.rs      |  165 ++-
 scripts/dashboard/run_dev.sh                       |   13 +-
 27 files changed, 3334 insertions(+), 266 deletions(-)

diff --git a/core/bench/dashboard/frontend/assets/style.css b/core/bench/dashboard/frontend/assets/style.css
index 8a82cce6c..c4368b939 100644
--- a/core/bench/dashboard/frontend/assets/style.css
+++ b/core/bench/dashboard/frontend/assets/style.css
@@ -331,6 +331,21 @@ body.dark select option {
     padding: 0 var(--spacing-md);
 }
 
+.logo-home-button {
+    display: block;
+    width: 100%;
+    background: transparent;
+    border: none;
+    cursor: pointer;
+    color: inherit;
+    font: inherit;
+    transition: opacity var(--transition-speed);
+}
+
+.logo-home-button:hover {
+    opacity: 0.8;
+}
+
 .logo img {
     max-width: 200px;
     height: auto;
@@ -349,8 +364,9 @@ body.dark select option {
 .top-buttons {
     display: flex;
     align-items: center;
+    justify-content: flex-end;
     gap: var(--spacing-sm);
-    padding: var(--spacing-sm);
+    padding: var(--spacing-sm) 60px var(--spacing-sm) 60px;
     background-color: var(--color-background);
     z-index: 20;
 }
@@ -1876,3 +1892,1089 @@ body.dark .benchmark-list-item-metric.message-throughput {
     color: #fff;
     border-color: #777;
 }
+
+/* Param filters panel (producers / consumers / streams / partitions / consumer groups) */
+.param-filters-container {
+    margin: var(--spacing-md) 0;
+    width: 100%;
+    border: 1px solid var(--color-border);
+    border-radius: var(--border-radius);
+    background-color: var(--color-background);
+    overflow: hidden;
+}
+
+.param-filters-header {
+    width: 100%;
+    display: flex;
+    align-items: center;
+    gap: var(--spacing-sm);
+    padding: var(--spacing-sm) var(--spacing-md);
+    background: none;
+    border: none;
+    cursor: pointer;
+    font-size: var(--font-size-sm);
+    color: var(--color-text);
+    text-align: left;
+    transition: background-color var(--transition-speed);
+}
+
+.param-filters-header:hover {
+    background-color: var(--color-active);
+}
+
+.param-filters-chevron {
+    display: inline-block;
+    width: 1em;
+    color: var(--color-text-secondary);
+    font-family: monospace;
+}
+
+.param-filters-title {
+    font-weight: 500;
+    flex: 1;
+}
+
+.param-filters-badge {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    min-width: 20px;
+    height: 20px;
+    padding: 0 6px;
+    border-radius: 10px;
+    background-color: var(--color-dark-active, #0d6efd);
+    color: #fff;
+    font-size: 11px;
+    font-weight: 600;
+}
+
+.param-filters-body {
+    display: flex;
+    flex-direction: column;
+    gap: var(--spacing-xs, 4px);
+    padding: var(--spacing-sm) var(--spacing-md) var(--spacing-md);
+    border-top: 1px solid var(--color-border);
+}
+
+.param-filter-row {
+    display: grid;
+    grid-template-columns: 110px 1fr auto 1fr;
+    align-items: center;
+    gap: var(--spacing-sm);
+}
+
+.param-filter-label {
+    font-size: var(--font-size-sm);
+    color: var(--color-text-secondary);
+}
+
+.param-filter-input {
+    width: 100%;
+    padding: 4px 8px;
+    border: 1px solid var(--color-border);
+    border-radius: var(--border-radius);
+    font-size: var(--font-size-sm);
+    box-sizing: border-box;
+    background-color: var(--color-background);
+    color: var(--color-text);
+}
+
+.param-filter-input:focus {
+    outline: none;
+    border-color: var(--color-active);
+    box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
+}
+
+.param-filter-sep {
+    color: var(--color-text-secondary);
+    font-size: var(--font-size-sm);
+    user-select: none;
+}
+
+.param-filters-clear {
+    margin-top: var(--spacing-xs, 4px);
+    padding: 4px 8px;
+    border: 1px solid var(--color-border);
+    border-radius: var(--border-radius);
+    background: transparent;
+    color: var(--color-text-secondary);
+    font-size: var(--font-size-sm);
+    cursor: pointer;
+    align-self: flex-end;
+    transition: all var(--transition-speed);
+}
+
+.param-filters-clear:hover {
+    color: var(--color-text);
+    border-color: var(--color-active);
+}
+
+body.dark .param-filter-input {
+    background-color: var(--color-dark-input);
+    border-color: var(--color-dark-border);
+    color: var(--color-dark-text);
+}
+
+body.dark .param-filter-input:focus {
+    border-color: var(--color-dark-active);
+    box-shadow: 0 0 0 2px rgba(var(--color-dark-active-rgb), 0.4);
+}
+
+body.dark .param-filters-container {
+    border-color: var(--color-dark-border);
+    background-color: var(--color-dark-sidebar, transparent);
+}
+
+body.dark .param-filters-body {
+    border-top-color: var(--color-dark-border);
+}
+
+body.dark .param-filters-header:hover {
+    background-color: var(--color-dark-active-bg, rgba(255, 255, 255, 0.05));
+}
+
+/* Sidebar collapse */
+.sidebar {
+    transition: width var(--transition-speed) ease, border-right-color var(--transition-speed) ease;
+}
+
+.container.sidebar-collapsed .sidebar {
+    width: 0;
+    min-width: 0;
+    border-right-color: transparent;
+    padding: 0;
+    pointer-events: none;
+}
+
+.container.sidebar-collapsed .sidebar > * {
+    opacity: 0;
+    transition: opacity calc(var(--transition-speed) / 2) ease;
+}
+
+.sidebar-toggle-floating {
+    position: fixed;
+    top: 14px;
+    left: 486px;
+    z-index: 50;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 32px;
+    height: 32px;
+    background: var(--color-background);
+    border: 1px solid var(--color-border);
+    border-radius: 8px;
+    cursor: pointer;
+    color: var(--color-text-secondary);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+    transition: left var(--transition-speed) ease,
+                color var(--transition-speed),
+                background-color var(--transition-speed),
+                border-color var(--transition-speed);
+}
+
+.sidebar-toggle-floating:hover {
+    color: var(--color-text);
+    border-color: var(--color-active, #ff9103);
+    background-color: var(--color-background);
+}
+
+.container.sidebar-collapsed .sidebar-toggle-floating {
+    left: 14px;
+    color: #ff9103;
+    border-color: rgba(255, 145, 3, 0.5);
+}
+
+body.dark .sidebar-toggle-floating {
+    background: var(--color-dark-sidebar);
+    border-color: var(--color-dark-border);
+    color: var(--color-dark-text-secondary);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+}
+
+body.dark .sidebar-toggle-floating:hover {
+    color: var(--color-dark-text);
+}
+
+.theme-toggle-floating {
+    position: fixed;
+    top: 14px;
+    right: 14px;
+    z-index: 50;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 32px;
+    height: 32px;
+    background: var(--color-background);
+    border: 1px solid var(--color-border);
+    border-radius: 8px;
+    cursor: pointer;
+    color: var(--color-text-secondary);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+    transition: color var(--transition-speed),
+                border-color var(--transition-speed),
+                transform var(--transition-speed);
+}
+
+.theme-toggle-floating:hover {
+    color: #ff9103;
+    border-color: rgba(255, 145, 3, 0.5);
+    transform: rotate(15deg);
+}
+
+body.dark .theme-toggle-floating {
+    background: var(--color-dark-sidebar);
+    border-color: var(--color-dark-border);
+    color: var(--color-dark-text-secondary);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+}
+
+/* Hero landing v2 - Iggy brand aesthetic. Orange #ff9103 accent, mono numerics,
+   clean dark/light surfaces with subtle dot-grid bg and radial glow. */
+.hero-v2 {
+    --hero-accent: #ff9103;
+    --hero-accent-soft: rgba(255, 145, 3, 0.14);
+    --hero-accent-dim: rgba(255, 145, 3, 0.4);
+    --hero-surface: rgba(255, 255, 255, 0.75);
+    --hero-border: rgba(0, 0, 0, 0.08);
+    --hero-grid-line: rgba(0, 0, 0, 0.05);
+    --hero-muted: #6a7280;
+    --hero-text: #0b1220;
+    --hero-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    flex: 1;
+    min-height: 0;
+    padding: clamp(28px, 4vw, 64px) clamp(20px, 4vw, 72px);
+    overflow: auto;
+    color: var(--hero-text);
+}
+
+body.dark .hero-v2 {
+    --hero-surface: rgba(255, 255, 255, 0.03);
+    --hero-border: rgba(255, 255, 255, 0.06);
+    --hero-grid-line: rgba(255, 255, 255, 0.05);
+    --hero-muted: #9ca3af;
+    --hero-text: #f5f5f5;
+}
+
+.hero-v2-inner {
+    position: relative;
+    z-index: 1;
+    display: flex;
+    flex-direction: column;
+    gap: clamp(24px, 3vw, 48px);
+    width: 100%;
+    max-width: 1320px;
+    margin: 0 auto;
+}
+
+.hero-v2-bg {
+    position: absolute;
+    inset: 0;
+    pointer-events: none;
+    overflow: hidden;
+    z-index: 0;
+}
+
+.hero-v2-bg-dot-grid {
+    position: absolute;
+    inset: 0;
+    background-image: radial-gradient(var(--hero-grid-line) 1px, transparent 1px);
+    background-size: 22px 22px;
+    mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, black 40%, transparent 100%);
+    -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, black 40%, transparent 100%);
+}
+
+.hero-v2-bg-glow {
+    position: absolute;
+    border-radius: 50%;
+    filter: blur(80px);
+    opacity: 0.55;
+}
+
+.hero-v2-bg-glow-primary {
+    top: -10%;
+    left: -10%;
+    width: 60%;
+    height: 60%;
+    background: radial-gradient(circle, rgba(255, 145, 3, 0.22), transparent 70%);
+}
+
+.hero-v2-bg-glow-secondary {
+    bottom: -20%;
+    right: -10%;
+    width: 55%;
+    height: 55%;
+    background: radial-gradient(circle, rgba(56, 189, 248, 0.14), transparent 70%);
+}
+
+.hero-v2-empty {
+    justify-content: center;
+    align-items: center;
+    text-align: center;
+}
+
+.hero-v2-empty-inner {
+    position: relative;
+    z-index: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 12px;
+    max-width: 520px;
+}
+
+.hero-v2-empty-title {
+    margin: 0;
+    font-size: clamp(44px, 7vw, 96px);
+    font-weight: 800;
+    letter-spacing: -0.04em;
+    line-height: 1;
+    color: var(--hero-text);
+}
+
+.hero-v2-empty-sub {
+    margin: 0;
+    color: var(--hero-muted);
+    font-size: var(--font-size-md);
+    line-height: 1.5;
+}
+
+.hero-v2-headline {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    animation: hero-fade-up 700ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
+}
+
+.hero-v2-eyebrow {
+    font-family: var(--hero-mono);
+    font-size: 12px;
+    text-transform: uppercase;
+    letter-spacing: 0.18em;
+    color: var(--hero-accent);
+    font-weight: 600;
+}
+
+.hero-v2-title {
+    display: flex;
+    align-items: baseline;
+    gap: 16px;
+    margin: 0;
+    line-height: 0.95;
+    letter-spacing: -0.04em;
+    font-weight: 800;
+    color: var(--hero-text);
+}
+
+.hero-v2-big {
+    font-size: clamp(64px, 10vw, 160px);
+    font-variant-numeric: tabular-nums;
+    background: linear-gradient(180deg, var(--hero-text) 0%, var(--hero-text) 55%, var(--hero-accent) 100%);
+    -webkit-background-clip: text;
+    background-clip: text;
+    -webkit-text-fill-color: transparent;
+    color: transparent;
+}
+
+.hero-v2-unit {
+    font-family: var(--hero-mono);
+    font-size: clamp(20px, 2.4vw, 36px);
+    color: var(--hero-accent);
+    font-weight: 600;
+    letter-spacing: -0.01em;
+}
+
+.hero-v2-sub {
+    margin: 0;
+    color: var(--hero-muted);
+    font-family: var(--hero-mono);
+    font-size: 13px;
+    letter-spacing: 0.02em;
+}
+
+.hero-v2-tagline {
+    margin: 18px 0 0;
+    font-size: clamp(18px, 1.7vw, 24px);
+    font-weight: 500;
+    line-height: 1.35;
+    color: var(--hero-text);
+    letter-spacing: -0.015em;
+    max-width: 720px;
+}
+
+.hero-v2-tagline-accent {
+    background: linear-gradient(90deg, var(--hero-accent) 0%, #ffb347 100%);
+    -webkit-background-clip: text;
+    background-clip: text;
+    -webkit-text-fill-color: transparent;
+    color: transparent;
+    font-weight: 700;
+    letter-spacing: -0.01em;
+}
+
+.hero-v2-cards {
+    display: grid;
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+    gap: 16px;
+}
+
+@media (max-width: 920px) {
+    .hero-v2-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+
+.hero-v2-card {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    padding: 20px 22px;
+    border: 1px solid var(--hero-border);
+    border-radius: 14px;
+    background: var(--hero-surface);
+    backdrop-filter: blur(12px);
+    -webkit-backdrop-filter: blur(12px);
+    transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
+                border-color 300ms ease;
+    animation: hero-fade-up 600ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
+    animation-delay: calc(200ms + var(--stagger, 0) * 80ms);
+    position: relative;
+    overflow: hidden;
+}
+
+.hero-v2-card::before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    height: 1px;
+    background: linear-gradient(90deg, transparent, var(--hero-accent-dim), transparent);
+    opacity: 0;
+    transition: opacity 300ms ease;
+}
+
+.hero-v2-card:hover {
+    transform: translateY(-2px);
+    border-color: var(--hero-accent-dim);
+}
+
+.hero-v2-card:hover::before {
+    opacity: 1;
+}
+
+.hero-v2-card-value-row {
+    display: flex;
+    align-items: baseline;
+    gap: 6px;
+}
+
+.hero-v2-card-value {
+    font-family: var(--hero-mono);
+    font-size: 32px;
+    font-weight: 700;
+    color: var(--hero-text);
+    line-height: 1;
+    letter-spacing: -0.02em;
+}
+
+.hero-v2-card-unit {
+    font-family: var(--hero-mono);
+    font-size: 13px;
+    font-weight: 500;
+    color: var(--hero-muted);
+}
+
+.hero-v2-card-label {
+    font-size: 12px;
+    color: var(--hero-muted);
+    font-weight: 500;
+}
+
+.hero-v2-card-sub {
+    font-family: var(--hero-mono);
+    font-size: 11px;
+    color: var(--hero-muted);
+    opacity: 0.8;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.hero-v2-card-accent {
+    border-color: var(--hero-accent-dim);
+    background: linear-gradient(180deg, var(--hero-accent-soft), var(--hero-surface));
+}
+
+.hero-v2-card-accent .hero-v2-card-value { color: var(--hero-accent); }
+
+.hero-v2-card-dim { opacity: 0.55; }
+
+/* Tail latency chart - reusable component.
+   Used standalone in Single view (Tail measurement) and inside Hero. */
+.tail-chart-wrap {
+    --tail-accent: #ff9103;
+    --tail-accent-dim: rgba(255, 145, 3, 0.4);
+    --tail-border: var(--color-border);
+    --tail-surface: var(--color-background);
+    --tail-muted: var(--color-text-secondary);
+    --tail-grid: rgba(0, 0, 0, 0.06);
+    --tail-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+
+    display: flex;
+    flex-direction: column;
+    border: 1px solid var(--tail-border);
+    border-radius: 16px;
+    background: var(--tail-surface);
+    overflow: hidden;
+    animation: hero-fade-up 800ms cubic-bezier(0.2, 0.8, 0.2, 1) 300ms both;
+}
+
+body.dark .tail-chart-wrap {
+    --tail-border: rgba(255, 255, 255, 0.08);
+    --tail-surface: rgba(255, 255, 255, 0.03);
+    --tail-grid: rgba(255, 255, 255, 0.05);
+    --tail-muted: #9ca3af;
+}
+
+.hero-v2 .tail-chart-wrap {
+    --tail-border: var(--hero-border);
+    --tail-surface: var(--hero-surface);
+    --tail-muted: var(--hero-muted);
+    --tail-grid: var(--hero-grid-line);
+    backdrop-filter: blur(12px);
+    -webkit-backdrop-filter: blur(12px);
+}
+
+.tail-chart-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 14px 20px;
+    border-bottom: 1px solid var(--tail-border);
+    gap: var(--spacing-md);
+}
+
+.tail-chart-legend {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.tail-chart-legend-dot {
+    display: inline-block;
+    width: 18px;
+    height: 3px;
+    border-radius: 2px;
+    background-color: var(--tail-accent);
+}
+
+.tail-chart-legend-label {
+    font-family: var(--tail-mono);
+    font-size: 12px;
+    color: var(--tail-muted);
+}
+
+.tail-chart-header-right {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    min-width: 0;
+}
+
+.tail-chart-subject {
+    font-family: var(--tail-mono);
+    font-size: 12px;
+    color: var(--tail-muted);
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    max-width: 320px;
+}
+
+.tail-chart-details-btn {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    padding: 6px 12px;
+    border: 1px solid var(--tail-accent-dim);
+    border-radius: 8px;
+    background: transparent;
+    color: var(--tail-accent);
+    font-family: var(--tail-mono);
+    font-size: 12px;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 200ms ease;
+    white-space: nowrap;
+}
+
+.tail-chart-details-btn:hover {
+    background: var(--tail-accent);
+    color: #0b1220;
+    border-color: var(--tail-accent);
+    transform: translateX(2px);
+}
+
+.tail-chart {
+    display: block;
+    width: 100%;
+    height: auto;
+    aspect-ratio: 1000 / 340;
+    max-height: 480px;
+    padding: 12px 8px 4px;
+}
+
+.tail-chart-wrap-compact .tail-chart {
+    max-height: 340px;
+}
+
+.tail-chart-line {
+    stroke-dasharray: 2400;
+    stroke-dashoffset: 2400;
+    animation: hero-tail-draw 1.8s cubic-bezier(0.25, 0.1, 0.25, 1) forwards 200ms;
+}
+
+.tail-chart-area { animation: hero-fade-in 800ms ease both 1.4s; }
+
+.tail-chart-point-dot {
+    fill: var(--tail-accent);
+    stroke: var(--tail-surface);
+    stroke-width: 2;
+    transform-origin: center;
+    animation: hero-point-pop 400ms cubic-bezier(0.2, 1.4, 0.3, 1) both;
+    animation-delay: calc(1.5s + var(--stagger, 0) * 100ms);
+    transition: r 150ms ease;
+    cursor: pointer;
+}
+
+.tail-chart-point-halo {
+    animation: tail-halo-pulse 1200ms ease-in-out infinite;
+    transform-origin: center;
+}
+
+.tail-chart-point.active .tail-chart-point-dot {
+    fill: #fff;
+    stroke: var(--tail-accent);
+    stroke-width: 3;
+}
+
+.tail-chart-crosshair line {
+    stroke: var(--tail-accent);
+    stroke-width: 1;
+    stroke-dasharray: 3 4;
+    stroke-opacity: 0.5;
+    pointer-events: none;
+}
+
+.tail-chart-tooltip {
+    animation: hero-fade-in 120ms ease forwards;
+}
+
+.tail-chart-tooltip-bg {
+    fill: var(--tail-surface);
+    stroke: var(--tail-accent);
+    stroke-opacity: 0.5;
+    stroke-width: 1;
+    filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.25));
+}
+
+body.dark .tail-chart-tooltip-bg {
+    fill: #0b1220;
+    stroke-opacity: 0.7;
+}
+
+.tail-chart-tooltip-label {
+    fill: var(--tail-accent);
+    font-family: var(--tail-mono);
+    font-size: 10px;
+    font-weight: 700;
+    letter-spacing: 0.1em;
+    text-transform: uppercase;
+}
+
+.tail-chart-tooltip-value {
+    fill: var(--color-text);
+    font-family: var(--tail-mono);
+    font-size: 12px;
+    font-weight: 700;
+}
+
+.tail-chart-axis-label-active {
+    fill: var(--tail-accent) !important;
+    font-weight: 700 !important;
+}
+
+.tail-chart-legend-hint {
+    font-family: var(--tail-mono);
+    font-size: 10px;
+    color: var(--tail-muted);
+    opacity: 0.6;
+    margin-left: 8px;
+}
+
+@keyframes tail-halo-pulse {
+    0%, 100% { opacity: 0.5; r: 9; }
+    50% { opacity: 0.2; r: 14; }
+}
+
+.tail-chart-grid line { stroke: var(--tail-grid); stroke-width: 1; }
+
+.tail-chart-grid text {
+    fill: var(--tail-muted);
+    font-family: var(--tail-mono);
+    font-size: 9px;
+}
+
+.tail-chart-axis-labels text {
+    fill: var(--tail-muted);
+    font-family: var(--tail-mono);
+    font-size: 10px;
+    font-weight: 500;
+}
+
+@keyframes hero-tail-draw {
+    from { stroke-dashoffset: 2400; }
+    to   { stroke-dashoffset: 0; }
+}
+
+@keyframes hero-fade-in {
+    from { opacity: 0; }
+    to   { opacity: 1; }
+}
+
+@keyframes hero-fade-up {
+    from { opacity: 0; transform: translateY(16px); }
+    to   { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes hero-point-pop {
+    from { opacity: 0; transform: scale(0); }
+    to   { opacity: 1; transform: scale(1); }
+}
+
+/* Pin for compare button on benchmark list items */
+.benchmark-list-item-title-row {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: var(--spacing-sm);
+}
+
+.compare-pin-btn {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    padding: 4px;
+    background: transparent;
+    border: 1px solid transparent;
+    border-radius: var(--border-radius);
+    color: var(--color-text-secondary);
+    cursor: pointer;
+    transition: all var(--transition-speed);
+    flex-shrink: 0;
+}
+
+.compare-pin-btn:hover {
+    color: var(--color-text);
+    border-color: var(--color-border);
+    background-color: var(--color-background);
+}
+
+.compare-pin-btn.active {
+    color: var(--color-active, #0d6efd);
+    border-color: var(--color-active, #0d6efd);
+}
+
+.benchmark-list-item.pinned {
+    border-left: 3px solid var(--color-active, #0d6efd);
+}
+
+body.dark .compare-pin-btn:hover {
+    background-color: var(--color-dark-input);
+}
+
+/* Compare mode (side-by-side) */
+.compare-wrapper {
+    display: flex;
+    flex-direction: column;
+    gap: var(--spacing-md);
+    min-height: 0;
+    overflow: auto;
+}
+
+.compare-banner {
+    display: flex;
+    align-items: center;
+    gap: var(--spacing-md);
+    padding: var(--spacing-sm) var(--spacing-md);
+    background: linear-gradient(90deg, rgba(13, 110, 253, 0.08), rgba(102, 16, 242, 0.08));
+    border-bottom: 1px solid var(--color-border);
+}
+
+.compare-banner-label {
+    font-weight: 600;
+    color: var(--color-text);
+    flex: 1;
+}
+
+.compare-banner-unpin {
+    padding: 4px 12px;
+    border: 1px solid var(--color-border);
+    border-radius: var(--border-radius);
+    background: transparent;
+    color: var(--color-text);
+    cursor: pointer;
+    font-size: var(--font-size-sm);
+    transition: all var(--transition-speed);
+}
+
+.compare-banner-unpin:hover {
+    border-color: var(--color-active);
+    color: var(--color-active);
+}
+
+.compare-grid {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: var(--spacing-md);
+    padding: 0 var(--spacing-md) var(--spacing-md);
+    min-height: 0;
+    flex: 1;
+}
+
+.compare-pane {
+    display: flex;
+    flex-direction: column;
+    gap: var(--spacing-sm);
+    min-width: 0;
+    min-height: 0;
+    border: 1px solid var(--color-border);
+    border-radius: 12px;
+    padding: var(--spacing-md);
+    background: var(--color-background);
+}
+
+.compare-pane-label {
+    font-size: var(--font-size-sm);
+    text-transform: uppercase;
+    letter-spacing: 0.08em;
+    color: var(--color-text-secondary);
+    font-weight: 600;
+}
+
+body.dark .compare-pane {
+    background: var(--color-dark-sidebar, #1a2234);
+    border-color: var(--color-dark-border);
+}
+
+/* Percentile strip (P50 / P95 / P99 / P99.9 / P99.99) above the main chart.
+   Sized to content so it doesn't span the full content-wrapper width. */
+.percentile-strip {
+    display: inline-flex;
+    align-items: center;
+    gap: 20px;
+    align-self: center;
+    box-sizing: border-box;
+    margin: 6px auto 12px;
+    padding: 10px 18px;
+    border: 1px solid var(--color-border);
+    border-radius: 10px;
+    background: var(--color-background);
+    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+}
+
+.percentile-strip-label {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    gap: 2px;
+    padding-right: 16px;
+    border-right: 1px solid var(--color-border);
+}
+
+.percentile-strip-title {
+    font-size: 11px;
+    text-transform: uppercase;
+    letter-spacing: 0.14em;
+    color: var(--color-text-secondary);
+    font-weight: 600;
+}
+
+.percentile-strip-unit {
+    font-size: 11px;
+    color: var(--color-text-secondary);
+    opacity: 0.6;
+}
+
+.percentile-strip-chips {
+    display: flex;
+    align-items: center;
+    gap: 22px;
+}
+
+.percentile-chip {
+    display: flex;
+    flex-direction: column;
+    gap: 3px;
+    min-width: 90px;
+}
+
+.percentile-chip-head {
+    display: flex;
+    align-items: baseline;
+    justify-content: space-between;
+    gap: 8px;
+}
+
+.percentile-chip-label {
+    font-size: 11px;
+    font-weight: 600;
+    color: var(--color-text-secondary);
+    letter-spacing: 0.06em;
+}
+
+.percentile-chip-value {
+    font-size: 14px;
+    font-weight: 700;
+    color: var(--color-text);
+    font-variant-numeric: tabular-nums;
+}
+
+.percentile-chip-bar {
+    height: 4px;
+    background-color: var(--color-border);
+    border-radius: 2px;
+    overflow: hidden;
+}
+
+.percentile-chip-fill {
+    height: 100%;
+    background: linear-gradient(90deg, #ff9103, #ffb347);
+    border-radius: 2px;
+    transition: width var(--transition-speed);
+}
+
+@media (max-width: 720px) {
+    .percentile-strip-chips { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+}
+
+body.dark .percentile-strip {
+    background: rgba(255, 255, 255, 0.03);
+    border-color: var(--color-dark-border);
+}
+
+body.dark .percentile-strip-label {
+    border-right-color: var(--color-dark-border);
+}
+
+body.dark .percentile-chip-bar {
+    background-color: var(--color-dark-border);
+}
+
+/* Sweep view (throughput + p99 vs one varying axis) */
+.sweep-view {
+    display: flex;
+    flex-direction: column;
+    gap: var(--spacing-sm);
+    padding: var(--spacing-md) var(--spacing-lg);
+    border: 1px solid var(--color-border);
+    border-radius: 12px;
+    background: var(--color-background);
+    margin: var(--spacing-md);
+}
+
+.sweep-view-title {
+    display: flex;
+    align-items: baseline;
+    gap: var(--spacing-sm);
+    font-weight: 600;
+    color: var(--color-text);
+}
+
+.sweep-axis {
+    color: var(--color-active, #0d6efd);
+    font-family: monospace;
+}
+
+.sweep-view-hint {
+    color: var(--color-text-secondary);
+    font-size: var(--font-size-sm);
+    font-weight: 400;
+    margin-left: auto;
+}
+
+.sweep-chart {
+    width: 100%;
+    height: 260px;
+}
+
+.sweep-grid line {
+    stroke: var(--color-border);
+    stroke-width: 1;
+}
+
+.sweep-series-tput {
+    color: #0d6efd;
+}
+
+.sweep-series-p99 {
+    color: #d63384;
+}
+
+.sweep-labels text {
+    fill: var(--color-text-secondary);
+    font-size: 11px;
+    font-family: inherit;
+}
+
+.sweep-axis-label {
+    font-style: italic;
+}
+
+.sweep-y-tput {
+    fill: #0d6efd;
+}
+
+.sweep-y-p99 {
+    fill: #d63384;
+}
+
+.sweep-legend {
+    display: flex;
+    gap: var(--spacing-lg);
+    font-size: var(--font-size-sm);
+}
+
+.sweep-legend-item {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.sweep-legend-item::before {
+    content: "";
+    display: inline-block;
+    width: 14px;
+    height: 2px;
+}
+
+.sweep-legend-tput::before {
+    background-color: #0d6efd;
+}
+
+.sweep-legend-p99::before {
+    background-color: #d63384;
+    border-top: 1px dashed #d63384;
+    background: none;
+    height: 0;
+}
+
+body.dark .sweep-view {
+    background: var(--color-dark-sidebar, #1a2234);
+    border-color: var(--color-dark-border);
+}
diff --git a/core/bench/dashboard/frontend/src/components/app_content.rs b/core/bench/dashboard/frontend/src/components/app_content.rs
index 3cc9ba070..04ea6a8b0 100644
--- a/core/bench/dashboard/frontend/src/components/app_content.rs
+++ b/core/bench/dashboard/frontend/src/components/app_content.rs
@@ -17,7 +17,10 @@
 
 use crate::{
     api,
-    components::layout::{main_content::MainContent, sidebar::Sidebar},
+    components::layout::{
+        main_content::MainContent, sidebar::Sidebar, sidebar_toggle::SidebarToggle,
+        theme_toggle_floating::ThemeToggleFloating,
+    },
     router::AppRoute,
     state::{
         benchmark::{BenchmarkAction, BenchmarkContext, use_benchmark},
@@ -82,14 +85,12 @@ fn use_init_hardware(
 }
 
 fn preferred_hardware_identifier(hw_list: &[BenchmarkHardware]) -> Option<String> {
-    hw_list
+    const PREFERRED: &str = "spetz-amd-rkyv";
+    let preferred = hw_list
         .iter()
-        .filter_map(|hw| {
-            hw.identifier
-                .clone()
-                .filter(|identifier| identifier == "spetz-amd-rkyv")
-        })
-        .next()
+        .filter_map(|hw| hw.identifier.clone())
+        .find(|identifier| identifier == PREFERRED);
+    preferred.or_else(|| hw_list.iter().find_map(|hw| hw.identifier.clone()))
 }
 
 #[hook]
@@ -342,7 +343,9 @@ pub fn app_content() -> Html {
     );
 
     html! {
-        <div class="container">
+        <div class={classes!("container", ui_state.is_sidebar_collapsed.then_some("sidebar-collapsed"))}>
+            <SidebarToggle />
+            <ThemeToggleFloating />
             <Sidebar
                 on_gitref_select={Callback::from(move |gitref: String| {
                     // When gitref is selected, we might also want to navigate to Home with new query params
diff --git a/core/bench/dashboard/frontend/src/components/chart/mod.rs b/core/bench/dashboard/frontend/src/components/chart/mod.rs
index 2ad178971..8a1468df1 100644
--- a/core/bench/dashboard/frontend/src/components/chart/mod.rs
+++ b/core/bench/dashboard/frontend/src/components/chart/mod.rs
@@ -17,6 +17,7 @@
 
 mod plot_trend;
 pub mod single_chart;
+pub mod tail_chart;
 pub mod trend_chart;
 
 use wasm_bindgen::prelude::wasm_bindgen;
diff --git a/core/bench/dashboard/frontend/src/components/chart/single_chart.rs b/core/bench/dashboard/frontend/src/components/chart/single_chart.rs
index 974e19f7e..1b366a0bc 100644
--- a/core/bench/dashboard/frontend/src/components/chart/single_chart.rs
+++ b/core/bench/dashboard/frontend/src/components/chart/single_chart.rs
@@ -105,7 +105,7 @@ pub fn single_chart(props: &SingleChartProps) -> Html {
 
                 if !is_loading {
                     let chart = match measurement_type {
-                        MeasurementType::Latency => {
+                        MeasurementType::Latency | MeasurementType::Tail => {
                             bench_report::create_latency_chart(data, config.is_dark, true)
                         }
                         MeasurementType::Throughput => {
diff --git a/core/bench/dashboard/frontend/src/components/chart/tail_chart.rs b/core/bench/dashboard/frontend/src/components/chart/tail_chart.rs
new file mode 100644
index 000000000..e5e8e2506
--- /dev/null
+++ b/core/bench/dashboard/frontend/src/components/chart/tail_chart.rs
@@ -0,0 +1,420 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use bench_dashboard_shared::BenchmarkReportLight;
+use bench_report::group_metrics_summary::BenchmarkGroupMetricsSummary;
+use yew::prelude::*;
+
+#[derive(Properties, PartialEq)]
+pub struct TailChartProps {
+    pub benchmark: BenchmarkReportLight,
+    #[prop_or(None)]
+    pub on_details: Option<Callback<()>>,
+    #[prop_or_default]
+    pub compact: bool,
+}
+
+#[function_component(TailChart)]
+pub fn tail_chart(props: &TailChartProps) -> Html {
+    let hovered = use_state(|| None::<usize>);
+
+    let Some(summary) = props.benchmark.group_metrics.first().map(|m| &m.summary) else {
+        return html! {};
+    };
+
+    let points = percentile_points(summary);
+    let max_latency = points
+        .iter()
+        .map(|(_, _, latency)| *latency)
+        .fold(0.0_f64, f64::max);
+    if max_latency <= 0.0 {
+        return html! {};
+    }
+    let projected: Vec<Projected> = points
+        .iter()
+        .map(|(label, percentile, latency)| {
+            let (screen_x, screen_y) = project_percentile(*percentile, *latency, max_latency);
+            Projected {
+                x: screen_x,
+                y: screen_y,
+                latency_ms: *latency,
+                label,
+            }
+        })
+        .collect();
+
+    let line_path = polyline(&projected);
+    let area_path = closed_polyline(&projected);
+    let details_button = props.on_details.as_ref().map(|details_callback| {
+        let details_callback = details_callback.clone();
+        html! {
+            <button
+                type="button"
+                class="tail-chart-details-btn"
+                onclick={Callback::from(move |_| details_callback.emit(()))}
+            >
+                {"View details"}
+                <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
+                     fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                    <line x1="5" y1="12" x2="19" y2="12" />
+                    <polyline points="12 5 19 12 12 19" />
+                </svg>
+            </button>
+        }
+    });
+
+    let wrap_classes = classes!(
+        "tail-chart-wrap",
+        props.compact.then_some("tail-chart-wrap-compact")
+    );
+
+    html! {
+        <div class={wrap_classes}>
+            <div class="tail-chart-header">
+                <div class="tail-chart-legend">
+                    <span class="tail-chart-legend-dot" />
+                    <span class="tail-chart-legend-label">{"Latency percentiles"}</span>
+                    <span class="tail-chart-legend-hint">{"hover for details"}</span>
+                </div>
+                <div class="tail-chart-header-right">
+                    <span class="tail-chart-subject" title={props.benchmark.params.pretty_name.clone()}>
+                        { props.benchmark.params.pretty_name.clone() }
+                    </span>
+                    { details_button.unwrap_or_default() }
+                </div>
+            </div>
+            <svg
+                class="tail-chart"
+                viewBox={format!("0 0 {CHART_W} {CHART_H}")}
+                preserveAspectRatio="xMidYMid meet"
+                onmouseleave={ {
+                    let hovered = hovered.clone();
+                    Callback::from(move |_| hovered.set(None))
+                } }
+            >
+                <defs>
+                    <linearGradient id="tail-area-grad" x1="0" y1="0" x2="0" y2="1">
+                        <stop offset="0%" stop-color="#ff9103" stop-opacity="0.32" />
+                        <stop offset="100%" stop-color="#ff9103" stop-opacity="0" />
+                    </linearGradient>
+                    <filter id="tail-line-glow" x="-10%" y="-50%" width="120%" height="200%">
+                        <feGaussianBlur stdDeviation="2.5" result="blur" />
+                        <feMerge>
+                            <feMergeNode in="blur" />
+                            <feMergeNode in="SourceGraphic" />
+                        </feMerge>
+                    </filter>
+                    <filter id="tail-dot-glow" x="-80%" y="-80%" width="260%" height="260%">
+                        <feGaussianBlur stdDeviation="4" result="blur" />
+                        <feMerge>
+                            <feMergeNode in="blur" />
+                            <feMergeNode in="SourceGraphic" />
+                        </feMerge>
+                    </filter>
+                </defs>
+                { render_gridlines(max_latency) }
+                <path class="tail-chart-area" d={area_path} fill="url(#tail-area-grad)" />
+                <path
+                    class="tail-chart-line"
+                    d={line_path}
+                    fill="none"
+                    stroke="#ff9103"
+                    stroke-width="2.5"
+                    stroke-linejoin="round"
+                    stroke-linecap="round"
+                    filter="url(#tail-line-glow)"
+                />
+                { render_crosshair(hovered.as_ref().and_then(|i| projected.get(*i))) }
+                { render_points(&projected, *hovered, hovered.clone()) }
+                { render_axis_labels(&projected, *hovered) }
+                { render_tooltip(hovered.as_ref().and_then(|i| projected.get(*i))) }
+            </svg>
+        </div>
+    }
+}
+
+const CHART_W: f64 = 1000.0;
+const CHART_H: f64 = 340.0;
+const MARGIN_L: f64 = 56.0;
+const MARGIN_R: f64 = 24.0;
+const MARGIN_T: f64 = 24.0;
+const MARGIN_B: f64 = 44.0;
+
+const PERCENTILE_POINTS: [(&str, f64); 6] = [
+    ("P50", 50.0),
+    ("P90", 90.0),
+    ("P95", 95.0),
+    ("P99", 99.0),
+    ("P99.9", 99.9),
+    ("P99.99", 99.99),
+];
+
+struct Projected {
+    x: f64,
+    y: f64,
+    latency_ms: f64,
+    label: &'static str,
+}
+
+fn render_gridlines(max_latency: f64) -> Html {
+    let steps = 4;
+    let items: Vec<_> = (0..=steps)
+        .map(|index| {
+            let fraction = index as f64 / steps as f64;
+            let y = CHART_H - MARGIN_B - fraction * (CHART_H - MARGIN_T - MARGIN_B);
+            let value = fraction * max_latency;
+            html! {
+                <g class="tail-chart-grid">
+                    <line x1={MARGIN_L.to_string()} y1={y.to_string()}
+                          x2={(CHART_W - MARGIN_R).to_string()} y2={y.to_string()} />
+                    <text x={(MARGIN_L - 10.0).to_string()} y={(y + 3.5).to_string()} text-anchor="end">
+                        {format_latency(value)}
+                    </text>
+                </g>
+            }
+        })
+        .collect();
+    html! { <>{for items}</> }
+}
+
+fn render_axis_labels(projected: &[Projected], hovered: Option<usize>) -> Html {
+    html! {
+        <g class="tail-chart-axis-labels">
+            { for projected.iter().enumerate().map(|(index, point)| {
+                let is_active = Some(index) == hovered;
+                html! {
+                    <text
+                        class={if is_active { "tail-chart-axis-label-active" } else { "" }}
+                        x={point.x.to_string()}
+                        y={(CHART_H - MARGIN_B + 22.0).to_string()}
+                        text-anchor="middle"
+                    >
+                        {point.label}
+                    </text>
+                }
+            })}
+        </g>
+    }
+}
+
+fn render_points(
+    projected: &[Projected],
+    hovered: Option<usize>,
+    hovered_handle: UseStateHandle<Option<usize>>,
+) -> Html {
+    html! {
+        <g>
+            { for projected.iter().enumerate().map(|(index, point)| {
+                let is_active = Some(index) == hovered;
+                let on_enter = {
+                    let hovered_handle = hovered_handle.clone();
+                    Callback::from(move |_| hovered_handle.set(Some(index)))
+                };
+                html! {
+                    <g
+                        class={classes!("tail-chart-point", is_active.then_some("active"))}
+                        style={format!("--stagger: {index}")}
+                        onmouseenter={on_enter.clone()}
+                        onmouseover={on_enter}
+                    >
+                        <circle
+                            class="tail-chart-point-outer"
+                            cx={point.x.to_string()}
+                            cy={point.y.to_string()}
+                            r="22"
+                            fill="transparent"
+                            pointer-events="all"
+                        />
+                        {
+                            if is_active {
+                                html! {
+                                    <circle
+                                        class="tail-chart-point-halo"
+                                        cx={point.x.to_string()}
+                                        cy={point.y.to_string()}
+                                        r="9"
+                                        fill="none"
+                                        stroke="#ff9103"
+                                        stroke-width="1.2"
+                                        stroke-opacity="0.5"
+                                    />
+                                }
+                            } else {
+                                html! {}
+                            }
+                        }
+                        <circle
+                            class="tail-chart-point-dot"
+                            cx={point.x.to_string()}
+                            cy={point.y.to_string()}
+                            r={if is_active { "4.5" } else { "3" }}
+                            filter={if is_active { "url(#tail-dot-glow)" } else { "" }}
+                        />
+                    </g>
+                }
+            })}
+        </g>
+    }
+}
+
+fn render_crosshair(hovered: Option<&Projected>) -> Html {
+    let Some(point) = hovered else {
+        return html! {};
+    };
+    html! {
+        <g class="tail-chart-crosshair">
+            <line
+                x1={point.x.to_string()}
+                y1={MARGIN_T.to_string()}
+                x2={point.x.to_string()}
+                y2={(CHART_H - MARGIN_B).to_string()}
+            />
+            <line
+                x1={MARGIN_L.to_string()}
+                y1={point.y.to_string()}
+                x2={(CHART_W - MARGIN_R).to_string()}
+                y2={point.y.to_string()}
+            />
+        </g>
+    }
+}
+
+fn render_tooltip(hovered: Option<&Projected>) -> Html {
+    let Some(point) = hovered else {
+        return html! {};
+    };
+    let box_width = 110.0;
+    let box_height = 46.0;
+    let gap = 12.0;
+
+    let mut box_x = point.x - box_width / 2.0;
+    let mut box_y = point.y - box_height - gap;
+    if box_x < MARGIN_L {
+        box_x = MARGIN_L;
+    }
+    if box_x + box_width > CHART_W - MARGIN_R {
+        box_x = CHART_W - MARGIN_R - box_width;
+    }
+    if box_y < MARGIN_T {
+        box_y = point.y + gap;
+    }
+    let center_x = box_x + box_width / 2.0;
+
+    html! {
+        <g class="tail-chart-tooltip" pointer-events="none">
+            <rect
+                x={box_x.to_string()}
+                y={box_y.to_string()}
+                width={box_width.to_string()}
+                height={box_height.to_string()}
+                rx="8"
+                ry="8"
+                class="tail-chart-tooltip-bg"
+            />
+            <text
+                x={center_x.to_string()}
+                y={(box_y + 18.0).to_string()}
+                text-anchor="middle"
+                class="tail-chart-tooltip-label"
+            >
+                {point.label}
+            </text>
+            <text
+                x={center_x.to_string()}
+                y={(box_y + 34.0).to_string()}
+                text-anchor="middle"
+                class="tail-chart-tooltip-value"
+            >
+                {format!("{} ms", format_latency(point.latency_ms))}
+            </text>
+        </g>
+    }
+}
+
+fn polyline(projected: &[Projected]) -> String {
+    projected
+        .iter()
+        .enumerate()
+        .map(|(index, point)| {
+            let command = if index == 0 { 'M' } else { 'L' };
+            format!("{command}{:.1},{:.1}", point.x, point.y)
+        })
+        .collect()
+}
+
+fn closed_polyline(projected: &[Projected]) -> String {
+    let baseline_y = CHART_H - MARGIN_B;
+    match (projected.first(), projected.last()) {
+        (Some(first), Some(last)) => {
+            format!(
+                "M{:.1},{baseline_y:.1} {} L{:.1},{baseline_y:.1} Z",
+                first.x,
+                polyline(projected),
+                last.x
+            )
+        }
+        _ => String::new(),
+    }
+}
+
+fn percentile_points(summary: &BenchmarkGroupMetricsSummary) -> Vec<(&'static str, f64, f64)> {
+    let values = [
+        summary.average_p50_latency_ms,
+        summary.average_p90_latency_ms,
+        summary.average_p95_latency_ms,
+        summary.average_p99_latency_ms,
+        summary.average_p999_latency_ms,
+        summary.average_p9999_latency_ms,
+    ];
+    PERCENTILE_POINTS
+        .iter()
+        .zip(values.iter())
+        .map(|((label, percentile), latency)| (*label, *percentile, *latency))
+        .collect()
+}
+
+fn project_percentile(percentile: f64, latency_ms: f64, max_latency: f64) -> (f64, f64) {
+    let tail_fraction = (1.0 - percentile / 100.0).max(1e-5);
+    let x_log = -tail_fraction.log10();
+    let x_min = -(1.0 - 0.50_f64).log10();
+    let x_max = -(1.0 - 0.9999_f64).log10();
+    let span = x_max - x_min;
+    let normalized = if span > 0.0 {
+        ((x_log - x_min) / span).clamp(0.0, 1.0)
+    } else {
+        0.0
+    };
+    let screen_x = MARGIN_L + normalized * (CHART_W - MARGIN_L - MARGIN_R);
+
+    let y_fraction = if max_latency > 0.0 {
+        (latency_ms / max_latency).clamp(0.0, 1.0)
+    } else {
+        0.0
+    };
+    let screen_y = CHART_H - MARGIN_B - y_fraction * (CHART_H - MARGIN_T - MARGIN_B);
+    (screen_x, screen_y)
+}
+
+fn format_latency(ms: f64) -> String {
+    if ms >= 10.0 {
+        format!("{ms:.1}")
+    } else if ms >= 1.0 {
+        format!("{ms:.2}")
+    } else {
+        format!("{ms:.3}")
+    }
+}
diff --git a/core/bench/dashboard/frontend/src/components/chart/trend_chart.rs b/core/bench/dashboard/frontend/src/components/chart/trend_chart.rs
index 26b877c1d..f180c70de 100644
--- a/core/bench/dashboard/frontend/src/components/chart/trend_chart.rs
+++ b/core/bench/dashboard/frontend/src/components/chart/trend_chart.rs
@@ -80,9 +80,9 @@ pub fn trend_chart(props: &TrendChartProps) -> Html {
             move |(data, measurement_type, is_dark, size)| {
                 if !data.is_empty() {
                     let plot_type = match measurement_type {
-                        MeasurementType::Latency | MeasurementType::Distribution => {
-                            PlotType::Latency
-                        }
+                        MeasurementType::Latency
+                        | MeasurementType::Distribution
+                        | MeasurementType::Tail => PlotType::Latency,
                         MeasurementType::Throughput => PlotType::Throughput,
                     };
 
diff --git a/core/bench/dashboard/frontend/src/components/embed_modal.rs b/core/bench/dashboard/frontend/src/components/embed_modal.rs
index 9609eaec2..86cb00a6e 100644
--- a/core/bench/dashboard/frontend/src/components/embed_modal.rs
+++ b/core/bench/dashboard/frontend/src/components/embed_modal.rs
@@ -50,6 +50,7 @@ pub fn embed_modal(props: &EmbedModalProps) -> Html {
         MeasurementType::Latency => "latency",
         MeasurementType::Throughput => "throughput",
         MeasurementType::Distribution => "distribution",
+        MeasurementType::Tail => "latency",
     };
 
     let theme = if props.is_dark { "dark" } else { "light" };
diff --git a/core/bench/dashboard/frontend/src/components/layout/hero.rs b/core/bench/dashboard/frontend/src/components/layout/hero.rs
new file mode 100644
index 000000000..bbecb4ead
--- /dev/null
+++ b/core/bench/dashboard/frontend/src/components/layout/hero.rs
@@ -0,0 +1,391 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::components::chart::tail_chart::TailChart;
+use crate::state::benchmark::{BenchmarkAction, pick_best_from_recent_batch, use_benchmark};
+use crate::state::ui::{UiAction, use_ui};
+use bench_dashboard_shared::BenchmarkReportLight;
+use chrono::DateTime;
+use yew::prelude::*;
+
+#[derive(Properties, PartialEq)]
+pub struct HeroProps {
+    pub selected_gitref: String,
+}
+
+#[function_component(Hero)]
+pub fn hero(props: &HeroProps) -> Html {
+    let benchmark_ctx = use_benchmark();
+    let ui = use_ui();
+    let unrestricted: Vec<&BenchmarkReportLight> = benchmark_ctx
+        .state
+        .entries
+        .values()
+        .flatten()
+        .filter(|benchmark| benchmark.params.rate_limit.is_none())
+        .collect();
+    let mut stats = compute_stats(unrestricted.iter().copied());
+    stats.showcase = unrestricted
+        .iter()
+        .copied()
+        .max_by(|left, right| {
+            throughput_mb(left)
+                .partial_cmp(&throughput_mb(right))
+                .unwrap_or(std::cmp::Ordering::Equal)
+        })
+        .cloned();
+    if stats.showcase.is_none() {
+        let all_entries: Vec<&BenchmarkReportLight> =
+            benchmark_ctx.state.entries.values().flatten().collect();
+        stats.showcase = pick_best_from_recent_batch(&all_entries);
+    }
+
+    if stats.total == 0 {
+        return html! {
+            <div class="hero-v2 hero-v2-empty">
+                { render_background_grid() }
+                <div class="hero-v2-empty-inner">
+                    <div class="hero-v2-eyebrow">{"Apache Iggy"}</div>
+                    <h1 class="hero-v2-empty-title">{"Benchmarks"}</h1>
+                    <p class="hero-v2-empty-sub">
+                        {"Pick a hardware and gitref in the sidebar to view performance data."}
+                    </p>
+                </div>
+            </div>
+        };
+    }
+
+    let hardware = benchmark_ctx
+        .state
+        .current_hardware
+        .clone()
+        .unwrap_or_default();
+    let gitref_suffix = if props.selected_gitref.is_empty() {
+        String::new()
+    } else {
+        format!(" @ {}", props.selected_gitref)
+    };
+
+    let on_view_details = stats.showcase.as_ref().map(|showcase| {
+        let benchmark = showcase.clone();
+        let ui = ui.clone();
+        let dispatch = benchmark_ctx.dispatch.clone();
+        Callback::from(move |_| {
+            ui.dispatch(UiAction::SetLanding(false));
+            dispatch.emit(BenchmarkAction::SelectBenchmark(Box::new(Some(
+                benchmark.clone(),
+            ))));
+        })
+    });
+
+    html! {
+        <div class="hero-v2">
+            { render_background_grid() }
+            <div class="hero-v2-inner">
+                { render_headline(&stats, &hardware, &gitref_suffix) }
+                { render_stat_cards(&stats) }
+                {
+                    match (stats.showcase.as_ref(), on_view_details) {
+                        (Some(showcase), Some(details_callback)) => html! {
+                            <TailChart
+                                benchmark={showcase.clone()}
+                                on_details={details_callback}
+                            />
+                        },
+                        _ => html! {},
+                    }
+                }
+            </div>
+        </div>
+    }
+}
+
+#[derive(Default)]
+struct HeroStats {
+    peak_mb_s: Option<(f64, String)>,
+    peak_msg_s: Option<(f64, String)>,
+    max_scale: Option<MaxScale>,
+    total: usize,
+    latest_ts: Option<String>,
+    showcase: Option<BenchmarkReportLight>,
+}
+
+struct MaxScale {
+    producers: u32,
+    consumers: u32,
+    pretty_name: String,
+}
+
+impl MaxScale {
+    fn total_actors(&self) -> u32 {
+        self.producers + self.consumers
+    }
+}
+
+fn compute_stats<'a>(benchmarks: impl Iterator<Item = &'a BenchmarkReportLight>) -> HeroStats {
+    let mut stats = HeroStats::default();
+
+    for benchmark in benchmarks {
+        stats.total += 1;
+        let Some(summary) = benchmark
+            .group_metrics
+            .first()
+            .map(|metrics| &metrics.summary)
+        else {
+            continue;
+        };
+        let throughput_megabytes = summary.total_throughput_megabytes_per_second;
+        let throughput_messages = summary.total_throughput_messages_per_second;
+        let pretty_name = benchmark.params.pretty_name.clone();
+
+        if stats
+            .peak_mb_s
+            .as_ref()
+            .is_none_or(|(current, _)| throughput_megabytes > *current)
+        {
+            stats.peak_mb_s = Some((throughput_megabytes, pretty_name.clone()));
+        }
+        if stats
+            .peak_msg_s
+            .as_ref()
+            .is_none_or(|(current, _)| throughput_messages > *current)
+        {
+            stats.peak_msg_s = Some((throughput_messages, pretty_name.clone()));
+        }
+        let producers = benchmark.params.producers;
+        let consumers = benchmark.params.consumers;
+        let total_actors = producers + consumers;
+        if stats
+            .max_scale
+            .as_ref()
+            .is_none_or(|current| total_actors > current.total_actors())
+        {
+            stats.max_scale = Some(MaxScale {
+                producers,
+                consumers,
+                pretty_name: pretty_name.clone(),
+            });
+        }
+        if stats
+            .latest_ts
+            .as_ref()
+            .is_none_or(|current| &benchmark.timestamp > current)
+        {
+            stats.latest_ts = Some(benchmark.timestamp.clone());
+        }
+    }
+    stats
+}
+
+fn render_background_grid() -> Html {
+    html! {
+        <div class="hero-v2-bg" aria-hidden="true">
+            <div class="hero-v2-bg-dot-grid" />
+            <div class="hero-v2-bg-glow hero-v2-bg-glow-primary" />
+            <div class="hero-v2-bg-glow hero-v2-bg-glow-secondary" />
+        </div>
+    }
+}
+
+fn render_headline(stats: &HeroStats, hardware: &str, gitref_suffix: &str) -> Html {
+    let (value, unit, subject) = match &stats.peak_mb_s {
+        Some((throughput, name)) => {
+            let (formatted, unit) = format_throughput_bytes(*throughput);
+            (formatted, unit, name.clone())
+        }
+        None => ("-".to_string(), "MB/s", String::new()),
+    };
+    let sub = if subject.is_empty() {
+        format!("{hardware}{gitref_suffix}")
+    } else {
+        format!("{subject} · {hardware}{gitref_suffix}")
+    };
+
+    html! {
+        <div class="hero-v2-headline">
+            <div class="hero-v2-eyebrow">{"Peak sustained throughput"}</div>
+            <h1 class="hero-v2-title">
+                <span class="hero-v2-big">{value}</span>
+                <span class="hero-v2-unit">{unit}</span>
+            </h1>
+            <p class="hero-v2-sub">{sub}</p>
+            <p class="hero-v2-tagline">
+                {"Modern hardware is incredibly capable. "}
+                <span class="hero-v2-tagline-accent">{"Iggy was built for it."}</span>
+            </p>
+        </div>
+    }
+}
+
+fn render_stat_cards(stats: &HeroStats) -> Html {
+    html! {
+        <div class="hero-v2-cards">
+            {
+                render_stat_card(0, "Peak throughput", stats.peak_msg_s.as_ref().map(|(rate, name)| {
+                    let (formatted, unit) = format_msg_rate(*rate);
+                    (formatted, unit, name.clone())
+                }))
+            }
+            { render_scale_card(1, stats.max_scale.as_ref()) }
+            { render_showcase_card(2, stats.showcase.as_ref()) }
+            { render_summary_card(3, stats.total, stats.latest_ts.as_deref()) }
+        </div>
+    }
+}
+
+fn render_stat_card(
+    stagger: usize,
+    label: &'static str,
+    value: Option<(String, &'static str, String)>,
+) -> Html {
+    let Some((formatted, unit, name)) = value else {
+        return html! {};
+    };
+    html! {
+        <div class="hero-v2-card" style={format!("--stagger: {stagger}")}>
+            <div class="hero-v2-card-value-row">
+                <span class="hero-v2-card-value">{formatted}</span>
+                <span class="hero-v2-card-unit">{unit}</span>
+            </div>
+            <div class="hero-v2-card-label">{label}</div>
+            <div class="hero-v2-card-sub" title={name.clone()}>{name}</div>
+        </div>
+    }
+}
+
+fn render_scale_card(stagger: usize, scale: Option<&MaxScale>) -> Html {
+    let Some(scale) = scale else {
+        return html! {};
+    };
+    let total = scale.total_actors();
+    let breakdown = if scale.producers > 0 && scale.consumers > 0 {
+        format!(
+            "{} producers × {} consumers",
+            scale.producers, scale.consumers
+        )
+    } else if scale.producers > 0 {
+        format!("{} producers", scale.producers)
+    } else {
+        format!("{} consumers", scale.consumers)
+    };
+    html! {
+        <div class="hero-v2-card" style={format!("--stagger: {stagger}")}>
+            <div class="hero-v2-card-value-row">
+                <span class="hero-v2-card-value">{total}</span>
+                <span class="hero-v2-card-unit">{"actors"}</span>
+            </div>
+            <div class="hero-v2-card-label">{"Max scale tested"}</div>
+            <div class="hero-v2-card-sub" title={scale.pretty_name.clone()}>{breakdown}</div>
+        </div>
+    }
+}
+
+fn render_showcase_card(stagger: usize, showcase: Option<&BenchmarkReportLight>) -> Html {
+    let Some(benchmark) = showcase else {
+        return html! {};
+    };
+    let Some(summary) = benchmark.group_metrics.first().map(|m| &m.summary) else {
+        return html! {};
+    };
+    let p99 = summary.average_p99_latency_ms;
+    let name = benchmark.params.pretty_name.clone();
+
+    html! {
+        <div class="hero-v2-card hero-v2-card-accent" style={format!("--stagger: {stagger}")}>
+            <div class="hero-v2-card-value-row">
+                <span class="hero-v2-card-value">{format_latency(p99)}</span>
+                <span class="hero-v2-card-unit">{"ms"}</span>
+            </div>
+            <div class="hero-v2-card-label">{"P99 at peak throughput"}</div>
+            <div class="hero-v2-card-sub" title={name.clone()}>{name}</div>
+        </div>
+    }
+}
+
+fn throughput_mb(benchmark: &BenchmarkReportLight) -> f64 {
+    benchmark
+        .group_metrics
+        .first()
+        .map(|metrics| metrics.summary.total_throughput_megabytes_per_second)
+        .unwrap_or(0.0)
+}
+
+fn render_summary_card(stagger: usize, total: usize, latest_ts: Option<&str>) -> Html {
+    let sub = match latest_ts {
+        Some(ts) => format!("Latest: {}", format_date(ts)),
+        None => String::new(),
+    };
+    html! {
+        <div class="hero-v2-card" style={format!("--stagger: {stagger}")}>
+            <div class="hero-v2-card-value-row">
+                <span class="hero-v2-card-value">{total}</span>
+                <span class="hero-v2-card-unit">{"runs"}</span>
+            </div>
+            <div class="hero-v2-card-label">{"Benchmarks loaded"}</div>
+            <div class="hero-v2-card-sub">{sub}</div>
+        </div>
+    }
+}
+
+fn format_throughput_bytes(mb_per_s: f64) -> (String, &'static str) {
+    if mb_per_s >= 1_000_000.0 {
+        (format_significant(mb_per_s / 1_000_000.0), "TB/s")
+    } else if mb_per_s >= 1_000.0 {
+        (format_significant(mb_per_s / 1_000.0), "GB/s")
+    } else {
+        (format_significant(mb_per_s), "MB/s")
+    }
+}
+
+fn format_msg_rate(rate: f64) -> (String, &'static str) {
+    if rate >= 1_000_000_000.0 {
+        (format_significant(rate / 1_000_000_000.0), "B msg/s")
+    } else if rate >= 1_000_000.0 {
+        (format_significant(rate / 1_000_000.0), "M msg/s")
+    } else if rate >= 1_000.0 {
+        (format_significant(rate / 1_000.0), "k msg/s")
+    } else {
+        (format!("{rate:.0}"), "msg/s")
+    }
+}
+
+fn format_significant(v: f64) -> String {
+    if v >= 100.0 {
+        format!("{v:.0}")
+    } else if v >= 10.0 {
+        format!("{v:.1}")
+    } else {
+        format!("{v:.2}")
+    }
+}
+
+fn format_latency(ms: f64) -> String {
+    if ms >= 10.0 {
+        format!("{ms:.1}")
+    } else if ms >= 1.0 {
+        format!("{ms:.2}")
+    } else {
+        format!("{ms:.3}")
+    }
+}
+
+fn format_date(timestamp_str: &str) -> String {
+    match DateTime::parse_from_rfc3339(timestamp_str) {
+        Ok(t) => t.format("%Y-%m-%d").to_string(),
+        Err(_) => "unknown".to_string(),
+    }
+}
diff --git a/core/bench/dashboard/frontend/src/components/layout/logo.rs b/core/bench/dashboard/frontend/src/components/layout/logo.rs
index 876acb1e1..7c0f64d4d 100644
--- a/core/bench/dashboard/frontend/src/components/layout/logo.rs
+++ b/core/bench/dashboard/frontend/src/components/layout/logo.rs
@@ -15,21 +15,46 @@
 // specific language governing permissions and limitations
 // under the License.
 
+use crate::router::AppRoute;
+use crate::state::benchmark::{BenchmarkAction, use_benchmark};
+use crate::state::ui::{UiAction, ViewMode, use_ui};
 use yew::prelude::*;
+use yew_router::prelude::use_navigator;
 
 #[function_component(Logo)]
 pub fn logo() -> Html {
     let (is_dark, _) = use_context::<(bool, Callback<()>)>().expect("Theme context not found");
-    let logo_src = if !is_dark {
-        "/assets/iggy-dark.png"
-    } else {
+    let ui = use_ui();
+    let benchmark_ctx = use_benchmark();
+    let navigator = use_navigator();
+    let logo_src = if is_dark {
         "/assets/iggy-light.png"
+    } else {
+        "/assets/iggy-dark.png"
+    };
+
+    let on_click = {
+        let ui = ui.clone();
+        let benchmark_ctx = benchmark_ctx.clone();
+        let navigator = navigator.clone();
+        Callback::from(move |_| {
+            benchmark_ctx
+                .dispatch
+                .emit(BenchmarkAction::SelectBenchmark(Box::new(None)));
+            ui.dispatch(UiAction::SetComparePin(None));
+            ui.dispatch(UiAction::SetViewMode(ViewMode::SingleGitref));
+            ui.dispatch(UiAction::SetLanding(true));
+            if let Some(nav) = navigator.as_ref() {
+                nav.push(&AppRoute::Home);
+            }
+        })
     };
 
     html! {
-        <div class="logo">
+        <button type="button" class="logo logo-home-button" onclick={on_click}
+                title="Back to overview">
             <img src={logo_src} alt="Apache Iggy Logo" />
             <h1>{"Apache Iggy Benchmarks"}</h1>
-        </div>
+        </button>
     }
 }
diff --git a/core/bench/dashboard/frontend/src/components/layout/main_content.rs b/core/bench/dashboard/frontend/src/components/layout/main_content.rs
index 56f53324e..ff8140299 100644
--- a/core/bench/dashboard/frontend/src/components/layout/main_content.rs
+++ b/core/bench/dashboard/frontend/src/components/layout/main_content.rs
@@ -16,9 +16,15 @@
 // under the License.
 
 use crate::components::chart::single_chart::SingleChart;
+use crate::components::chart::tail_chart::TailChart;
+use crate::components::layout::hero::Hero;
+use crate::components::layout::percentile_card::PercentileCard;
+use crate::components::layout::sweep_view::SweepView;
 use crate::components::layout::topbar::TopBar;
+use crate::components::selectors::measurement_type_selector::MeasurementType;
 use crate::state::benchmark::use_benchmark;
-use crate::state::ui::{ViewMode, use_ui};
+use crate::state::ui::{UiAction, ViewMode, use_ui};
+use bench_dashboard_shared::BenchmarkReportLight;
 use yew::prelude::*;
 
 #[derive(Properties, PartialEq)]
@@ -33,77 +39,175 @@ pub struct MainContentProps {
 pub fn main_content(props: &MainContentProps) -> Html {
     let benchmark_ctx = use_benchmark();
     let ui = use_ui();
-    let selected_measurement = ui.selected_measurement.clone();
-
     let is_recent_view = matches!(props.view_mode, ViewMode::RecentBenchmarks);
+    let selected = benchmark_ctx.state.selected_benchmark.clone();
+    let pinned = ui.compare_pin.as_deref().cloned();
+    let entries = benchmark_ctx.state.entries.clone();
 
-    let content = if let Some(selected_benchmark) = &benchmark_ctx.state.selected_benchmark {
-        html! {
-            <div class="content-wrapper">
-                <div class="chart-title">
-                    <div class="chart-title-primary">
-                        { selected_benchmark.title(&selected_measurement.to_string()) }
-                    </div>
-                    <div class="chart-title-sub">
-                        { selected_benchmark.subtext() }
-                    </div>
-                    <div class="chart-title-identifier">
-                        { selected_benchmark.identifier_with_cpu_and_version() }
-                    </div>
-                </div>
-                <div class="single-view">
-                    <SingleChart
-                        benchmark_uuid={selected_benchmark.uuid}
-                        measurement_type={selected_measurement.clone()}
-                        is_dark={props.is_dark}
-                    />
-                </div>
-            </div>
-        }
-    } else if is_recent_view {
+    let on_unpin = {
+        let ui = ui.clone();
+        Callback::from(move |_: MouseEvent| ui.dispatch(UiAction::SetComparePin(None)))
+    };
+
+    let show_hero = ui.is_landing && !is_recent_view;
+    let content = if show_hero {
         html! {
             <div class="content-wrapper">
-                <div class="empty-state">
-                    <div class="empty-state-content">
-                        <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-                            <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
-                            <polyline points="13 2 13 9 20 9"/>
-                            <line x1="16" y1="13" x2="8" y2="13"/>
-                            <line x1="16" y1="17" x2="8" y2="17"/>
-                        </svg>
-                        <h2>{"Select a recent benchmark"}</h2>
-                        <p>{"Choose a benchmark from the sidebar to display performance data."}</p>
-                    </div>
-                </div>
+                <Hero selected_gitref={props.selected_gitref.clone()} />
             </div>
         }
     } else {
-        html! {
-            <div class="content-wrapper">
-                <div class="empty-state">
-                    <div class="empty-state-content">
-                        <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-                            <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
-                            <polyline points="13 2 13 9 20 9"/>
-                            <line x1="16" y1="13" x2="8" y2="13"/>
-                            <line x1="16" y1="17" x2="8" y2="17"/>
-                        </svg>
-                        <h2>{"Select a benchmark to view results"}</h2>
-                        <p>{"Choose a benchmark from the sidebar to display performance data."}</p>
-                    </div>
+        match (selected.as_ref(), pinned.as_ref()) {
+            (Some(selected_benchmark), Some(pinned_benchmark))
+                if selected_benchmark.uuid != pinned_benchmark.uuid =>
+            {
+                render_compare(
+                    selected_benchmark,
+                    pinned_benchmark,
+                    ui.selected_measurement.clone(),
+                    props.is_dark,
+                    on_unpin,
+                )
+            }
+            (Some(selected_benchmark), _) => render_single(
+                selected_benchmark,
+                ui.selected_measurement.clone(),
+                props.is_dark,
+                &entries,
+            ),
+            (None, _) if is_recent_view => render_empty_recent(),
+            (None, _) => html! {
+                <div class="content-wrapper">
+                    <Hero selected_gitref={props.selected_gitref.clone()} />
                 </div>
-            </div>
+            },
         }
     };
 
     html! {
         <div class="content">
-            <TopBar
-                is_dark={props.is_dark}
-                selected_gitref={props.selected_gitref.clone()}
-                on_theme_toggle={props.on_theme_toggle.clone()}
-            />
+            if !show_hero {
+                <TopBar
+                    is_dark={props.is_dark}
+                    selected_gitref={props.selected_gitref.clone()}
+                    on_theme_toggle={props.on_theme_toggle.clone()}
+                />
+            }
             {content}
         </div>
     }
 }
+
+fn render_single(
+    benchmark: &BenchmarkReportLight,
+    measurement: MeasurementType,
+    is_dark: bool,
+    entries: &std::collections::BTreeMap<
+        bench_report::benchmark_kind::BenchmarkKind,
+        Vec<BenchmarkReportLight>,
+    >,
+) -> Html {
+    html! {
+        <div class="content-wrapper">
+            <div class="chart-title">
+                <div class="chart-title-primary">
+                    { benchmark.title(&measurement.to_string()) }
+                </div>
+                <div class="chart-title-sub">{ benchmark.subtext() }</div>
+                <div class="chart-title-identifier">
+                    { benchmark.identifier_with_cpu_and_version() }
+                </div>
+            </div>
+            <PercentileCard benchmark={benchmark.clone()} />
+            <div class="single-view">
+                { render_measurement_chart(benchmark, measurement, is_dark) }
+            </div>
+            <SweepView benchmark={benchmark.clone()} entries={entries.clone()} is_dark={is_dark} />
+        </div>
+    }
+}
+
+fn render_measurement_chart(
+    benchmark: &BenchmarkReportLight,
+    measurement: MeasurementType,
+    is_dark: bool,
+) -> Html {
+    if measurement == MeasurementType::Tail {
+        return html! { <TailChart benchmark={benchmark.clone()} /> };
+    }
+    html! {
+        <SingleChart
+            benchmark_uuid={benchmark.uuid}
+            measurement_type={measurement}
+            is_dark={is_dark}
+        />
+    }
+}
+
+fn render_compare(
+    selected_benchmark: &BenchmarkReportLight,
+    pinned_benchmark: &BenchmarkReportLight,
+    measurement: MeasurementType,
+    is_dark: bool,
+    on_unpin: Callback<MouseEvent>,
+) -> Html {
+    html! {
+        <div class="content-wrapper compare-wrapper">
+            <div class="compare-banner">
+                <span class="compare-banner-label">{"Compare mode"}</span>
+                <button type="button" class="compare-banner-unpin" onclick={on_unpin}>
+                    {"Unpin"}
+                </button>
+            </div>
+            <div class="compare-grid">
+                { render_compare_pane("A - selected", selected_benchmark, measurement.clone(), is_dark) }
+                { render_compare_pane("B - pinned", pinned_benchmark, measurement, is_dark) }
+            </div>
+        </div>
+    }
+}
+
+fn render_compare_pane(
+    label: &str,
+    benchmark: &BenchmarkReportLight,
+    measurement: MeasurementType,
+    is_dark: bool,
+) -> Html {
+    html! {
+        <div class="compare-pane">
+            <div class="compare-pane-label">{label}</div>
+            <div class="chart-title">
+                <div class="chart-title-primary">
+                    { benchmark.title(&measurement.to_string()) }
+                </div>
+                <div class="chart-title-sub">{ benchmark.subtext() }</div>
+                <div class="chart-title-identifier">
+                    { benchmark.identifier_with_cpu_and_version() }
+                </div>
+            </div>
+            <PercentileCard benchmark={benchmark.clone()} />
+            <div class="single-view">
+                { render_measurement_chart(benchmark, measurement, is_dark) }
+            </div>
+        </div>
+    }
+}
+
+fn render_empty_recent() -> Html {
+    html! {
+        <div class="content-wrapper">
+            <div class="empty-state">
+                <div class="empty-state-content">
+                    <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                        <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
+                        <polyline points="13 2 13 9 20 9"/>
+                        <line x1="16" y1="13" x2="8" y2="13"/>
+                        <line x1="16" y1="17" x2="8" y2="17"/>
+                    </svg>
+                    <h2>{"Select a recent benchmark"}</h2>
+                    <p>{"Choose a benchmark from the sidebar to display performance data."}</p>
+                </div>
+            </div>
+        </div>
+    }
+}
diff --git a/core/bench/dashboard/frontend/src/components/layout/mod.rs b/core/bench/dashboard/frontend/src/components/layout/mod.rs
index 1357be355..a1dd4934d 100644
--- a/core/bench/dashboard/frontend/src/components/layout/mod.rs
+++ b/core/bench/dashboard/frontend/src/components/layout/mod.rs
@@ -15,7 +15,12 @@
 // specific language governing permissions and limitations
 // under the License.
 
+pub mod hero;
 pub mod logo;
 pub mod main_content;
+pub mod percentile_card;
 pub mod sidebar;
+pub mod sidebar_toggle;
+pub mod sweep_view;
+pub mod theme_toggle_floating;
 pub mod topbar;
diff --git a/core/bench/dashboard/frontend/src/components/layout/percentile_card.rs b/core/bench/dashboard/frontend/src/components/layout/percentile_card.rs
new file mode 100644
index 000000000..bab3e8024
--- /dev/null
+++ b/core/bench/dashboard/frontend/src/components/layout/percentile_card.rs
@@ -0,0 +1,93 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use bench_dashboard_shared::BenchmarkReportLight;
+use yew::prelude::*;
+
+#[derive(Properties, PartialEq)]
+pub struct PercentileCardProps {
+    pub benchmark: BenchmarkReportLight,
+}
+
+#[function_component(PercentileCard)]
+pub fn percentile_card(props: &PercentileCardProps) -> Html {
+    let Some(summary) = props
+        .benchmark
+        .group_metrics
+        .first()
+        .map(|m| m.summary.clone())
+    else {
+        return html! {};
+    };
+
+    let rows = [
+        ("P50", summary.average_p50_latency_ms),
+        ("P95", summary.average_p95_latency_ms),
+        ("P99", summary.average_p99_latency_ms),
+        ("P99.9", summary.average_p999_latency_ms),
+        ("P99.99", summary.average_p9999_latency_ms),
+    ];
+    let max_value = rows
+        .iter()
+        .map(|(_, latency)| *latency)
+        .fold(0.0_f64, |acc, v| if v > acc { v } else { acc });
+
+    if max_value <= 0.0 {
+        return html! {};
+    }
+
+    html! {
+        <div class="percentile-strip">
+            <div class="percentile-strip-label">
+                <span class="percentile-strip-title">{"Latency"}</span>
+                <span class="percentile-strip-unit">{"ms"}</span>
+            </div>
+            <div class="percentile-strip-chips">
+                { for rows.iter().map(|(label, value)| render_chip(label, *value, max_value)) }
+            </div>
+        </div>
+    }
+}
+
+fn render_chip(label: &str, value: f64, max_value: f64) -> Html {
+    let percent = if max_value > 0.0 {
+        (value / max_value * 100.0).clamp(0.0, 100.0)
+    } else {
+        0.0
+    };
+    html! {
+        <div class="percentile-chip" title={format!("{label}: {value:.3} ms")}>
+            <div class="percentile-chip-head">
+                <span class="percentile-chip-label">{label}</span>
+                <span class="percentile-chip-value">{format_latency(value)}</span>
+            </div>
+            <div class="percentile-chip-bar">
+                <div class="percentile-chip-fill" style={format!("width: {percent:.1}%")} />
+            </div>
+        </div>
+    }
+}
+
+fn format_latency(ms: f64) -> String {
+    if ms >= 10.0 {
+        format!("{ms:.1}")
+    } else if ms >= 1.0 {
+        format!("{ms:.2}")
+    } else {
+        format!("{ms:.3}")
+    }
+}
diff --git a/core/bench/dashboard/frontend/src/components/layout/sidebar.rs b/core/bench/dashboard/frontend/src/components/layout/sidebar.rs
index ce1fc00db..498a844c0 100644
--- a/core/bench/dashboard/frontend/src/components/layout/sidebar.rs
+++ b/core/bench/dashboard/frontend/src/components/layout/sidebar.rs
@@ -20,6 +20,7 @@ use crate::components::selectors::benchmark_search_box::BenchmarkSearchBox;
 use crate::components::selectors::benchmark_selector::BenchmarkSelector;
 use crate::components::selectors::gitref_selector::GitrefSelector;
 use crate::components::selectors::hardware_selector::HardwareSelector;
+use crate::components::selectors::param_filters_panel::ParamFiltersPanel;
 use crate::components::selectors::recent_benchmarks_selector::RecentBenchmarksSelector;
 use crate::components::selectors::view_mode_selector::ViewModeSelector;
 use crate::state::benchmark::{BenchmarkAction, use_benchmark};
@@ -86,12 +87,14 @@ pub fn sidebar(props: &SidebarProps) -> Html {
         )
     };
 
+    let filters = ui_state.param_filters.clone();
+
     let has_benchmarks = |f: fn(&BenchmarkKind) -> bool| {
-        benchmark_ctx
-            .state
-            .entries
-            .values()
-            .any(|benchmarks| benchmarks.iter().any(|b| f(&b.params.benchmark_kind)))
+        benchmark_ctx.state.entries.values().any(|benchmarks| {
+            benchmarks
+                .iter()
+                .any(|b| f(&b.params.benchmark_kind) && filters.matches(b))
+        })
     };
 
     let count_benchmarks = |f: fn(&BenchmarkKind) -> bool| {
@@ -102,7 +105,7 @@ pub fn sidebar(props: &SidebarProps) -> Html {
             .map(|benchmarks| {
                 benchmarks
                     .iter()
-                    .filter(|b| f(&b.params.benchmark_kind))
+                    .filter(|b| f(&b.params.benchmark_kind) && filters.matches(b))
                     .count()
             })
             .sum::<usize>()
@@ -139,6 +142,7 @@ pub fn sidebar(props: &SidebarProps) -> Html {
             <div class="sidebar-fixed-header">
                 <Logo />
                 <ViewModeSelector />
+                <ParamFiltersPanel />
 
                 if !is_recent_view {
                     <HardwareSelector
diff --git a/core/bench/dashboard/frontend/src/components/layout/sidebar_toggle.rs b/core/bench/dashboard/frontend/src/components/layout/sidebar_toggle.rs
new file mode 100644
index 000000000..27efa732a
--- /dev/null
+++ b/core/bench/dashboard/frontend/src/components/layout/sidebar_toggle.rs
@@ -0,0 +1,57 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::state::ui::{UiAction, use_ui};
+use yew::prelude::*;
+
+#[function_component(SidebarToggle)]
+pub fn sidebar_toggle() -> Html {
+    let ui = use_ui();
+    let is_collapsed = ui.is_sidebar_collapsed;
+
+    let on_click = {
+        let ui = ui.clone();
+        Callback::from(move |_| ui.dispatch(UiAction::ToggleSidebar))
+    };
+
+    let title = if is_collapsed {
+        "Show sidebar"
+    } else {
+        "Hide sidebar"
+    };
+
+    html! {
+        <button
+            type="button"
+            class={classes!("sidebar-toggle-floating", is_collapsed.then_some("collapsed"))}
+            onclick={on_click}
+            title={title}
+            aria-label={title}
+        >
+            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
+                 fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                {
+                    if is_collapsed {
+                        html! { <polyline points="9 18 15 12 9 6" /> }
+                    } else {
+                        html! { <polyline points="15 18 9 12 15 6" /> }
+                    }
+                }
+            </svg>
+        </button>
+    }
+}
diff --git a/core/bench/dashboard/frontend/src/components/layout/sweep_view.rs b/core/bench/dashboard/frontend/src/components/layout/sweep_view.rs
new file mode 100644
index 000000000..5e556d570
--- /dev/null
+++ b/core/bench/dashboard/frontend/src/components/layout/sweep_view.rs
@@ -0,0 +1,290 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use bench_dashboard_shared::BenchmarkReportLight;
+use bench_report::benchmark_kind::BenchmarkKind;
+use bench_report::params::BenchmarkParams;
+use std::collections::BTreeMap;
+use yew::prelude::*;
+
+#[derive(Properties, PartialEq)]
+pub struct SweepViewProps {
+    pub benchmark: BenchmarkReportLight,
+    pub entries: BTreeMap<BenchmarkKind, Vec<BenchmarkReportLight>>,
+    pub is_dark: bool,
+}
+
+#[function_component(SweepView)]
+pub fn sweep_view(props: &SweepViewProps) -> Html {
+    let Some((axis, points)) = find_sweep(&props.benchmark, &props.entries) else {
+        return html! {};
+    };
+
+    let max_throughput = points
+        .iter()
+        .map(|point| point.throughput_mb_s)
+        .fold(0.0_f64, f64::max);
+    let max_p99 = points
+        .iter()
+        .map(|point| point.p99_ms)
+        .fold(0.0_f64, f64::max);
+    let min_axis = points.first().map(|point| point.axis_value).unwrap_or(0);
+    let max_axis = points
+        .last()
+        .map(|point| point.axis_value)
+        .unwrap_or(1)
+        .max(1);
+
+    html! {
+        <div class="sweep-view">
+            <div class="sweep-view-title">
+                {"Scaling sweep: "}
+                <span class="sweep-axis">{axis.label()}</span>
+                <span class="sweep-view-hint">{format!("{} matched runs", points.len())}</span>
+            </div>
+            <svg class="sweep-chart" viewBox="0 0 800 260" preserveAspectRatio="none">
+                { render_grid() }
+                { render_tput_series(&points, min_axis, max_axis, max_throughput) }
+                { render_p99_series(&points, min_axis, max_axis, max_p99) }
+                { render_axes_labels(axis.label(), min_axis, max_axis, max_throughput, max_p99) }
+            </svg>
+            <div class="sweep-legend">
+                <span class="sweep-legend-item sweep-legend-tput">{"Throughput (MB/s)"}</span>
+                <span class="sweep-legend-item sweep-legend-p99">{"P99 latency (ms)"}</span>
+            </div>
+        </div>
+    }
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum SweepAxis {
+    Producers,
+    Consumers,
+    Streams,
+    Partitions,
+    ConsumerGroups,
+    MessageSize,
+    MessagesPerBatch,
+    MessageBatches,
+}
+
+impl SweepAxis {
+    const ALL: [SweepAxis; 8] = [
+        SweepAxis::Producers,
+        SweepAxis::Consumers,
+        SweepAxis::Streams,
+        SweepAxis::Partitions,
+        SweepAxis::ConsumerGroups,
+        SweepAxis::MessageSize,
+        SweepAxis::MessagesPerBatch,
+        SweepAxis::MessageBatches,
+    ];
+
+    fn label(self) -> &'static str {
+        match self {
+            SweepAxis::Producers => "producers",
+            SweepAxis::Consumers => "consumers",
+            SweepAxis::Streams => "streams",
+            SweepAxis::Partitions => "partitions",
+            SweepAxis::ConsumerGroups => "consumer groups",
+            SweepAxis::MessageSize => "message size",
+            SweepAxis::MessagesPerBatch => "messages per batch",
+            SweepAxis::MessageBatches => "message batches",
+        }
+    }
+
+    fn value(self, params: &BenchmarkParams) -> u32 {
+        match self {
+            SweepAxis::Producers => params.producers,
+            SweepAxis::Consumers => params.consumers,
+            SweepAxis::Streams => params.streams,
+            SweepAxis::Partitions => params.partitions,
+            SweepAxis::ConsumerGroups => params.consumer_groups,
+            SweepAxis::MessageSize => params.message_size.min(),
+            SweepAxis::MessagesPerBatch => params.messages_per_batch.min(),
+            SweepAxis::MessageBatches => params.message_batches as u32,
+        }
+    }
+}
+
+#[derive(Clone, Copy)]
+struct Point {
+    axis_value: u32,
+    throughput_mb_s: f64,
+    p99_ms: f64,
+}
+
+fn find_sweep(
+    selected: &BenchmarkReportLight,
+    entries: &BTreeMap<BenchmarkKind, Vec<BenchmarkReportLight>>,
+) -> Option<(SweepAxis, Vec<Point>)> {
+    let mut best: Option<(SweepAxis, Vec<Point>)> = None;
+    for axis in SweepAxis::ALL {
+        let mut points: Vec<Point> = entries
+            .values()
+            .flatten()
+            .filter(|benchmark| siblings_match(selected, benchmark, axis))
+            .filter_map(|benchmark| {
+                benchmark
+                    .group_metrics
+                    .first()
+                    .map(|group| (benchmark, group))
+            })
+            .map(|(benchmark, group)| Point {
+                axis_value: axis.value(&benchmark.params),
+                throughput_mb_s: group.summary.total_throughput_megabytes_per_second,
+                p99_ms: group.summary.average_p99_latency_ms,
+            })
+            .collect();
+
+        points.sort_by_key(|point| point.axis_value);
+        points.dedup_by_key(|point| point.axis_value);
+
+        if points.len() >= 2
+            && best
+                .as_ref()
+                .is_none_or(|(_, previous)| points.len() > previous.len())
+        {
+            best = Some((axis, points));
+        }
+    }
+    best
+}
+
+fn siblings_match(
+    selected: &BenchmarkReportLight,
+    candidate: &BenchmarkReportLight,
+    varying_axis: SweepAxis,
+) -> bool {
+    if selected.params.benchmark_kind != candidate.params.benchmark_kind {
+        return false;
+    }
+    if selected.params.transport != candidate.params.transport
+        || selected.params.remark != candidate.params.remark
+    {
+        return false;
+    }
+    SweepAxis::ALL
+        .iter()
+        .filter(|axis| **axis != varying_axis)
+        .all(|axis| axis.value(&selected.params) == axis.value(&candidate.params))
+}
+
+fn render_grid() -> Html {
+    html! {
+        <g class="sweep-grid">
+            <line x1="60" y1="20" x2="60" y2="220" />
+            <line x1="60" y1="220" x2="780" y2="220" />
+            <line x1="780" y1="20" x2="780" y2="220" />
+        </g>
+    }
+}
+
+fn render_tput_series(points: &[Point], min_axis: u32, max_axis: u32, max_throughput: f64) -> Html {
+    if max_throughput <= 0.0 {
+        return html! {};
+    }
+    let path = polyline_path(points, min_axis, max_axis, max_throughput, |point| {
+        point.throughput_mb_s
+    });
+    html! {
+        <g class="sweep-series sweep-series-tput">
+            <polyline points={path} fill="none" stroke="currentColor" stroke-width="2.5" />
+            { for points.iter().map(|point| {
+                let (screen_x, screen_y) = project(point.axis_value, point.throughput_mb_s, min_axis, max_axis, max_throughput);
+                html! { <circle cx={screen_x.to_string()} cy={screen_y.to_string()} r="3.5" fill="currentColor" /> }
+            })}
+        </g>
+    }
+}
+
+fn render_p99_series(points: &[Point], min_axis: u32, max_axis: u32, max_p99: f64) -> Html {
+    if max_p99 <= 0.0 {
+        return html! {};
+    }
+    let path = polyline_path(points, min_axis, max_axis, max_p99, |point| point.p99_ms);
+    html! {
+        <g class="sweep-series sweep-series-p99">
+            <polyline points={path} fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="4 4" />
+            { for points.iter().map(|point| {
+                let (screen_x, screen_y) = project(point.axis_value, point.p99_ms, min_axis, max_axis, max_p99);
+                html! { <circle cx={screen_x.to_string()} cy={screen_y.to_string()} r="3" fill="currentColor" /> }
+            })}
+        </g>
+    }
+}
+
+fn render_axes_labels(
+    axis_label: &str,
+    min_axis: u32,
+    max_axis: u32,
+    max_throughput: f64,
+    max_p99: f64,
+) -> Html {
+    html! {
+        <g class="sweep-labels">
+            <text x="60" y="240" text-anchor="start">{min_axis}</text>
+            <text x="780" y="240" text-anchor="end">{max_axis}</text>
+            <text x="420" y="254" text-anchor="middle" class="sweep-axis-label">{axis_label}</text>
+            <text x="55" y="25" text-anchor="end" class="sweep-y-tput">{format!("{max_throughput:.0} MB/s")}</text>
+            <text x="55" y="220" text-anchor="end" class="sweep-y-tput">{"0"}</text>
+            <text x="785" y="25" text-anchor="start" class="sweep-y-p99">{format!("{max_p99:.1} ms")}</text>
+            <text x="785" y="220" text-anchor="start" class="sweep-y-p99">{"0"}</text>
+        </g>
+    }
+}
+
+fn polyline_path(
+    points: &[Point],
+    min_axis: u32,
+    max_axis: u32,
+    max_value: f64,
+    extract_value: impl Fn(&Point) -> f64,
+) -> String {
+    points
+        .iter()
+        .map(|point| {
+            let (screen_x, screen_y) = project(
+                point.axis_value,
+                extract_value(point),
+                min_axis,
+                max_axis,
+                max_value,
+            );
+            format!("{screen_x:.1},{screen_y:.1}")
+        })
+        .collect::<Vec<_>>()
+        .join(" ")
+}
+
+fn project(
+    axis_value: u32,
+    y_value: f64,
+    min_axis: u32,
+    max_axis: u32,
+    max_value: f64,
+) -> (f64, f64) {
+    let span = (max_axis.saturating_sub(min_axis)).max(1) as f64;
+    let normalized_x = (axis_value.saturating_sub(min_axis)) as f64 / span;
+    let screen_x = 60.0 + normalized_x * 720.0;
+    let screen_y = if max_value > 0.0 {
+        220.0 - (y_value / max_value).clamp(0.0, 1.0) * 200.0
+    } else {
+        220.0
+    };
+    (screen_x, screen_y)
+}
diff --git a/core/bench/dashboard/frontend/src/components/layout/theme_toggle_floating.rs b/core/bench/dashboard/frontend/src/components/layout/theme_toggle_floating.rs
new file mode 100644
index 000000000..716f58108
--- /dev/null
+++ b/core/bench/dashboard/frontend/src/components/layout/theme_toggle_floating.rs
@@ -0,0 +1,66 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use yew::prelude::*;
+
+#[function_component(ThemeToggleFloating)]
+pub fn theme_toggle_floating() -> Html {
+    let (is_dark, toggle) = use_context::<(bool, Callback<()>)>().expect("Theme context not found");
+
+    let onclick = {
+        let toggle = toggle.clone();
+        Callback::from(move |_| toggle.emit(()))
+    };
+
+    let title = if is_dark {
+        "Switch to light theme"
+    } else {
+        "Switch to dark theme"
+    };
+
+    html! {
+        <button
+            type="button"
+            class="theme-toggle-floating"
+            onclick={onclick}
+            title={title}
+            aria-label={title}
+        >
+            if is_dark {
+                <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"
+                     fill="none" stroke="currentColor" stroke-width="2"
+                     stroke-linecap="round" stroke-linejoin="round">
+                    <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
+                </svg>
+            } else {
+                <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"
+                     fill="none" stroke="currentColor" stroke-width="2"
+                     stroke-linecap="round" stroke-linejoin="round">
+                    <circle cx="12" cy="12" r="5" />
+                    <line x1="12" y1="1" x2="12" y2="3" />
+                    <line x1="12" y1="21" x2="12" y2="23" />
+                    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
+                    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
+                    <line x1="1" y1="12" x2="3" y2="12" />
+                    <line x1="21" y1="12" x2="23" y2="12" />
+                    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
+                    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
+                </svg>
+            }
+        </button>
+    }
+}
diff --git a/core/bench/dashboard/frontend/src/components/layout/topbar.rs b/core/bench/dashboard/frontend/src/components/layout/topbar.rs
index 49368d24f..232bd8f9f 100644
--- a/core/bench/dashboard/frontend/src/components/layout/topbar.rs
+++ b/core/bench/dashboard/frontend/src/components/layout/topbar.rs
@@ -18,7 +18,6 @@
 use crate::api;
 use crate::components::embed_modal::EmbedModal;
 use crate::components::selectors::measurement_type_selector::MeasurementType;
-use crate::components::theme::theme_toggle::ThemeToggle;
 use crate::components::tooltips::benchmark_info_toggle::BenchmarkInfoToggle;
 use crate::components::tooltips::benchmark_info_tooltip::BenchmarkInfoTooltip;
 use crate::components::tooltips::server_stats_toggle::ServerStatsToggle;
@@ -100,10 +99,6 @@ pub fn topbar(props: &TopBarProps) -> Html {
     html! {
         <div class="top-buttons">
             <div class="controls">
-                <ThemeToggle
-                    is_dark={props.is_dark}
-                    on_toggle={props.on_theme_toggle.clone()}
-                />
                 {
                     if !props.selected_gitref.is_empty() {
                         html! {
@@ -227,6 +222,15 @@ pub fn topbar(props: &TopBarProps) -> Html {
                                     >
                                         { "Throughput" }
                                     </button>
+                                    <button
+                                        class={classes!(
+                                            "measurement-button",
+                                            (selected_measurement == MeasurementType::Tail).then_some("active")
+                                        )}
+                                        onclick={on_measurement_select.reform(|_| MeasurementType::Tail)}
+                                    >
+                                        { "Tail" }
+                                    </button>
                                 </div>
                             </>
                         }
diff --git a/core/bench/dashboard/frontend/src/components/selectors/benchmark_selector.rs b/core/bench/dashboard/frontend/src/components/selectors/benchmark_selector.rs
index d93daedf4..a0ad760f5 100644
--- a/core/bench/dashboard/frontend/src/components/selectors/benchmark_selector.rs
+++ b/core/bench/dashboard/frontend/src/components/selectors/benchmark_selector.rs
@@ -18,9 +18,12 @@
 use crate::{
     components::selectors::benchmark_kind_selector::BenchmarkKindSelector,
     state::benchmark::{BenchmarkAction, use_benchmark},
+    state::ui::{UiAction, use_ui},
 };
+use bench_dashboard_shared::BenchmarkReportLight;
 use bench_report::benchmark_kind::BenchmarkKind;
 use std::collections::HashSet;
+use uuid::Uuid;
 use yew::prelude::*;
 
 #[derive(Properties, PartialEq)]
@@ -31,63 +34,49 @@ pub struct BenchmarkSelectorProps {
 #[function_component(BenchmarkSelector)]
 pub fn benchmark_selector(props: &BenchmarkSelectorProps) -> Html {
     let benchmark_ctx = use_benchmark();
+    let ui_state = use_ui();
+    let filters = ui_state.param_filters.clone();
+    let pinned_uuid = ui_state.compare_pin.as_ref().map(|b| b.uuid);
     let selected_kind = benchmark_ctx.state.selected_kind;
+    let selected_uuid = benchmark_ctx
+        .state
+        .selected_benchmark
+        .as_ref()
+        .map(|b| b.uuid);
 
     let available_kinds: HashSet<_> = benchmark_ctx
         .state
         .entries
         .keys()
-        .filter(|k| match props.kind {
-            BenchmarkKind::PinnedProducer
-            | BenchmarkKind::PinnedConsumer
-            | BenchmarkKind::PinnedProducerAndConsumer => {
-                matches!(
-                    k,
-                    BenchmarkKind::PinnedProducer
-                        | BenchmarkKind::PinnedConsumer
-                        | BenchmarkKind::PinnedProducerAndConsumer
-                )
-            }
-            BenchmarkKind::BalancedProducer
-            | BenchmarkKind::BalancedConsumerGroup
-            | BenchmarkKind::BalancedProducerAndConsumerGroup => {
-                matches!(
-                    k,
-                    BenchmarkKind::BalancedProducer
-                        | BenchmarkKind::BalancedConsumerGroup
-                        | BenchmarkKind::BalancedProducerAndConsumerGroup
-                )
-            }
-            BenchmarkKind::EndToEndProducingConsumer
-            | BenchmarkKind::EndToEndProducingConsumerGroup => {
-                matches!(
-                    k,
-                    BenchmarkKind::EndToEndProducingConsumer
-                        | BenchmarkKind::EndToEndProducingConsumerGroup
-                )
-            }
-        })
+        .filter(|k| matches_tab(&props.kind, k))
         .cloned()
         .collect();
 
     let empty_vec = Vec::new();
-    let current_benchmarks = benchmark_ctx
+    let all_benchmarks = benchmark_ctx
         .state
         .entries
         .get(&selected_kind)
         .unwrap_or(&empty_vec);
+    let current_benchmarks: Vec<_> = all_benchmarks
+        .iter()
+        .filter(|b| filters.matches(b))
+        .collect();
+    let hidden_by_filter = all_benchmarks.len() - current_benchmarks.len();
 
     let on_benchmark_select = {
         let dispatch = benchmark_ctx.dispatch.clone();
         let entries = benchmark_ctx.state.entries.clone();
+        let ui_state = ui_state.clone();
         Callback::from(move |pretty_name: String| {
-            let selected_benchmark = entries.get(&selected_kind).and_then(|benchmarks| {
+            let selected = entries.get(&selected_kind).and_then(|benchmarks| {
                 benchmarks
                     .iter()
                     .find(|b| b.params.pretty_name == pretty_name)
             });
+            ui_state.dispatch(UiAction::SetLanding(false));
             dispatch.emit(BenchmarkAction::SelectBenchmark(Box::new(
-                selected_benchmark.cloned(),
+                selected.cloned(),
             )));
         })
     };
@@ -99,12 +88,17 @@ pub fn benchmark_selector(props: &BenchmarkSelectorProps) -> Html {
         })
     };
 
-    let current_value = benchmark_ctx
-        .state
-        .selected_benchmark
-        .as_ref()
-        .map(|b| b.params.pretty_name.clone())
-        .unwrap_or_default();
+    let on_toggle_pin = {
+        let ui_state = ui_state.clone();
+        Callback::from(move |benchmark: BenchmarkReportLight| {
+            let next = if ui_state.compare_pin.as_ref().map(|b| b.uuid) == Some(benchmark.uuid) {
+                None
+            } else {
+                Some(Box::new(benchmark))
+            };
+            ui_state.dispatch(UiAction::SetComparePin(next));
+        })
+    };
 
     html! {
         <div class="benchmark-select">
@@ -114,58 +108,133 @@ pub fn benchmark_selector(props: &BenchmarkSelectorProps) -> Html {
                 available_kinds={available_kinds}
             />
 
+            if current_benchmarks.is_empty() && hidden_by_filter > 0 {
+                <div class="no-search-results">
+                    <p>{format!("All {hidden_by_filter} benchmark(s) hidden by active filters.")}</p>
+                </div>
+            }
             <div class="benchmark-list">
-                {current_benchmarks.iter().map(|benchmark| {
-                    let pretty_name = benchmark.params.pretty_name.clone();
-                    let is_active = pretty_name == current_value;
-                    let on_click = {
-                        let on_benchmark_select = on_benchmark_select.clone();
-                        let pretty_name = pretty_name.clone();
-                        Callback::from(move |_| {
-                            on_benchmark_select.emit(pretty_name.clone());
-                        })
-                    };
-                    let pretty_name = pretty_name.split("(").next().unwrap().to_string();
-
-                    html! {
-                        <div
-                            class={classes!(
-                                "benchmark-list-item",
-                                is_active.then_some("active")
-                            )}
-                            onclick={on_click}
-                        >
-                            <div class="benchmark-list-item-content">
-                                <div class="benchmark-list-item-title">
-                                    <span class="benchmark-list-item-dot" />
-                                    {pretty_name}
-                                </div>
-
-                                <div class="benchmark-list-item-details">
-                                    {if let Some(remark) = benchmark.params.remark.as_deref() {
-                                        if !remark.is_empty() {
-                                            let truncated_remark = if remark.len() > 30 {
-                                                format!("{}..", &remark[0..28])
-                                            } else {
-                                                remark.to_string()
-                                            };
-                                            html! {
-                                                <div class="benchmark-list-item-subtitle">
-                                                    <span class="benchmark-list-item-label">{"Remark:"}</span>
-                                                    <span>{truncated_remark}</span>
-                                                </div>
-                                            }
-                                        } else {
-                                            html! {}
-                                        }
-                                    } else {
-                                        html! {}
-                                    }}
-                                </div>
-                            </div>
-                        </div>
-                    }
-                }).collect::<Html>()}
+                { for current_benchmarks.iter().map(|benchmark| render_item(
+                    benchmark,
+                    selected_uuid,
+                    pinned_uuid,
+                    &on_benchmark_select,
+                    &on_toggle_pin,
+                ))}
+            </div>
+        </div>
+    }
+}
+
+fn matches_tab(tab_kind: &BenchmarkKind, candidate: &BenchmarkKind) -> bool {
+    use BenchmarkKind::*;
+    match tab_kind {
+        PinnedProducer | PinnedConsumer | PinnedProducerAndConsumer => matches!(
+            candidate,
+            PinnedProducer | PinnedConsumer | PinnedProducerAndConsumer
+        ),
+        BalancedProducer | BalancedConsumerGroup | BalancedProducerAndConsumerGroup => matches!(
+            candidate,
+            BalancedProducer | BalancedConsumerGroup | BalancedProducerAndConsumerGroup
+        ),
+        EndToEndProducingConsumer | EndToEndProducingConsumerGroup => matches!(
+            candidate,
+            EndToEndProducingConsumer | EndToEndProducingConsumerGroup
+        ),
+    }
+}
+
+fn render_item(
+    benchmark: &BenchmarkReportLight,
+    selected_uuid: Option<Uuid>,
+    pinned_uuid: Option<Uuid>,
+    on_select: &Callback<String>,
+    on_toggle_pin: &Callback<BenchmarkReportLight>,
+) -> Html {
+    let is_active = Some(benchmark.uuid) == selected_uuid;
+    let is_pinned = Some(benchmark.uuid) == pinned_uuid;
+    let full_name = benchmark.params.pretty_name.clone();
+    let display_name = full_name
+        .split('(')
+        .next()
+        .unwrap_or(&full_name)
+        .to_string();
+
+    let on_click = {
+        let on_select = on_select.clone();
+        let name = full_name.clone();
+        Callback::from(move |_| on_select.emit(name.clone()))
+    };
+
+    let on_pin_click = {
+        let on_toggle_pin = on_toggle_pin.clone();
+        let benchmark = benchmark.clone();
+        Callback::from(move |e: MouseEvent| {
+            e.stop_propagation();
+            on_toggle_pin.emit(benchmark.clone());
+        })
+    };
+
+    let pin_title = if is_pinned {
+        "Unpin from compare"
+    } else {
+        "Pin for compare"
+    };
+
+    html! {
+        <div
+            class={classes!(
+                "benchmark-list-item",
+                is_active.then_some("active"),
+                is_pinned.then_some("pinned"),
+            )}
+            onclick={on_click}
+        >
+            <div class="benchmark-list-item-content">
+                <div class="benchmark-list-item-title-row">
+                    <div class="benchmark-list-item-title">
+                        <span class="benchmark-list-item-dot" />
+                        {display_name}
+                    </div>
+                    <button
+                        type="button"
+                        class={classes!("compare-pin-btn", is_pinned.then_some("active"))}
+                        onclick={on_pin_click}
+                        title={pin_title}
+                        aria-label={pin_title}
+                    >
+                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
+                             fill={if is_pinned { "currentColor" } else { "none" }}
+                             stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                            <path d="M12 17v5" />
+                            <path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z" />
+                        </svg>
+                    </button>
+                </div>
+
+                { render_remark(&benchmark.params.remark) }
+            </div>
+        </div>
+    }
+}
+
+fn render_remark(remark: &Option<String>) -> Html {
+    let Some(remark) = remark.as_deref() else {
+        return html! {};
+    };
+    if remark.is_empty() {
+        return html! {};
+    }
+    let truncated = if remark.len() > 30 {
+        format!("{}..", &remark[..28])
+    } else {
+        remark.to_string()
+    };
+    html! {
+        <div class="benchmark-list-item-details">
+            <div class="benchmark-list-item-subtitle">
+                <span class="benchmark-list-item-label">{"Remark:"}</span>
+                <span>{truncated}</span>
             </div>
         </div>
     }
diff --git a/core/bench/dashboard/frontend/src/components/selectors/measurement_type_selector.rs b/core/bench/dashboard/frontend/src/components/selectors/measurement_type_selector.rs
index d8023ed93..ca5482829 100644
--- a/core/bench/dashboard/frontend/src/components/selectors/measurement_type_selector.rs
+++ b/core/bench/dashboard/frontend/src/components/selectors/measurement_type_selector.rs
@@ -24,6 +24,7 @@ pub enum MeasurementType {
     Latency,
     Throughput,
     Distribution,
+    Tail,
 }
 
 impl Display for MeasurementType {
@@ -32,6 +33,7 @@ impl Display for MeasurementType {
             MeasurementType::Latency => write!(f, "Latency"),
             MeasurementType::Throughput => write!(f, "Throughput"),
             MeasurementType::Distribution => write!(f, "Distribution"),
+            MeasurementType::Tail => write!(f, "Tail"),
         }
     }
 }
@@ -44,6 +46,7 @@ impl FromStr for MeasurementType {
             "Latency" => Ok(MeasurementType::Latency),
             "Throughput" => Ok(MeasurementType::Throughput),
             "Distribution" => Ok(MeasurementType::Distribution),
+            "Tail" => Ok(MeasurementType::Tail),
             _ => Err(()),
         }
     }
@@ -82,6 +85,12 @@ pub fn measurement_type_selector(props: &MeasurementTypeSelectorProps) -> Html {
                 >
                     {"Distribution"}
                 </button>
+                <button
+                    class={if *selected == MeasurementType::Tail { "segment active" } else { "segment" }}
+                    onclick={props.on_measurement_select.reform(|_| MeasurementType::Tail)}
+                >
+                    {"Tail"}
+                </button>
             </div>
         </div>
     }
diff --git a/core/bench/dashboard/frontend/src/components/selectors/mod.rs b/core/bench/dashboard/frontend/src/components/selectors/mod.rs
index 04fc93b63..947c21bb7 100644
--- a/core/bench/dashboard/frontend/src/components/selectors/mod.rs
+++ b/core/bench/dashboard/frontend/src/components/selectors/mod.rs
@@ -21,5 +21,6 @@ pub mod benchmark_selector;
 pub mod gitref_selector;
 pub mod hardware_selector;
 pub mod measurement_type_selector;
+pub mod param_filters_panel;
 pub mod recent_benchmarks_selector;
 pub mod view_mode_selector;
diff --git a/core/bench/dashboard/frontend/src/components/selectors/param_filters_panel.rs b/core/bench/dashboard/frontend/src/components/selectors/param_filters_panel.rs
new file mode 100644
index 000000000..df5f14edf
--- /dev/null
+++ b/core/bench/dashboard/frontend/src/components/selectors/param_filters_panel.rs
@@ -0,0 +1,211 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::state::ui::{MetricField, MetricRange, ParamField, ParamRange, UiAction, use_ui};
+use yew::prelude::*;
+
+#[function_component(ParamFiltersPanel)]
+pub fn param_filters_panel() -> Html {
+    let ui = use_ui();
+    let is_open = use_state(|| false);
+
+    let toggle = {
+        let is_open = is_open.clone();
+        Callback::from(move |_| is_open.set(!*is_open))
+    };
+
+    let on_clear = {
+        let ui = ui.clone();
+        Callback::from(move |e: MouseEvent| {
+            e.stop_propagation();
+            ui.dispatch(UiAction::ClearParamFilters);
+        })
+    };
+
+    let filters = ui.param_filters.clone();
+    let active = filters.active_count();
+    let chevron = if *is_open { "v" } else { ">" };
+
+    html! {
+        <div class="param-filters-container">
+            <button type="button" class="param-filters-header" onclick={toggle}>
+                <span class="param-filters-chevron">{chevron}</span>
+                <span class="param-filters-title">{"Filters"}</span>
+                if active > 0 {
+                    <span class="param-filters-badge">{active}</span>
+                }
+            </button>
+            if *is_open {
+                <div class="param-filters-body">
+                    <ParamFilterRow label="Producers" field={ParamField::Producers}
+                        range={filters.producers.clone()} />
+                    <ParamFilterRow label="Consumers" field={ParamField::Consumers}
+                        range={filters.consumers.clone()} />
+                    <ParamFilterRow label="Streams" field={ParamField::Streams}
+                        range={filters.streams.clone()} />
+                    <ParamFilterRow label="Partitions" field={ParamField::Partitions}
+                        range={filters.partitions.clone()} />
+                    <ParamFilterRow label="Cons. Groups" field={ParamField::ConsumerGroups}
+                        range={filters.consumer_groups.clone()} />
+                    <MetricFilterRow label="Tput (MB/s)" field={MetricField::ThroughputMbS}
+                        range={filters.throughput_mb_s.clone()} step="1" />
+                    <MetricFilterRow label="P99 (ms)" field={MetricField::P99LatencyMs}
+                        range={filters.p99_latency_ms.clone()} step="0.01" />
+                    if active > 0 {
+                        <button type="button" class="param-filters-clear" onclick={on_clear}>
+                            {"Clear all"}
+                        </button>
+                    }
+                </div>
+            }
+        </div>
+    }
+}
+
+#[derive(Properties, PartialEq)]
+struct ParamFilterRowProps {
+    label: AttrValue,
+    field: ParamField,
+    range: ParamRange,
+}
+
+#[function_component(ParamFilterRow)]
+fn param_filter_row(props: &ParamFilterRowProps) -> Html {
+    let ui = use_ui();
+    let field = props.field;
+    let range = props.range.clone();
+
+    let on_from = {
+        let ui = ui.clone();
+        let range = range.clone();
+        Callback::from(move |e: InputEvent| {
+            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
+            let next = ParamRange {
+                from: parse_u32(&input.value()),
+                to: range.to,
+            };
+            ui.dispatch(UiAction::SetParamRange(field, next));
+        })
+    };
+
+    let on_to = {
+        let ui = ui.clone();
+        let range = range.clone();
+        Callback::from(move |e: InputEvent| {
+            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
+            let next = ParamRange {
+                from: range.from,
+                to: parse_u32(&input.value()),
+            };
+            ui.dispatch(UiAction::SetParamRange(field, next));
+        })
+    };
+
+    let from_value = range.from.map(|v| v.to_string()).unwrap_or_default();
+    let to_value = range.to.map(|v| v.to_string()).unwrap_or_default();
+
+    html! {
+        <div class="param-filter-row">
+            <label class="param-filter-label">{&props.label}</label>
+            <input type="number" min="0" step="1" class="param-filter-input"
+                placeholder="min" value={from_value} oninput={on_from} />
+            <span class="param-filter-sep">{"-"}</span>
+            <input type="number" min="0" step="1" class="param-filter-input"
+                placeholder="max" value={to_value} oninput={on_to} />
+        </div>
+    }
+}
+
+#[derive(Properties, PartialEq)]
+struct MetricFilterRowProps {
+    label: AttrValue,
+    field: MetricField,
+    range: MetricRange,
+    step: AttrValue,
+}
+
+#[function_component(MetricFilterRow)]
+fn metric_filter_row(props: &MetricFilterRowProps) -> Html {
+    let ui = use_ui();
+    let field = props.field;
+    let range = props.range.clone();
+
+    let on_from = {
+        let ui = ui.clone();
+        let range = range.clone();
+        Callback::from(move |e: InputEvent| {
+            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
+            let next = MetricRange {
+                from: parse_f64(&input.value()),
+                to: range.to,
+            };
+            ui.dispatch(UiAction::SetMetricRange(field, next));
+        })
+    };
+
+    let on_to = {
+        let ui = ui.clone();
+        let range = range.clone();
+        Callback::from(move |e: InputEvent| {
+            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
+            let next = MetricRange {
+                from: range.from,
+                to: parse_f64(&input.value()),
+            };
+            ui.dispatch(UiAction::SetMetricRange(field, next));
+        })
+    };
+
+    let from_value = range.from.map(format_f64).unwrap_or_default();
+    let to_value = range.to.map(format_f64).unwrap_or_default();
+
+    html! {
+        <div class="param-filter-row">
+            <label class="param-filter-label">{&props.label}</label>
+            <input type="number" min="0" step={props.step.clone()} class="param-filter-input"
+                placeholder="min" value={from_value} oninput={on_from} />
+            <span class="param-filter-sep">{"-"}</span>
+            <input type="number" min="0" step={props.step.clone()} class="param-filter-input"
+                placeholder="max" value={to_value} oninput={on_to} />
+        </div>
+    }
+}
+
+fn parse_u32(raw: &str) -> Option<u32> {
+    let trimmed = raw.trim();
+    if trimmed.is_empty() {
+        None
+    } else {
+        trimmed.parse().ok()
+    }
+}
+
+fn parse_f64(raw: &str) -> Option<f64> {
+    let trimmed = raw.trim();
+    if trimmed.is_empty() {
+        return None;
+    }
+    trimmed.parse().ok().filter(|v: &f64| v.is_finite())
+}
+
+fn format_f64(v: f64) -> String {
+    if v.fract() == 0.0 {
+        format!("{v:.0}")
+    } else {
+        format!("{v}")
+    }
+}
diff --git a/core/bench/dashboard/frontend/src/components/selectors/recent_benchmarks_selector.rs b/core/bench/dashboard/frontend/src/components/selectors/recent_benchmarks_selector.rs
index 240b1fc70..027d67f78 100644
--- a/core/bench/dashboard/frontend/src/components/selectors/recent_benchmarks_selector.rs
+++ b/core/bench/dashboard/frontend/src/components/selectors/recent_benchmarks_selector.rs
@@ -18,6 +18,7 @@
 use crate::{
     api,
     state::benchmark::{BenchmarkAction, use_benchmark},
+    state::ui::{UiAction, use_ui},
 };
 use bench_dashboard_shared::BenchmarkReportLight;
 use chrono::{DateTime, Utc};
@@ -56,6 +57,9 @@ pub struct RecentBenchmarksSelectorProps {
 #[function_component(RecentBenchmarksSelector)]
 pub fn recent_benchmarks_selector(props: &RecentBenchmarksSelectorProps) -> Html {
     let benchmark_ctx = use_benchmark();
+    let ui_state = use_ui();
+    let filters = ui_state.param_filters.clone();
+    let pinned_uuid = ui_state.compare_pin.as_ref().map(|b| b.uuid);
 
     let recent_benchmarks = use_state(Vec::<BenchmarkReportLight>::new);
     let is_loading = use_state(|| true);
@@ -64,23 +68,31 @@ pub fn recent_benchmarks_selector(props: &RecentBenchmarksSelectorProps) -> Html
         let recent_benchmarks = recent_benchmarks.clone();
         let is_loading = is_loading.clone();
         let limit = props.limit;
+        let benchmark_dispatch = benchmark_ctx.dispatch.clone();
+        let ui_dispatch_handle = ui_state.clone();
 
         use_effect_with((), move |_| {
             spawn_local(async move {
                 match api::fetch_recent_benchmarks(Some(limit)).await {
                     Ok(mut data) => {
-                        data.sort_by(|a, b| {
-                            let parse_a = DateTime::parse_from_rfc3339(&a.timestamp);
-                            let parse_b = DateTime::parse_from_rfc3339(&b.timestamp);
-                            match (parse_a, parse_b) {
-                                (Ok(time_a), Ok(time_b)) => time_b.cmp(&time_a),
+                        data.sort_by(|left, right| {
+                            let parse_left = DateTime::parse_from_rfc3339(&left.timestamp);
+                            let parse_right = DateTime::parse_from_rfc3339(&right.timestamp);
+                            match (parse_left, parse_right) {
+                                (Ok(time_left), Ok(time_right)) => time_right.cmp(&time_left),
                                 _ => std::cmp::Ordering::Equal,
                             }
                         });
+                        if let Some(most_recent) = data.first().cloned() {
+                            ui_dispatch_handle.dispatch(UiAction::SetLanding(false));
+                            benchmark_dispatch.emit(BenchmarkAction::SelectBenchmark(Box::new(
+                                Some(most_recent),
+                            )));
+                        }
                         recent_benchmarks.set(data);
                     }
-                    Err(e) => {
-                        log!(format!("Error fetching recent benchmarks: {}", e));
+                    Err(error) => {
+                        log!(format!("Error fetching recent benchmarks: {}", error));
                     }
                 }
                 is_loading.set(false);
@@ -90,15 +102,30 @@ pub fn recent_benchmarks_selector(props: &RecentBenchmarksSelectorProps) -> Html
 
     let on_benchmark_select = {
         let benchmark_ctx = benchmark_ctx.clone();
+        let ui_state = ui_state.clone();
         Callback::from(move |benchmark: BenchmarkReportLight| {
+            ui_state.dispatch(UiAction::SetLanding(false));
             benchmark_ctx
                 .dispatch
                 .emit(BenchmarkAction::SelectBenchmark(Box::new(Some(benchmark))));
         })
     };
 
+    let on_toggle_pin = {
+        let ui_state = ui_state.clone();
+        Callback::from(move |benchmark: BenchmarkReportLight| {
+            let next = if ui_state.compare_pin.as_ref().map(|b| b.uuid) == Some(benchmark.uuid) {
+                None
+            } else {
+                Some(Box::new(benchmark))
+            };
+            ui_state.dispatch(UiAction::SetComparePin(next));
+        })
+    };
+
     let filtered_benchmarks = (*recent_benchmarks)
         .iter()
+        .filter(|benchmark| filters.matches(benchmark))
         .filter(|benchmark| {
             if props.search_query.is_empty() {
                 return true;
@@ -151,7 +178,11 @@ pub fn recent_benchmarks_selector(props: &RecentBenchmarksSelectorProps) -> Html
                     <p class="loading-message">{"Loading recent benchmarks..."}</p>
                 } else if filtered_benchmarks.is_empty() {
                     <div class="no-search-results">
-                        <p>{format!("No benchmarks found matching \"{}\"", props.search_query)}</p>
+                        { if props.search_query.is_empty() && filters.is_any_active() {
+                            html! { <p>{"No benchmarks match the active filters."}</p> }
+                        } else {
+                            html! { <p>{format!("No benchmarks found matching \"{}\"", props.search_query)}</p> }
+                        }}
                     </div>
                 } else {
                     <ul class="benchmark-list">
@@ -163,14 +194,29 @@ pub fn recent_benchmarks_selector(props: &RecentBenchmarksSelectorProps) -> Html
                                     on_benchmark_select.emit(benchmark_clone.clone());
                                 })
                             };
+                            let on_pin_click = {
+                                let on_toggle_pin = on_toggle_pin.clone();
+                                let benchmark_clone = benchmark.clone();
+                                Callback::from(move |e: MouseEvent| {
+                                    e.stop_propagation();
+                                    on_toggle_pin.emit(benchmark_clone.clone());
+                                })
+                            };
                             let timestamp_display = format_relative_time(&benchmark.timestamp);
 
                             let is_selected = benchmark_ctx.state.selected_benchmark.as_ref()
                                 .map(|selected| selected.uuid == benchmark.uuid)
                                 .unwrap_or(false);
+                            let is_pinned = Some(benchmark.uuid) == pinned_uuid;
+                            let pin_title = if is_pinned { "Unpin from compare" } else { "Pin for compare" };
 
                             html! {
-                                <li class={classes!("benchmark-list-item", "recent-benchmark-item", is_selected.then_some("active"))} onclick={on_select}>
+                                <li class={classes!(
+                                    "benchmark-list-item",
+                                    "recent-benchmark-item",
+                                    is_selected.then_some("active"),
+                                    is_pinned.then_some("pinned"),
+                                )} onclick={on_select}>
                                     <div class="benchmark-list-item-content">
                                         <div class="benchmark-list-item-header">
                                             <div class="benchmark-list-item-title">
@@ -178,6 +224,20 @@ pub fn recent_benchmarks_selector(props: &RecentBenchmarksSelectorProps) -> Html
                                                 {benchmark.params.benchmark_kind.to_string()}
                                             </div>
                                             <div class="benchmark-list-item-time">{timestamp_display}</div>
+                                            <button
+                                                type="button"
+                                                class={classes!("compare-pin-btn", is_pinned.then_some("active"))}
+                                                onclick={on_pin_click}
+                                                title={pin_title}
+                                                aria-label={pin_title}
+                                            >
+                                                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
+                                                     fill={if is_pinned { "currentColor" } else { "none" }}
+                                                     stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                                                    <path d="M12 17v5" />
+                                                    <path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z" />
+                                                </svg>
+                                            </button>
                                         </div>
 
                                         <div class="benchmark-list-item-details">
diff --git a/core/bench/dashboard/frontend/src/components/theme/mod.rs b/core/bench/dashboard/frontend/src/components/theme/mod.rs
index ab0d535aa..d9d031fcc 100644
--- a/core/bench/dashboard/frontend/src/components/theme/mod.rs
+++ b/core/bench/dashboard/frontend/src/components/theme/mod.rs
@@ -16,4 +16,3 @@
 // under the License.
 
 pub mod theme_provider;
-pub mod theme_toggle;
diff --git a/core/bench/dashboard/frontend/src/components/theme/theme_toggle.rs b/core/bench/dashboard/frontend/src/components/theme/theme_toggle.rs
deleted file mode 100644
index 19bd0a601..000000000
--- a/core/bench/dashboard/frontend/src/components/theme/theme_toggle.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use yew::prelude::*;
-
-#[derive(Properties, PartialEq)]
-pub struct ThemeToggleProps {
-    pub on_toggle: Callback<bool>,
-    pub is_dark: bool,
-}
-
-#[function_component(ThemeToggle)]
-pub fn theme_toggle(props: &ThemeToggleProps) -> Html {
-    let onclick = {
-        let on_toggle = props.on_toggle.clone();
-        let is_dark = props.is_dark;
-        Callback::from(move |_| {
-            on_toggle.emit(!is_dark);
-        })
-    };
-
-    html! {
-        <button class="theme-toggle" {onclick}>
-            if props.is_dark {
-                <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-                    <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
-                </svg>
-            } else {
-                <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-                    <circle cx="12" cy="12" r="5"></circle>
-                    <line x1="12" y1="1" x2="12" y2="3"></line>
-                    <line x1="12" y1="21" x2="12" y2="23"></line>
-                    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
-                    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
-                    <line x1="1" y1="12" x2="3" y2="12"></line>
-                    <line x1="21" y1="12" x2="23" y2="12"></line>
-                    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
-                    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
-                </svg>
-            }
-        </button>
-    }
-}
diff --git a/core/bench/dashboard/frontend/src/state/benchmark.rs b/core/bench/dashboard/frontend/src/state/benchmark.rs
index 5c7c0f15f..b11c85596 100644
--- a/core/bench/dashboard/frontend/src/state/benchmark.rs
+++ b/core/bench/dashboard/frontend/src/state/benchmark.rs
@@ -20,6 +20,7 @@ use bench_report::{
     benchmark_kind::BenchmarkKind, numeric_parameter::BenchmarkNumericParameter,
     transport::BenchmarkTransport,
 };
+use chrono::{DateTime, Duration};
 use gloo::console::log;
 use std::{collections::BTreeMap, rc::Rc};
 use yew::prelude::*;
@@ -224,7 +225,7 @@ impl BenchmarkState {
                 self.current_hardware, hardware, self.current_gitref, gitref_for_entries
             ));
 
-            let best = Self::find_best_benchmark(&entries);
+            let best = self.find_best_benchmark(&entries);
             if let Some(best) = best {
                 new_selected_benchmark = Some(best.clone());
                 new_selected_kind = best.params.benchmark_kind;
@@ -332,23 +333,72 @@ impl BenchmarkState {
         }
     }
 
+    /// Pick a sensible default benchmark from the freshest run batch.
+    ///
+    /// Scopes to `self.selected_kind` when that kind has entries (keeps the default
+    /// consistent with the active sidebar tab); otherwise falls back to all entries.
     fn find_best_benchmark(
-        benchmarks: &BTreeMap<BenchmarkKind, Vec<BenchmarkReportLight>>,
+        &self,
+        entries: &BTreeMap<BenchmarkKind, Vec<BenchmarkReportLight>>,
     ) -> Option<BenchmarkReportLight> {
-        benchmarks
-            .values()
+        let in_kind: Vec<&BenchmarkReportLight> = entries
+            .get(&self.selected_kind)
+            .into_iter()
             .flatten()
-            .max_by_key(|bm| {
-                bm.group_metrics
-                    .first()
-                    .unwrap()
-                    .summary
-                    .average_p99_latency_ms as u64
-            })
-            .cloned()
+            .collect();
+        let scoped: Vec<&BenchmarkReportLight> = if in_kind.is_empty() {
+            entries.values().flatten().collect()
+        } else {
+            in_kind
+        };
+        pick_best_from_recent_batch(&scoped)
     }
 }
 
+/// From a set of benchmarks, pick the one that best represents "current peak":
+/// restrict to runs within 2h of the latest timestamp (avoids UTC day-split artifacts
+/// on overnight benchmark sweeps), then pick lowest P99 latency; ties broken by
+/// highest total throughput.
+pub fn pick_best_from_recent_batch(
+    benchmarks: &[&BenchmarkReportLight],
+) -> Option<BenchmarkReportLight> {
+    let latest_timestamp = benchmarks
+        .iter()
+        .filter_map(|benchmark| DateTime::parse_from_rfc3339(&benchmark.timestamp).ok())
+        .max()?;
+    let window_start = latest_timestamp - Duration::hours(2);
+
+    benchmarks
+        .iter()
+        .copied()
+        .filter(|benchmark| {
+            DateTime::parse_from_rfc3339(&benchmark.timestamp)
+                .map(|timestamp| timestamp >= window_start)
+                .unwrap_or(false)
+        })
+        .min_by(|left, right| {
+            let metrics = |report: &BenchmarkReportLight| {
+                report.group_metrics.first().map(|group| {
+                    (
+                        group.summary.average_p99_latency_ms,
+                        group.summary.total_throughput_megabytes_per_second,
+                    )
+                })
+            };
+            let (left_p99, left_throughput) = metrics(left).unwrap_or((f64::INFINITY, 0.0));
+            let (right_p99, right_throughput) = metrics(right).unwrap_or((f64::INFINITY, 0.0));
+            left_p99
+                .partial_cmp(&right_p99)
+                .unwrap_or(std::cmp::Ordering::Equal)
+                .then(
+                    right_throughput
+                        .partial_cmp(&left_throughput)
+                        .unwrap_or(std::cmp::Ordering::Equal),
+                )
+        })
+        .cloned()
+}
+
 /// Actions that can be performed on the benchmark state
 #[derive(Debug)]
 pub enum BenchmarkAction {
diff --git a/core/bench/dashboard/frontend/src/state/ui.rs b/core/bench/dashboard/frontend/src/state/ui.rs
index 57c0146d1..a1359b533 100644
--- a/core/bench/dashboard/frontend/src/state/ui.rs
+++ b/core/bench/dashboard/frontend/src/state/ui.rs
@@ -16,15 +16,117 @@
 // under the License.
 
 use crate::components::selectors::measurement_type_selector::MeasurementType;
+use bench_dashboard_shared::BenchmarkReportLight;
 use std::rc::Rc;
 use yew::prelude::*;
 
 #[derive(Clone, Debug, PartialEq)]
 #[allow(dead_code)]
 pub enum ViewMode {
-    SingleGitref,     // View detailed performance for a specific gitref
-    GitrefTrend,      // View performance trends across all gitrefs
-    RecentBenchmarks, // View recently added benchmarks
+    SingleGitref,
+    GitrefTrend,
+    RecentBenchmarks,
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct ParamRange {
+    pub from: Option<u32>,
+    pub to: Option<u32>,
+}
+
+impl ParamRange {
+    pub fn is_active(&self) -> bool {
+        self.from.is_some() || self.to.is_some()
+    }
+
+    pub fn matches(&self, value: u32) -> bool {
+        self.from.is_none_or(|f| value >= f) && self.to.is_none_or(|t| value <= t)
+    }
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct MetricRange {
+    pub from: Option<f64>,
+    pub to: Option<f64>,
+}
+
+impl MetricRange {
+    pub fn is_active(&self) -> bool {
+        self.from.is_some() || self.to.is_some()
+    }
+
+    pub fn matches(&self, value: f64) -> bool {
+        self.from.is_none_or(|f| value >= f) && self.to.is_none_or(|t| value <= t)
+    }
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct ParamFilters {
+    pub producers: ParamRange,
+    pub consumers: ParamRange,
+    pub streams: ParamRange,
+    pub partitions: ParamRange,
+    pub consumer_groups: ParamRange,
+    pub throughput_mb_s: MetricRange,
+    pub p99_latency_ms: MetricRange,
+}
+
+impl ParamFilters {
+    pub fn active_count(&self) -> usize {
+        let u32_active = [
+            &self.producers,
+            &self.consumers,
+            &self.streams,
+            &self.partitions,
+            &self.consumer_groups,
+        ]
+        .iter()
+        .filter(|r| r.is_active())
+        .count();
+        let metric_active = [&self.throughput_mb_s, &self.p99_latency_ms]
+            .iter()
+            .filter(|r| r.is_active())
+            .count();
+        u32_active + metric_active
+    }
+
+    pub fn is_any_active(&self) -> bool {
+        self.active_count() > 0
+    }
+
+    pub fn matches(&self, benchmark: &BenchmarkReportLight) -> bool {
+        let p = &benchmark.params;
+        let params_ok = self.producers.matches(p.producers)
+            && self.consumers.matches(p.consumers)
+            && self.streams.matches(p.streams)
+            && self.partitions.matches(p.partitions)
+            && self.consumer_groups.matches(p.consumer_groups);
+        if !params_ok {
+            return false;
+        }
+        let metrics_active = self.throughput_mb_s.is_active() || self.p99_latency_ms.is_active();
+        let Some(summary) = benchmark.group_metrics.first().map(|m| &m.summary) else {
+            return !metrics_active;
+        };
+        self.throughput_mb_s
+            .matches(summary.total_throughput_megabytes_per_second)
+            && self.p99_latency_ms.matches(summary.average_p99_latency_ms)
+    }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ParamField {
+    Producers,
+    Consumers,
+    Streams,
+    Partitions,
+    ConsumerGroups,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MetricField {
+    ThroughputMbS,
+    P99LatencyMs,
 }
 
 #[derive(Clone, Debug, PartialEq)]
@@ -34,6 +136,12 @@ pub struct UiState {
     pub is_benchmark_tooltip_visible: bool,
     pub is_server_stats_tooltip_visible: bool,
     pub is_embed_modal_visible: bool,
+    pub param_filters: ParamFilters,
+    pub is_sidebar_collapsed: bool,
+    pub compare_pin: Option<Box<BenchmarkReportLight>>,
+    /// Overrides chart rendering to keep the hero landing on screen until the user picks a benchmark.
+    /// Reducer never mutates selected_benchmark for this; the flag only gates rendering in MainContent.
+    pub is_landing: bool,
 }
 
 impl Default for UiState {
@@ -44,6 +152,10 @@ impl Default for UiState {
             is_benchmark_tooltip_visible: false,
             is_server_stats_tooltip_visible: false,
             is_embed_modal_visible: false,
+            param_filters: ParamFilters::default(),
+            is_sidebar_collapsed: false,
+            compare_pin: None,
+            is_landing: true,
         }
     }
 }
@@ -54,6 +166,12 @@ pub enum UiAction {
     ToggleServerStatsTooltip,
     ToggleEmbedModal,
     SetViewMode(ViewMode),
+    SetParamRange(ParamField, ParamRange),
+    SetMetricRange(MetricField, MetricRange),
+    ClearParamFilters,
+    ToggleSidebar,
+    SetComparePin(Option<Box<BenchmarkReportLight>>),
+    SetLanding(bool),
 }
 
 impl Reducible for UiState {
@@ -81,6 +199,47 @@ impl Reducible for UiState {
                 view_mode: vm,
                 ..(*self).clone()
             },
+            UiAction::SetParamRange(field, range) => {
+                let mut filters = self.param_filters.clone();
+                match field {
+                    ParamField::Producers => filters.producers = range,
+                    ParamField::Consumers => filters.consumers = range,
+                    ParamField::Streams => filters.streams = range,
+                    ParamField::Partitions => filters.partitions = range,
+                    ParamField::ConsumerGroups => filters.consumer_groups = range,
+                }
+                UiState {
+                    param_filters: filters,
+                    ..(*self).clone()
+                }
+            }
+            UiAction::SetMetricRange(field, range) => {
+                let mut filters = self.param_filters.clone();
+                match field {
+                    MetricField::ThroughputMbS => filters.throughput_mb_s = range,
+                    MetricField::P99LatencyMs => filters.p99_latency_ms = range,
+                }
+                UiState {
+                    param_filters: filters,
+                    ..(*self).clone()
+                }
+            }
+            UiAction::ClearParamFilters => UiState {
+                param_filters: ParamFilters::default(),
+                ..(*self).clone()
+            },
+            UiAction::ToggleSidebar => UiState {
+                is_sidebar_collapsed: !self.is_sidebar_collapsed,
+                ..(*self).clone()
+            },
+            UiAction::SetComparePin(pin) => UiState {
+                compare_pin: pin,
+                ..(*self).clone()
+            },
+            UiAction::SetLanding(is_landing) => UiState {
+                is_landing,
+                ..(*self).clone()
+            },
         };
         next.into()
     }
diff --git a/scripts/dashboard/run_dev.sh b/scripts/dashboard/run_dev.sh
index 02606bfb3..5fb8c83ac 100755
--- a/scripts/dashboard/run_dev.sh
+++ b/scripts/dashboard/run_dev.sh
@@ -20,10 +20,11 @@
 # Exit on any error
 set -euo pipefail
 
-# Check if performance_results directory exists
-if [ ! -d "${PWD}/performance_results" ]; then
-    echo "Error: 'performance_results' directory does not exist."
-    echo "Please run benchmarks with \`output\` subcommand first to generate performance results."
+RESULTS_DIR="${RESULTS_DIR:-${PWD}/performance_results}"
+
+if [ ! -d "$RESULTS_DIR" ]; then
+    echo "Error: results directory does not exist: $RESULTS_DIR"
+    echo "Set RESULTS_DIR=/path/to/performance_results or run benchmarks with \`output\` subcommand first."
     exit 1
 fi
 
@@ -56,8 +57,8 @@ if [ $# -gt 0 ]; then
 fi
 
 # Start server in background
-echo "Starting server..."
-cargo run --bin iggy-bench-dashboard-server &
+echo "Starting server (results: $RESULTS_DIR)..."
+cargo run --bin iggy-bench-dashboard-server -- --results-dir "$RESULTS_DIR" &
 
 # Wait a bit for server to start
 sleep 1