More consistent split panels and tabs
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 8210721..6d80735 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -33,6 +33,10 @@
   -webkit-tap-highlight-color: transparent;
 }
 
+.pf-muted {
+  color: var(--pf-color-text-muted);
+}
+
 html {
   font-size: 16px;
   font-family: var(--pf-font);
diff --git a/ui/src/assets/theme_provider.scss b/ui/src/assets/theme_provider.scss
index f93a0a0..26dc955 100644
--- a/ui/src/assets/theme_provider.scss
+++ b/ui/src/assets/theme_provider.scss
@@ -36,11 +36,11 @@
     --pf-color-text-hint: #808080;
     --pf-color-box-shadow: rgba(0, 0, 0, 0.2);
     --pf-color-neutral: gray;
-    --pf-color-accent: #2667e7;
+    --pf-color-accent: #3d7cc4;
     --pf-color-text-on-accent: white;
     --pf-color-highlight: #ffe263;
 
-    --pf-color-primary: #3d5688;
+    --pf-color-primary: #3d7cc4;
     --pf-color-text-on-primary: white;
     --pf-color-danger: rgb(202, 38, 38);
     --pf-color-text-on-danger: white;
@@ -64,8 +64,8 @@
   }
 
   &--dark {
-    --pf-color-background: #232426;
-    --pf-color-background-secondary: #383a3e;
+    --pf-color-background: #1d1d1d;
+    --pf-color-background-secondary: #2d2d2d;
     --pf-color-interactive-base: white;
     --pf-color-text: #dce0e2;
     --pf-color-text-muted: #a0a2a5;
@@ -75,11 +75,11 @@
     --pf-color-text-hint: #9aa0a6;
     --pf-color-box-shadow: rgba(0, 0, 0, 0.4);
     --pf-color-neutral: gray;
-    --pf-color-accent: #2667e7;
+    --pf-color-accent: #3d7cc4;
     --pf-color-text-on-accent: white;
     --pf-color-highlight: #5f4d06;
 
-    --pf-color-primary: #7197e3;
+    --pf-color-primary: #4a8ad4;
     --pf-color-text-on-primary: #333;
     --pf-color-danger: rgb(230, 90, 90);
     --pf-color-text-on-danger: #333;
diff --git a/ui/src/assets/timeline_page.scss b/ui/src/assets/timeline_page.scss
index 032ef29..f590d40 100644
--- a/ui/src/assets/timeline_page.scss
+++ b/ui/src/assets/timeline_page.scss
@@ -16,6 +16,7 @@
   isolation: isolate;
   overflow: hidden;
   height: 100%;
+  font-size: 14px;
 
   .pf-resize-handle {
     flex-shrink: 0;
diff --git a/ui/src/assets/widgets/split_panel.scss b/ui/src/assets/widgets/split_panel.scss
index d9f3ceb..2fb9898 100644
--- a/ui/src/assets/widgets/split_panel.scss
+++ b/ui/src/assets/widgets/split_panel.scss
@@ -12,94 +12,100 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-@import "../theme";
-
 .pf-split-panel {
-  height: 100%;
   display: flex;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+
+.pf-split-horizontal {
+  flex-direction: row;
+}
+
+.pf-split-vertical {
   flex-direction: column;
+}
 
-  &__main {
-    flex: 1; // Fill the remaining space
-    overflow: auto;
+.pf-split-panel__first,
+.pf-split-panel__second {
+  overflow: hidden;
+  min-width: 0;
+  min-height: 0;
+}
 
-    // Making this a flex element allows multiple individually overflowing
-    // sections to be added to the main content area.
-    display: flex;
-    flex-direction: column; // Users normally expect content to stack vertically
-  }
+.pf-split-panel__handle {
+  flex-shrink: 0;
+  background-color: var(--pf-color-background-secondary);
+  touch-action: none;
+}
 
-  &__handle {
-    gap: 2px;
-    background-color: var(--pf-color-background-secondary);
-    border-top: 1px solid var(--pf-color-border);
-    cursor: row-resize;
-    display: flex;
-    align-items: baseline;
-    padding-inline: 2px;
-    align-items: center;
-  }
+.pf-split-horizontal > .pf-split-panel__handle {
+  width: 4px;
+  cursor: col-resize;
+  position: relative;
+}
 
-  &__tabs {
-    padding-inline: 3px; // Leave some space for the drop shadow
-    display: flex;
-    overflow: hidden;
-    flex: 1; // Expand to fill remaining space.
+// Larger hit area for easier grabbing
+.pf-split-horizontal > .pf-split-panel__handle::before {
+  content: "";
+  position: absolute;
+  inset: 0 -6px;
+}
 
-    // Align tabs to the bottom of the handle & overlap the drawer border so
-    // that the active tab looks like it is part of the drawer.
-    align-self: flex-end;
-    margin-bottom: -1px;
-  }
+.pf-split-vertical > .pf-split-panel__handle {
+  height: 4px;
+  cursor: row-resize;
+  position: relative;
+}
 
-  &__tab {
-    display: flex;
-    align-items: baseline;
-    gap: 2px;
+// Larger hit area for easier grabbing
+.pf-split-vertical > .pf-split-panel__handle::before {
+  content: "";
+  position: absolute;
+  inset: -6px 0;
+}
 
-    font-family: var(--pf-font-compact);
-    font-size: 14px;
+.pf-split-panel__handle:hover,
+.pf-split-panel__handle.pf-split-handle--active {
+  background-color: var(--pf-color-accent);
+}
 
-    padding: 5px; // Space around the tab content
-    margin-top: 2px; // Space between tab and top of handle
-    cursor: pointer; // Active tab is not clickable
+// Grip pattern indicator
+.pf-split-panel__handle::after {
+  content: "";
+  display: block;
+  position: absolute;
+}
 
-    border-radius: 3px 3px 0 0; // Rounded top corners only
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-    border-style: solid;
-    border-color: var(--pf-color-border-secondary);
-    border-width: 1px 1px 0 1px;
+.pf-split-horizontal > .pf-split-panel__handle::after {
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 2px;
+  height: 24px;
+  background: repeating-linear-gradient(
+    to bottom,
+    var(--pf-color-text-muted) 0px,
+    var(--pf-color-text-muted) 2px,
+    transparent 2px,
+    transparent 4px
+  );
+  opacity: 0.5;
+}
 
-    &:not(:first-child) {
-      // Merge borders
-      margin-left: -1px;
-    }
-
-    &:hover {
-      background: color_hover(transparent);
-    }
-
-    &.pf-split-panel__tab--active {
-      cursor: default;
-      background-color: var(--pf-color-background);
-      box-shadow: var(--pf-color-box-shadow) 0px 0px 3px;
-      border-color: var(--pf-color-border);
-      z-index: 1;
-    }
-  }
-
-  &__tab-title {
-    margin: 0px 4px;
-    overflow: hidden;
-    user-select: none;
-  }
-
-  &__drawer {
-    flex-shrink: 0;
-    overflow: auto;
-    box-shadow: var(--pf-color-box-shadow) 0px 0px 3px;
-    border-top: 1px solid var(--pf-color-border);
-  }
+.pf-split-vertical > .pf-split-panel__handle::after {
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 24px;
+  height: 2px;
+  background: repeating-linear-gradient(
+    to right,
+    var(--pf-color-text-muted) 0px,
+    var(--pf-color-text-muted) 2px,
+    transparent 2px,
+    transparent 4px
+  );
+  opacity: 0.5;
 }
diff --git a/ui/src/assets/widgets/tabs.scss b/ui/src/assets/widgets/tabs.scss
index 29ef45c..792aa36 100644
--- a/ui/src/assets/widgets/tabs.scss
+++ b/ui/src/assets/widgets/tabs.scss
@@ -14,45 +14,74 @@
 
 .pf-tabs {
   display: flex;
-  align-items: baseline;
+  flex-direction: column;
+  min-height: 0;
+  background-color: var(--pf-color-background);
+  font-family: var(--pf-font-compact);
+
+  &__header {
+    display: flex;
+    align-items: center;
+    border-bottom: 1px solid var(--pf-color-border);
+    padding: 4px 4px 0;
+    flex-shrink: 0;
+  }
+
+  &__left-content {
+    display: flex;
+    align-items: center;
+    flex-shrink: 0;
+  }
 
   &__tabs {
     display: flex;
-    overflow: hidden;
+    gap: 1px;
     flex: 1;
+    min-width: 0;
+  }
+
+  &__right-content {
+    display: flex;
+    align-items: center;
+    flex-shrink: 0;
   }
 
   &__tab {
-    color: var(--pf-color-text-muted);
-    padding: 4px;
-    margin-top: 3px;
+    user-select: none;
+    display: inline-flex;
     align-items: center;
+    gap: 4px;
+    padding: 3px 8px;
+    color: var(--pf-color-text-muted);
+    background: transparent;
+    border: 1px solid transparent;
+    border-bottom: none;
+    border-radius: 4px 4px 0 0;
     cursor: pointer;
     overflow: hidden;
     white-space: nowrap;
     text-overflow: ellipsis;
-    display: flex;
-    align-items: baseline;
-    gap: 2px;
 
     &:hover {
+      color: var(--pf-color-text);
       background-color: color_hover(transparent);
     }
 
     &[active] {
-      color: var(--pf-color-primary);
-      border-bottom: solid 2px var(--pf-color-primary);
+      color: var(--pf-color-text);
+      background-color: var(--pf-color-background-secondary);
+      border-color: var(--pf-color-border);
+      position: relative;
+      z-index: 1;
+      margin-bottom: -1px;
+      padding-bottom: 4px;
       cursor: default;
     }
-
-    &:nth-child(1) {
-      margin-left: 3px;
-    }
   }
 
   &__tab-title {
-    margin: 0px 4px;
     overflow: hidden;
+    text-overflow: ellipsis;
   }
 
   &__tab-icon {
@@ -66,4 +95,11 @@
       align-self: center;
     }
   }
+
+  &__content {
+    flex: 1;
+    overflow: auto;
+    min-height: 0;
+    background-color: var(--pf-color-background-secondary);
+  }
 }
diff --git a/ui/src/components/details/sql_table_tab.ts b/ui/src/components/details/sql_table_tab.ts
index 2547dc2..c1cc6df 100644
--- a/ui/src/components/details/sql_table_tab.ts
+++ b/ui/src/components/details/sql_table_tab.ts
@@ -36,7 +36,7 @@
 import {SqlBarChart, SqlBarChartState} from '../widgets/charts/sql_bar_chart';
 import {SqlHistogram, SqlHistogramState} from '../widgets/charts/sql_histogram';
 import {sqlColumnId} from '../widgets/sql/table/sql_column';
-import {TabOption, TabStrip} from '../../widgets/tabs';
+import {Tab as TabDef, Tabs} from '../../widgets/tabs';
 import {Gate} from '../../base/mithril_utils';
 import {isQuantitativeType} from '../../trace_processor/perfetto_sql_type';
 
@@ -194,7 +194,7 @@
   render() {
     const hasFilters = this.tableState.filters.get().length > 0;
 
-    const tabs: (TabOption & {content: m.Children})[] = [
+    const tabs: TabDef[] = [
       {
         key: this.tableState.uuid,
         title: 'Table',
@@ -298,7 +298,7 @@
           m('.pf-sql-table__toolbar', [
             hasFilters && renderFilters(this.tableState.filters),
             tabs.length > 1 &&
-              m(TabStrip, {
+              m(Tabs, {
                 tabs,
                 currentTabKey: this.selectedTab,
                 onTabChange: (key) => (this.selectedTab = key),
diff --git a/ui/src/core/tab_manager.ts b/ui/src/core/tab_manager.ts
index 94306cd..53bd8a4 100644
--- a/ui/src/core/tab_manager.ts
+++ b/ui/src/core/tab_manager.ts
@@ -12,19 +12,34 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {assertUnreachable} from '../base/logging';
 import {DetailsPanel} from '../public/details_panel';
 import {TabDescriptor, TabManager} from '../public/tab';
-import {
-  SplitPanelDrawerVisibility,
-  toggleVisibility,
-} from '../widgets/split_panel';
 
 export interface ResolvedTab {
   uri: string;
   tab?: TabDescriptor;
 }
 
-export type TabPanelVisibility = 'COLLAPSED' | 'VISIBLE' | 'FULLSCREEN';
+export enum TabPanelVisibility {
+  VISIBLE,
+  FULLSCREEN,
+  COLLAPSED,
+}
+
+export function toggleTabPanelVisibility(
+  visibility: TabPanelVisibility,
+): TabPanelVisibility {
+  switch (visibility) {
+    case TabPanelVisibility.COLLAPSED:
+    case TabPanelVisibility.FULLSCREEN:
+      return TabPanelVisibility.VISIBLE;
+    case TabPanelVisibility.VISIBLE:
+      return TabPanelVisibility.COLLAPSED;
+    default:
+      assertUnreachable(visibility);
+  }
+}
 
 /**
  * Stores tab & current selection section registries.
@@ -37,7 +52,7 @@
   private _instantiatedTabs = new Map<string, TabDescriptor>();
   private _openTabs: string[] = []; // URIs of the tabs open.
   private _currentTab: string = 'current_selection';
-  private _tabPanelVisibility = SplitPanelDrawerVisibility.COLLAPSED;
+  private _tabPanelVisibility = TabPanelVisibility.COLLAPSED;
   private _tabPanelVisibilityChanged = false;
 
   [Symbol.dispose]() {
@@ -86,9 +101,9 @@
     // they are.
     if (
       !this._tabPanelVisibilityChanged &&
-      this._tabPanelVisibility === SplitPanelDrawerVisibility.COLLAPSED
+      this._tabPanelVisibility === TabPanelVisibility.COLLAPSED
     ) {
-      this.setTabPanelVisibility(SplitPanelDrawerVisibility.VISIBLE);
+      this.setTabPanelVisibility(TabPanelVisibility.VISIBLE);
     }
   }
 
@@ -191,13 +206,15 @@
     return tabs;
   }
 
-  setTabPanelVisibility(visibility: SplitPanelDrawerVisibility): void {
+  setTabPanelVisibility(visibility: TabPanelVisibility): void {
     this._tabPanelVisibility = visibility;
     this._tabPanelVisibilityChanged = true;
   }
 
   toggleTabPanelVisibility(): void {
-    this.setTabPanelVisibility(toggleVisibility(this._tabPanelVisibility));
+    this.setTabPanelVisibility(
+      toggleTabPanelVisibility(this._tabPanelVisibility),
+    );
   }
 
   get tabPanelVisibility() {
diff --git a/ui/src/core_plugins/dev.perfetto.MultiTraceOpen/multi_trace_modal.ts b/ui/src/core_plugins/dev.perfetto.MultiTraceOpen/multi_trace_modal.ts
index b6306a4..b41b329 100644
--- a/ui/src/core_plugins/dev.perfetto.MultiTraceOpen/multi_trace_modal.ts
+++ b/ui/src/core_plugins/dev.perfetto.MultiTraceOpen/multi_trace_modal.ts
@@ -23,7 +23,7 @@
 import {Callout} from '../../widgets/callout';
 import {Spinner} from '../../widgets/spinner';
 import {Stack} from '../../widgets/stack';
-import {TabStrip, TabOption} from '../../widgets/tabs';
+import {Tab, Tabs} from '../../widgets/tabs';
 import {TextParagraph} from '../../widgets/text_paragraph';
 import {MultiTraceController} from './multi_trace_controller';
 import {TraceFile} from './multi_trace_types';
@@ -67,7 +67,7 @@
   }
 
   private renderDescription() {
-    const tabs: TabOption[] = [
+    const tabs: Tab[] = [
       {key: 'synchronous', title: 'Synchronous Traces'},
       {key: 'cross-machine', title: 'Cross-Machine Traces'},
       {key: 'comparison', title: 'Trace Comparison'},
@@ -79,7 +79,7 @@
         className: 'pf-multi-trace-modal__description-panel',
         orientation: 'vertical',
       },
-      m(TabStrip, {
+      m(Tabs, {
         className: 'pf-multi-trace-modal__tabs',
         tabs,
         currentTabKey: this.currentTab,
diff --git a/ui/src/frontend/timeline_page/tab_panel.ts b/ui/src/frontend/timeline_page/tab_panel.ts
index a0f7d36..eacbacd 100644
--- a/ui/src/frontend/timeline_page/tab_panel.ts
+++ b/ui/src/frontend/timeline_page/tab_panel.ts
@@ -13,13 +13,17 @@
 // limitations under the License.
 
 import m from 'mithril';
+import {
+  TabPanelVisibility,
+  toggleTabPanelVisibility,
+} from '../../core/tab_manager';
 import {TraceImpl} from '../../core/trace_impl';
-import {Button} from '../../widgets/button';
+import {Button, ButtonBar} from '../../widgets/button';
 import {MenuItem, PopupMenu} from '../../widgets/menu';
-import {Tab, SplitPanel} from '../../widgets/split_panel';
+import {SplitPanel} from '../../widgets/split_panel';
+import {Tab, Tabs} from '../../widgets/tabs';
 import {DEFAULT_DETAILS_CONTENT_HEIGHT} from '../css_constants';
 import {CurrentSelectionTab} from './current_selection_tab';
-import {Gate} from '../../base/mithril_utils';
 
 export interface TabPanelAttrs {
   readonly trace: TraceImpl;
@@ -27,82 +31,117 @@
 }
 
 export class TabPanel implements m.ClassComponent<TabPanelAttrs> {
-  view({
-    attrs,
-    children,
-  }: m.Vnode<TabPanelAttrs, this>): m.Children | null | void {
-    const {tabs, drawerContent} = this.gatherTabs(attrs.trace);
+  private drawerHeight = DEFAULT_DETAILS_CONTENT_HEIGHT;
+  private containerHeight = 0;
 
-    return m(
-      SplitPanel,
-      {
-        className: attrs.className,
-        startingHeight: DEFAULT_DETAILS_CONTENT_HEIGHT,
-        leftHandleContent: this.renderDropdownMenu(attrs.trace),
-        tabs,
-        drawerContent,
-        visibility: attrs.trace.tabs.tabPanelVisibility,
-        onVisibilityChange: (visibility) =>
-          attrs.trace.tabs.setTabPanelVisibility(visibility),
+  view({attrs, children}: m.Vnode<TabPanelAttrs, this>): m.Children {
+    const trace = attrs.trace;
+    const visibility = trace.tabs.tabPanelVisibility;
+
+    // Calculate effective height based on visibility
+    let effectiveHeight: number;
+    switch (visibility) {
+      case TabPanelVisibility.COLLAPSED:
+        effectiveHeight = 0;
+        break;
+      case TabPanelVisibility.FULLSCREEN:
+        effectiveHeight = this.containerHeight;
+        break;
+      case TabPanelVisibility.VISIBLE:
+      default:
+        effectiveHeight = Math.min(
+          Math.max(this.drawerHeight, 0),
+          this.containerHeight,
+        );
+        break;
+    }
+
+    const tabs = this.buildTabs(trace);
+    const currentTabUri = trace.tabs.currentTabUri;
+
+    return m(SplitPanel, {
+      className: attrs.className,
+      direction: 'vertical',
+      split: {fixed: {panel: 'second', size: effectiveHeight}},
+      minSize: 0,
+      onResize: (size) => {
+        this.drawerHeight = size;
+        // When user resizes, switch to VISIBLE mode
+        if (visibility !== TabPanelVisibility.VISIBLE) {
+          trace.tabs.setTabPanelVisibility(TabPanelVisibility.VISIBLE);
+        }
       },
-      children,
-    );
+      firstPanel: children,
+      secondPanel: m(Tabs, {
+        tabs,
+        currentTabKey: currentTabUri,
+        onTabChange: (key) => trace.tabs.showTab(key),
+        leftContent: this.renderDropdownMenu(trace),
+        rightContent: this.renderVisibilityButtons(trace, visibility),
+        fillHeight: true,
+      }),
+    });
   }
 
-  private gatherTabs(trace: TraceImpl) {
+  oncreate(vnode: m.VnodeDOM<TabPanelAttrs, this>) {
+    this.setupResizeObserver(vnode);
+  }
+
+  onupdate(vnode: m.VnodeDOM<TabPanelAttrs, this>) {
+    // Re-measure container on update in case it changed
+    const container = vnode.dom as HTMLElement;
+    if (container.parentElement) {
+      this.containerHeight = container.parentElement.clientHeight;
+    }
+  }
+
+  private setupResizeObserver(vnode: m.VnodeDOM<TabPanelAttrs, this>) {
+    const container = vnode.dom as HTMLElement;
+    if (container.parentElement) {
+      this.containerHeight = container.parentElement.clientHeight;
+      const resizeObs = new ResizeObserver(() => {
+        this.containerHeight = container.parentElement!.clientHeight;
+        m.redraw();
+      });
+      resizeObs.observe(container.parentElement);
+    }
+  }
+
+  private buildTabs(trace: TraceImpl): Tab[] {
     const tabMan = trace.tabs;
     const tabList = trace.tabs.openTabsUri;
     const resolvedTabs = tabMan.resolveTabs(tabList);
     const currentTabUri = trace.tabs.currentTabUri;
 
-    const drawerContent: m.Child[] = [];
+    const tabs: Tab[] = [];
 
-    const tabs = resolvedTabs.map(({uri, tab: tabDesc}) => {
-      const active = uri === currentTabUri;
-      if (tabDesc) {
-        drawerContent.push(m(Gate, {open: active}, tabDesc.content.render()));
-        return m(
-          Tab,
-          {
-            active,
-            onclick: () => trace.tabs.showTab(uri),
-            hasCloseButton: true,
-            onClose: () => {
-              trace.tabs.hideTab(uri);
-            },
-          },
-          tabDesc.content.getTitle(),
-        );
-      } else {
-        return m(
-          Tab,
-          {
-            active,
-            onclick: () => trace.tabs.showTab(uri),
-          },
-          'Tab does not exist',
-        );
-      }
+    // Add the permanent current selection tab first
+    tabs.push({
+      key: 'current_selection',
+      title: 'Current Selection',
+      content: m(CurrentSelectionTab, {trace}),
     });
 
-    // Add the permanent current selection tab to the front of the list of tabs
-    const active = currentTabUri === 'current_selection';
-    drawerContent.unshift(
-      m(Gate, {open: active}, m(CurrentSelectionTab, {trace})),
-    );
+    // Add dynamic tabs
+    for (const {uri, tab: tabDesc} of resolvedTabs) {
+      if (tabDesc) {
+        tabs.push({
+          key: uri,
+          title: tabDesc.content.getTitle(),
+          content: currentTabUri === uri ? tabDesc.content.render() : undefined,
+          hasCloseButton: true,
+          onClose: () => trace.tabs.hideTab(uri),
+        });
+      } else {
+        tabs.push({
+          key: uri,
+          title: 'Tab does not exist',
+          content: undefined,
+        });
+      }
+    }
 
-    tabs.unshift(
-      m(
-        Tab,
-        {
-          active,
-          onclick: () => trace.tabs.showTab('current_selection'),
-        },
-        'Current Selection',
-      ),
-    );
-
-    return {tabs, drawerContent};
+    return tabs;
   }
 
   private renderDropdownMenu(trace: TraceImpl): m.Child {
@@ -136,4 +175,31 @@
       }),
     );
   }
+
+  private renderVisibilityButtons(
+    trace: TraceImpl,
+    visibility: TabPanelVisibility,
+  ): m.Child {
+    const isClosed = visibility === TabPanelVisibility.COLLAPSED;
+    return m(
+      ButtonBar,
+      m(Button, {
+        title: 'Open fullscreen',
+        disabled: visibility === TabPanelVisibility.FULLSCREEN,
+        icon: 'vertical_align_top',
+        onclick: () => {
+          trace.tabs.setTabPanelVisibility(TabPanelVisibility.FULLSCREEN);
+        },
+      }),
+      m(Button, {
+        onclick: () => {
+          trace.tabs.setTabPanelVisibility(
+            toggleTabPanelVisibility(visibility),
+          );
+        },
+        title: isClosed ? 'Show panel' : 'Hide panel',
+        icon: isClosed ? 'keyboard_arrow_up' : 'keyboard_arrow_down',
+      }),
+    );
+  }
 }
diff --git a/ui/src/frontend/timeline_page/timeline_page.ts b/ui/src/frontend/timeline_page/timeline_page.ts
index a9156e4..8a224f1 100644
--- a/ui/src/frontend/timeline_page/timeline_page.ts
+++ b/ui/src/frontend/timeline_page/timeline_page.ts
@@ -27,7 +27,7 @@
 import {KeyboardNavigationHandler} from './wasd_navigation_handler';
 import {trackMatchesFilter} from '../../core/track_manager';
 import {TraceImpl} from '../../core/trace_impl';
-import {ResizeHandle} from '../../widgets/resize_handle';
+import {SplitPanel} from '../../widgets/split_panel';
 
 const OVERVIEW_PANEL_FLAG = featureFlags.register({
   id: 'overviewVisible',
@@ -53,7 +53,7 @@
 class TimelinePage implements m.ClassComponent<TimelinePageAttrs> {
   private readonly trash = new DisposableStack();
   private timelineBounds?: Rect2D;
-  private pinnedTracksHeight: number | 'auto' = 'auto';
+  private pinnedTracksHeight = 200;
 
   view({attrs}: m.CVnode<TimelinePageAttrs>) {
     const {trace} = attrs;
@@ -76,51 +76,39 @@
           onTimelineBoundsChange: (rect) => (this.timelineBounds = rect),
         }),
         // Hide tracks while the trace is loading to prevent thrashing.
-        !AppImpl.instance.isLoadingTrace && [
-          // Don't render pinned tracks if we have none.
-          trace.currentWorkspace.pinnedTracks.length > 0 && [
-            m(
-              '.pf-timeline-page__pinned-track-tree',
-              {
-                style:
-                  this.pinnedTracksHeight === 'auto'
-                    ? {maxHeight: '40%'}
-                    : {height: `${this.pinnedTracksHeight}px`},
-              },
-              m(TrackTreeView, {
+        !AppImpl.instance.isLoadingTrace &&
+          (trace.currentWorkspace.pinnedTracks.length > 0
+            ? m(SplitPanel, {
+                direction: 'vertical',
+                split: {fixed: {panel: 'first', size: this.pinnedTracksHeight}},
+                minSize: 50,
+                onResize: (size: number) => {
+                  this.pinnedTracksHeight = size;
+                },
+                firstPanel: m(TrackTreeView, {
+                  trace,
+                  className: 'pf-timeline-page__pinned-track-tree',
+                  rootNode: trace.currentWorkspace.pinnedTracksNode,
+                  canReorderNodes: true,
+                  scrollToNewTracks: true,
+                }),
+                secondPanel: m(TrackTreeView, {
+                  trace,
+                  className: 'pf-timeline-page__scrolling-track-tree',
+                  rootNode: trace.currentWorkspace.tracks,
+                  canReorderNodes: trace.currentWorkspace.userEditable,
+                  canRemoveNodes: trace.currentWorkspace.userEditable,
+                  trackFilter: (track) => trackMatchesFilter(trace, track),
+                }),
+              })
+            : m(TrackTreeView, {
                 trace,
-                rootNode: trace.currentWorkspace.pinnedTracksNode,
-                canReorderNodes: true,
-                scrollToNewTracks: true,
-              }),
-            ),
-            m(ResizeHandle, {
-              onResize: (deltaPx: number) => {
-                if (this.pinnedTracksHeight === 'auto') {
-                  this.pinnedTracksHeight = toHTMLElement(
-                    document.querySelector(
-                      '.pf-timeline-page__pinned-track-tree',
-                    )!,
-                  ).getBoundingClientRect().height;
-                }
-                this.pinnedTracksHeight = this.pinnedTracksHeight + deltaPx;
-                m.redraw();
-              },
-              ondblclick: () => {
-                this.pinnedTracksHeight = 'auto';
-              },
-            }),
-          ],
-
-          m(TrackTreeView, {
-            trace,
-            className: 'pf-timeline-page__scrolling-track-tree',
-            rootNode: trace.currentWorkspace.tracks,
-            canReorderNodes: trace.currentWorkspace.userEditable,
-            canRemoveNodes: trace.currentWorkspace.userEditable,
-            trackFilter: (track) => trackMatchesFilter(trace, track),
-          }),
-        ],
+                className: 'pf-timeline-page__scrolling-track-tree',
+                rootNode: trace.currentWorkspace.tracks,
+                canReorderNodes: trace.currentWorkspace.userEditable,
+                canRemoveNodes: trace.currentWorkspace.userEditable,
+                trackFilter: (track) => trackMatchesFilter(trace, track),
+              })),
       ),
     );
   }
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.scss b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.scss
index c2d48fa..88d4ed3 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.scss
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.scss
@@ -36,34 +36,34 @@
 
   height: 100%;
 
-  // SplitPanel's main section needs to have the grid layout
-  .pf-split-panel__main {
-    display: grid;
-    grid-template-columns: 1fr auto auto auto;
-    overflow: hidden;
-  }
-
   .pf-qb-node-graph {
-    grid-column: 1;
     overflow: auto;
     position: relative;
+    height: 100%;
   }
 
-  // Position the resize handle in the grid, but let the widget handle its own styling
-  .pf-resize-handle {
-    grid-column: 2;
+  .pf-qb-results-panel {
+    overflow: auto;
+    height: 100%;
+  }
+
+  .pf-qb-sidebar {
+    display: flex;
+    flex-direction: row;
+    height: 100%;
+    overflow: hidden;
   }
 
   .pf-qb-explorer {
-    grid-column: 3;
     overflow: hidden;
     height: 100%;
+    flex-shrink: 0;
     // Width is controlled by inline style for smooth resizing
   }
 
   .pf-qb-side-panel {
-    grid-column: 4;
     width: var(--pf-qb-side-panel-width);
+    flex-shrink: 0;
     height: 100%;
     display: flex;
     flex-direction: column;
@@ -156,10 +156,6 @@
   }
 
   &.explorer-collapsed {
-    .pf-resize-handle {
-      display: none;
-    }
-
     .pf-qb-explorer {
       border-left: none;
       overflow: hidden;
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.ts b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.ts
index 0bd0bd5..4a3cf6a 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.ts
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.ts
@@ -65,7 +65,6 @@
 // - this.dataSource: Wrapped data source for DataGrid display
 
 import m from 'mithril';
-import {classNames} from '../../../base/classnames';
 import {Button} from '../../../widgets/button';
 import {Icons} from '../../../base/semantic_icons';
 import {Icon} from '../../../widgets/icon';
@@ -76,10 +75,7 @@
 import {NodeExplorer} from './node_explorer';
 import {Graph} from './graph/graph';
 import {DataExplorer} from './data_explorer';
-import {
-  SplitPanel,
-  SplitPanelDrawerVisibility,
-} from '../../../widgets/split_panel';
+import {SplitPanel} from '../../../widgets/split_panel';
 import {SQLDataSource} from '../../../components/widgets/datagrid/sql_data_source';
 import {createSimpleSchema} from '../../../components/widgets/datagrid/sql_schema';
 import {QueryResponse} from '../../../components/query_table/queries';
@@ -90,7 +86,6 @@
 import {DataExplorerEmptyState, RoundActionButton} from './widgets';
 import {UIFilter} from './operations/filter';
 import {QueryExecutionService} from './query_execution_service';
-import {ResizeHandle} from '../../../widgets/resize_handle';
 import {getAllDownstreamNodes} from './graph_utils';
 import {Popup, PopupPosition} from '../../../widgets/popup';
 import {DataSource} from '../../../components/widgets/datagrid/data_source';
@@ -194,12 +189,8 @@
   private previousSelectedNode?: QueryNode;
   private response?: QueryResponse;
   private dataSource?: DataSource;
-  private drawerVisibility = SplitPanelDrawerVisibility.COLLAPSED;
   private selectedView: SelectedView = SelectedView.kInfo;
-  private readonly MIN_SIDEBAR_WIDTH = 250;
-  private readonly MAX_SIDEBAR_WIDTH = 800;
   private readonly DEFAULT_SIDEBAR_WIDTH = 500;
-  private hasEverSelectedNode = false;
 
   constructor({attrs}: m.Vnode<BuilderAttrs>) {
     this.trace = attrs.trace;
@@ -207,18 +198,6 @@
     this.queryExecutionService = attrs.queryExecutionService;
   }
 
-  private handleSidebarResize(attrs: BuilderAttrs, deltaPx: number) {
-    const currentWidth = attrs.sidebarWidth ?? this.DEFAULT_SIDEBAR_WIDTH;
-    // Subtract delta because the handle is on the left edge of the sidebar
-    // Dragging left (negative delta) = narrower sidebar (positive change)
-    // Dragging right (positive delta) = wider sidebar (negative change)
-    const newWidth = Math.max(
-      this.MIN_SIDEBAR_WIDTH,
-      Math.min(this.MAX_SIDEBAR_WIDTH, currentWidth - deltaPx),
-    );
-    attrs.onSidebarWidthChange?.(newWidth);
-  }
-
   view({attrs}: m.CVnode<BuilderAttrs>) {
     const {trace, rootNodes, onNodeSelected, selectedNode, onClearAllNodes} =
       attrs;
@@ -228,12 +207,6 @@
       this.isQueryRunning = false;
       this.isAnalyzing = false;
 
-      // Show drawer the first time any node is selected
-      if (!this.hasEverSelectedNode) {
-        this.drawerVisibility = SplitPanelDrawerVisibility.VISIBLE;
-        this.hasEverSelectedNode = true;
-      }
-
       const hasModifyPanel = selectedNode.nodeSpecificModify() != null;
       // If current view is Info, switch to Modify (if available) when selecting a new node
       if (this.selectedView === SelectedView.kInfo && hasModifyPanel) {
@@ -251,17 +224,129 @@
     // When transitioning to unselected state with collapsed explorer, reappear at minimum size
     if (!selectedNode && this.previousSelectedNode && isExplorerCollapsed) {
       attrs.onExplorerCollapsedChange?.(false);
-      attrs.onSidebarWidthChange?.(this.MIN_SIDEBAR_WIDTH);
     }
 
     this.previousSelectedNode = selectedNode;
 
-    const layoutClasses =
-      classNames(
-        'pf-query-builder-layout',
-        isExplorerCollapsed && 'explorer-collapsed',
-      ) || '';
+    const layoutClasses = isExplorerCollapsed ? 'explorer-collapsed' : '';
 
+    // Node graph panel with floating controls
+    const graphPanel = m(
+      '.pf-qb-node-graph',
+      m(Graph, {
+        rootNodes,
+        selectedNode,
+        onNodeSelected,
+        nodeLayouts: attrs.nodeLayouts,
+        labels: attrs.labels,
+        onNodeLayoutChange: attrs.onNodeLayoutChange,
+        onLabelsChange: attrs.onLabelsChange,
+        onDeselect: attrs.onDeselect,
+        onAddSourceNode: attrs.onAddSourceNode,
+        onClearAllNodes,
+        onDuplicateNode: attrs.onDuplicateNode,
+        onAddOperationNode: (id, node) => attrs.onAddOperationNode(id, node),
+        onDeleteNode: attrs.onDeleteNode,
+        onConnectionRemove: attrs.onConnectionRemove,
+        onImport: attrs.onImport,
+        onExport: attrs.onExport,
+        onRecenterReady: attrs.onRecenterReady,
+      }),
+      selectedNode &&
+        m(
+          '.pf-qb-floating-controls',
+          !selectedNode.validate() &&
+            m(
+              Popup,
+              {
+                trigger: m(
+                  '.pf-qb-floating-warning',
+                  m(Icon, {
+                    icon: Icons.Warning,
+                    filled: true,
+                    className: 'pf-qb-warning-icon',
+                    title: 'Click to see error details',
+                  }),
+                ),
+                position: PopupPosition.BottomEnd,
+                showArrow: true,
+              },
+              m(
+                '.pf-error-details',
+                selectedNode.state.issues?.getTitle() ?? 'No error details',
+              ),
+            ),
+        ),
+      m(
+        '.pf-qb-floating-controls-bottom',
+        attrs.onUndo &&
+          RoundActionButton({
+            icon: Icons.Undo,
+            title: 'Undo (Ctrl+Z)',
+            onclick: attrs.onUndo,
+            disabled: !attrs.canUndo,
+          }),
+        attrs.onRedo &&
+          RoundActionButton({
+            icon: Icons.Redo,
+            title: 'Redo (Ctrl+Shift+Z)',
+            onclick: attrs.onRedo,
+            disabled: !attrs.canRedo,
+          }),
+      ),
+    );
+
+    // Results panel (DataExplorer)
+    const resultsPanel = m(
+      '.pf-qb-results-panel',
+      selectedNode
+        ? m(DataExplorer, {
+            trace: this.trace,
+            query: this.query,
+            node: selectedNode,
+            response: this.response,
+            dataSource: this.dataSource,
+            isQueryRunning: this.isQueryRunning,
+            isAnalyzing: this.isAnalyzing,
+            onchange: () => {
+              attrs.onNodeStateChange?.();
+            },
+            onFilterAdd: (filter, filterOperator) => {
+              attrs.onFilterAdd(selectedNode, filter, filterOperator);
+            },
+            isFullScreen: false,
+            onFullScreenToggle: () => {},
+            onExecute: async () => {
+              if (!selectedNode.validate()) {
+                console.warn(
+                  `Cannot execute query: node ${selectedNode.nodeId} failed validation`,
+                );
+                return;
+              }
+
+              // Use the centralized service with manual=true.
+              // The service handles both analysis and execution.
+              await this.queryExecutionService.processNode(
+                selectedNode,
+                this.trace.engine,
+                {
+                  manual: true, // User explicitly clicked "Run Query"
+                  hasExistingResult: this.queryExecuted,
+                  ...this.createManualExecutionCallbacks(selectedNode),
+                },
+              );
+            },
+            onExportToTimeline: () => {
+              this.exportToTimeline(selectedNode);
+            },
+          })
+        : m(DataExplorerEmptyState, {
+            icon: 'info',
+            title: 'Select a node to see the data',
+          }),
+    );
+
+    // Explorer sidebar
     const explorer = selectedNode
       ? m(NodeExplorer, {
           // The key to force mithril to re-create the component when the
@@ -322,146 +407,14 @@
           }),
         );
 
-    return m(
-      SplitPanel,
-      {
-        className: layoutClasses,
-        visibility: this.drawerVisibility,
-        onVisibilityChange: (v) => {
-          this.drawerVisibility = v;
-        },
-        startingHeight: 300,
-        drawerContent: selectedNode
-          ? m(DataExplorer, {
-              trace: this.trace,
-              query: this.query,
-              node: selectedNode,
-              response: this.response,
-              dataSource: this.dataSource,
-              isQueryRunning: this.isQueryRunning,
-              isAnalyzing: this.isAnalyzing,
-              onchange: () => {
-                attrs.onNodeStateChange?.();
-              },
-              onFilterAdd: (filter, filterOperator) => {
-                attrs.onFilterAdd(selectedNode, filter, filterOperator);
-              },
-              isFullScreen:
-                this.drawerVisibility === SplitPanelDrawerVisibility.FULLSCREEN,
-              onFullScreenToggle: () => {
-                if (
-                  this.drawerVisibility ===
-                  SplitPanelDrawerVisibility.FULLSCREEN
-                ) {
-                  this.drawerVisibility = SplitPanelDrawerVisibility.VISIBLE;
-                } else {
-                  this.drawerVisibility = SplitPanelDrawerVisibility.FULLSCREEN;
-                }
-              },
-              onExecute: async () => {
-                if (!selectedNode.validate()) {
-                  console.warn(
-                    `Cannot execute query: node ${selectedNode.nodeId} failed validation`,
-                  );
-                  return;
-                }
-
-                // Use the centralized service with manual=true.
-                // The service handles both analysis and execution.
-                await this.queryExecutionService.processNode(
-                  selectedNode,
-                  this.trace.engine,
-                  {
-                    manual: true, // User explicitly clicked "Run Query"
-                    hasExistingResult: this.queryExecuted,
-                    ...this.createManualExecutionCallbacks(selectedNode),
-                  },
-                );
-              },
-              onExportToTimeline: () => {
-                this.exportToTimeline(selectedNode);
-              },
-            })
-          : m(DataExplorerEmptyState, {
-              icon: 'info',
-              title: 'Select a node to see the data',
-            }),
-      },
-      m(
-        '.pf-qb-node-graph',
-        m(Graph, {
-          rootNodes,
-          selectedNode,
-          onNodeSelected,
-          nodeLayouts: attrs.nodeLayouts,
-          labels: attrs.labels,
-          onNodeLayoutChange: attrs.onNodeLayoutChange,
-          onLabelsChange: attrs.onLabelsChange,
-          onDeselect: attrs.onDeselect,
-          onAddSourceNode: attrs.onAddSourceNode,
-          onClearAllNodes,
-          onDuplicateNode: attrs.onDuplicateNode,
-          onAddOperationNode: (id, node) => attrs.onAddOperationNode(id, node),
-          onDeleteNode: attrs.onDeleteNode,
-          onConnectionRemove: attrs.onConnectionRemove,
-          onImport: attrs.onImport,
-          onExport: attrs.onExport,
-          onRecenterReady: attrs.onRecenterReady,
-        }),
-        selectedNode &&
-          m(
-            '.pf-qb-floating-controls',
-            !selectedNode.validate() &&
-              m(
-                Popup,
-                {
-                  trigger: m(
-                    '.pf-qb-floating-warning',
-                    m(Icon, {
-                      icon: Icons.Warning,
-                      filled: true,
-                      className: 'pf-qb-warning-icon',
-                      title: 'Click to see error details',
-                    }),
-                  ),
-                  position: PopupPosition.BottomEnd,
-                  showArrow: true,
-                },
-                m(
-                  '.pf-error-details',
-                  selectedNode.state.issues?.getTitle() ?? 'No error details',
-                ),
-              ),
-          ),
-        m(
-          '.pf-qb-floating-controls-bottom',
-          attrs.onUndo &&
-            RoundActionButton({
-              icon: Icons.Undo,
-              title: 'Undo (Ctrl+Z)',
-              onclick: attrs.onUndo,
-              disabled: !attrs.canUndo,
-            }),
-          attrs.onRedo &&
-            RoundActionButton({
-              icon: Icons.Redo,
-              title: 'Redo (Ctrl+Shift+Z)',
-              onclick: attrs.onRedo,
-              disabled: !attrs.canRedo,
-            }),
-        ),
-      ),
-      m(ResizeHandle, {
-        direction: 'horizontal',
-        onResize: (deltaPx) => this.handleSidebarResize(attrs, deltaPx),
-      }),
+    // Sidebar panel (explorer + side panel buttons)
+    const sidebarPanel = m(
+      '.pf-qb-sidebar',
       m(
         '.pf-qb-explorer',
         {
           style: {
-            width: isExplorerCollapsed
-              ? '0'
-              : `${sidebarWidth + (selectedNode ? 0 : SIDE_PANEL_WIDTH)}px`,
+            width: isExplorerCollapsed ? '0' : `${sidebarWidth}px`,
           },
         },
         explorer,
@@ -530,6 +483,41 @@
           }),
         ),
     );
+
+    // Main content: vertical split (graph top, results bottom)
+    const mainContent = m(SplitPanel, {
+      direction: 'vertical',
+      split: {percent: 60},
+      minSize: 100,
+      firstPanel: graphPanel,
+      secondPanel: resultsPanel,
+    });
+
+    // Full layout: horizontal split (main content left, sidebar right)
+    return m(
+      '.pf-query-builder-layout',
+      {className: layoutClasses},
+      m(SplitPanel, {
+        direction: 'horizontal',
+        split: {
+          fixed: {
+            panel: 'second',
+            size: isExplorerCollapsed
+              ? SIDE_PANEL_WIDTH
+              : sidebarWidth + SIDE_PANEL_WIDTH,
+          },
+        },
+        minSize: SIDE_PANEL_WIDTH,
+        firstPanel: mainContent,
+        secondPanel: sidebarPanel,
+        onResize: (size: number) => {
+          const newWidth = size - SIDE_PANEL_WIDTH;
+          if (newWidth > 0) {
+            attrs.onSidebarWidthChange?.(newWidth);
+          }
+        },
+      }),
+    );
   }
 
   private resolveNode(
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/graph/graph.scss b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/graph/graph.scss
index 0ef4086..45e026f 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/graph/graph.scss
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/graph/graph.scss
@@ -18,8 +18,7 @@
   position: relative;
   height: 100%;
   overflow: hidden;
-  border: 1px solid var(--pf-color-border);
-  background-color: var(--pf-color-background);
+  // background-color: var(--pf-color-background);
 
   // Ensure NodeGraph takes full height
   .pf-canvas {
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/node_explorer.scss b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/node_explorer.scss
index ef8db03..a25205d 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/node_explorer.scss
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/node_explorer.scss
@@ -18,7 +18,6 @@
   display: flex;
   flex-direction: column;
   height: 100%;
-  border-left: 1px solid var(--pf-color-border);
 
   &__header {
     display: flex;
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/node_explorer.ts b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/node_explorer.ts
index 290f8ff..acaa6ba 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/node_explorer.ts
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/node_explorer.ts
@@ -22,7 +22,7 @@
 import {CodeSnippet} from '../../../widgets/code_snippet';
 import {AggregationNode} from './nodes/aggregation_node';
 import {NodeIssues} from './node_issues';
-import {TabStrip} from '../../../widgets/tabs';
+import {Tabs} from '../../../widgets/tabs';
 import {NodeModifyAttrs} from './node_explorer_types';
 import {Button, ButtonAttrs, ButtonVariant} from '../../../widgets/button';
 import {DataExplorerEmptyState, InfoBox} from './widgets';
@@ -284,7 +284,7 @@
       selectedView === SelectedView.kModify && this.renderModifyView(node),
       selectedView === SelectedView.kResult &&
         m('.', [
-          m(TabStrip, {
+          m(Tabs, {
             tabs: [
               {key: 'sql', title: 'SQL'},
               {key: 'proto', title: 'Proto'},
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/styles.scss b/ui/src/plugins/dev.perfetto.ExplorePage/styles.scss
index 4f8d731..df026f8 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/styles.scss
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/styles.scss
@@ -23,7 +23,6 @@
   height: 100%;
   position: relative;
   overflow: auto;
-  padding: 0.25rem;
 
   &__header {
     display: flex;
diff --git a/ui/src/plugins/dev.perfetto.PprofProfiles/pprof_page.ts b/ui/src/plugins/dev.perfetto.PprofProfiles/pprof_page.ts
index 789c05a..2650820 100644
--- a/ui/src/plugins/dev.perfetto.PprofProfiles/pprof_page.ts
+++ b/ui/src/plugins/dev.perfetto.PprofProfiles/pprof_page.ts
@@ -25,7 +25,7 @@
 import {Stack, StackAuto, StackFixed} from '../../widgets/stack';
 import {EmptyState} from '../../widgets/empty_state';
 import {Callout} from '../../widgets/callout';
-import {TabStrip} from '../../widgets/tabs';
+import {Tabs} from '../../widgets/tabs';
 import {Icon} from '../../widgets/icon';
 import {Flamegraph} from '../../widgets/flamegraph';
 import {PprofProfile, PprofPageState} from './types';
@@ -138,7 +138,7 @@
     const showViewExplanation = this.shouldShowExplanation(
       HIDE_VIEW_EXPLANATION_KEY,
     );
-    return m(TabStrip, {
+    return m(Tabs, {
       className: 'pf-pprof-page__tabs',
       tabs: [
         {
diff --git a/ui/src/plugins/dev.perfetto.QueryPage/query_page.scss b/ui/src/plugins/dev.perfetto.QueryPage/query_page.scss
index 14386cc..6c88e10 100644
--- a/ui/src/plugins/dev.perfetto.QueryPage/query_page.scss
+++ b/ui/src/plugins/dev.perfetto.QueryPage/query_page.scss
@@ -17,23 +17,40 @@
   font-family: var(--pf-font-compact);
   position: relative;
 
+  &__editor-panel {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    overflow: hidden;
+
+    .pf-editor {
+      flex: 1;
+      min-height: 0;
+      contain: strict; // See b/388579546
+    }
+  }
+
+  &__results-panel {
+    height: 100%;
+    overflow: auto;
+  }
+
   &__results {
-    max-height: 500px;
+    height: 100%;
   }
 
   &__results-summary {
+    display: inline-flex;
+    align-items: baseline;
     margin-left: 0.2em;
+    gap: 0.5em;
   }
 
   &__toolbar {
+    flex-shrink: 0;
     border-bottom: 1px solid var(--pf-color-border);
   }
 
-  .pf-editor {
-    contain: strict; // See b/388579546
-    min-height: 3rem;
-  }
-
   &__hotkeys {
     display: inline-flex;
     color: gray;
@@ -50,4 +67,9 @@
     font-weight: 300;
     white-space: pre;
   }
+
+  &__history {
+    height: 100%;
+    border-left: 1px solid var(--pf-color-border);
+  }
 }
diff --git a/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts b/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts
index 89a261e..f95d456 100644
--- a/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts
+++ b/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts
@@ -13,8 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {findRef, toHTMLElement} from '../../base/dom_utils';
-import {assertExists} from '../../base/logging';
 import {Icons} from '../../base/semantic_icons';
 import {QueryResponse} from '../../components/query_table/queries';
 import {DataGrid, renderCell} from '../../components/widgets/datagrid/datagrid';
@@ -32,7 +30,6 @@
 import {Intent} from '../../widgets/common';
 import {Editor} from '../../widgets/editor';
 import {HotkeyGlyphs} from '../../widgets/hotkey_glyphs';
-import {ResizeHandle} from '../../widgets/resize_handle';
 import {Stack, StackAuto} from '../../widgets/stack';
 import {CopyToClipboardButton} from '../../widgets/copy_to_clipboard_button';
 import {Anchor} from '../../widgets/anchor';
@@ -41,6 +38,8 @@
 import {PopupMenu} from '../../widgets/menu';
 import {PopupPosition} from '../../widgets/popup';
 import {AddDebugTrackMenu} from '../../components/tracks/add_debug_track_menu';
+import {SplitPanel} from '../../widgets/split_panel';
+import {Chip} from '../../widgets/chip';
 
 const HIDE_PERFETTO_SQL_AGENT_BANNER_KEY = 'hidePerfettoSqlAgentBanner';
 
@@ -57,13 +56,6 @@
 
 export class QueryPage implements m.ClassComponent<QueryPageAttrs> {
   private dataSource?: DataSource;
-  private editorHeight: number = 0;
-  private editorElement?: HTMLElement;
-
-  oncreate({dom}: m.VnodeDOM<QueryPageAttrs>) {
-    this.editorElement = toHTMLElement(assertExists(findRef(dom, 'editor')));
-    this.editorElement.style.height = '200px';
-  }
 
   onbeforeupdate(
     vnode: m.Vnode<QueryPageAttrs>,
@@ -80,8 +72,8 @@
   }
 
   view({attrs}: m.CVnode<QueryPageAttrs>) {
-    return m(
-      '.pf-query-page',
+    const editorPanel = m(
+      '.pf-query-page__editor-panel',
       m(Box, {className: 'pf-query-page__toolbar'}, [
         m(Stack, {orientation: 'horizontal'}, [
           m(Button, {
@@ -169,33 +161,48 @@
           ),
         ),
       m(Editor, {
-        ref: 'editor',
+        className: 'pf-query-page__editor',
         language: 'perfetto-sql',
         text: attrs.editorText,
         onUpdate: attrs.onEditorContentUpdate,
         onExecute: attrs.onExecute,
       }),
-      m(ResizeHandle, {
-        onResize: (deltaPx: number) => {
-          this.editorHeight += deltaPx;
-          this.editorElement!.style.height = `${this.editorHeight}px`;
-        },
-        onResizeStart: () => {
-          this.editorHeight = this.editorElement!.clientHeight;
-        },
-      }),
+    );
+
+    const resultsPanel = m(
+      '.pf-query-page__results-panel',
       this.dataSource &&
         attrs.queryResult &&
         this.renderQueryResult(attrs.trace, attrs.queryResult, this.dataSource),
-      m(QueryHistoryComponent, {
-        className: 'pf-query-page__history',
-        trace: attrs.trace,
-        runQuery: (query: string) => {
-          attrs.onExecute?.(query);
-        },
-        setQuery: (query: string) => {
-          attrs.onEditorContentUpdate?.(query);
-        },
+    );
+
+    const historyPanel = m(QueryHistoryComponent, {
+      className: 'pf-query-page__history',
+      trace: attrs.trace,
+      runQuery: (query: string) => {
+        attrs.onExecute?.(query);
+      },
+      setQuery: (query: string) => {
+        attrs.onEditorContentUpdate?.(query);
+      },
+    });
+
+    const mainContent = m(SplitPanel, {
+      direction: 'vertical',
+      split: {percent: 40},
+      minSize: 100,
+      firstPanel: editorPanel,
+      secondPanel: resultsPanel,
+    });
+
+    return m(
+      '.pf-query-page',
+      m(SplitPanel, {
+        direction: 'horizontal',
+        split: {fixed: {panel: 'second', size: 300}},
+        minSize: 200,
+        firstPanel: mainContent,
+        secondPanel: historyPanel,
       }),
     );
   }
@@ -266,7 +273,13 @@
             showExportButton: true,
             toolbarItemsLeft: m(
               'span.pf-query-page__results-summary',
-              `Returned ${queryResult.totalRowCount.toLocaleString()} rows in ${queryTimeString}`,
+              m(
+                Chip,
+                {intent: Intent.Success},
+                queryResult.totalRowCount.toLocaleString(),
+                ' rows',
+              ),
+              m('span.pf-muted', queryTimeString),
             ),
             toolbarItemsRight: [
               m(
diff --git a/ui/src/plugins/dev.perfetto.TraceInfoPage/trace_info_page.ts b/ui/src/plugins/dev.perfetto.TraceInfoPage/trace_info_page.ts
index 5289811..e970583 100644
--- a/ui/src/plugins/dev.perfetto.TraceInfoPage/trace_info_page.ts
+++ b/ui/src/plugins/dev.perfetto.TraceInfoPage/trace_info_page.ts
@@ -14,7 +14,7 @@
 
 import m from 'mithril';
 import {Trace} from '../../public/trace';
-import {TabStrip, TabOption} from '../../widgets/tabs';
+import {Tab, Tabs} from '../../widgets/tabs';
 import {EmptyState} from '../../widgets/empty_state';
 import type {TabKey} from './utils';
 import {isValidTabKey} from './utils';
@@ -87,69 +87,17 @@
             'High-level summary of trace health, metrics, and system information',
           ),
         ),
-        m(TabStrip, {
-          tabs: this.getTabs(),
+        m(Tabs, {
+          tabs: this.getTabs(attrs.trace),
           currentTabKey: this.currentTab,
           onTabChange: (key: string) => {
             this.currentTab = isValidTabKey(key) ? key : 'overview';
           },
         }),
-        this.renderCurrentTab(attrs.trace, this.currentTab),
       ),
     );
   }
 
-  private renderCurrentTab(trace: Trace, currentTab: TabKey): m.Children {
-    if (!this.tabData) {
-      return m(EmptyState, {
-        icon: 'hourglass_empty',
-        title: 'Loading trace info...',
-      });
-    }
-    switch (currentTab) {
-      case 'overview':
-        return m(OverviewTab, {
-          trace,
-          data: this.tabData.overview,
-          onTabChange: (key: TabKey) => {
-            this.currentTab = key;
-          },
-        });
-      case 'config':
-        return m(ConfigTab, {
-          data: this.tabData.config,
-        });
-      case 'android':
-        return m(AndroidTab, {
-          data: this.tabData.android,
-        });
-      case 'machines':
-        return m(MachinesTab, {
-          data: this.tabData.machines,
-        });
-      case 'import_errors':
-        return m(ImportErrorsTab, {
-          data: this.tabData.importErrors,
-        });
-      case 'trace_errors':
-        return m(TraceErrorsTab, {
-          data: this.tabData.traceErrors,
-        });
-      case 'data_losses':
-        return m(DataLossesTab, {
-          data: this.tabData.dataLosses,
-        });
-      case 'ui_loading_errors':
-        return m(UiLoadingErrorsTab, {
-          data: this.tabData.uiLoadingErrors,
-        });
-      case 'stats':
-        return m(StatsTab, {
-          data: this.tabData.stats,
-        });
-    }
-  }
-
   private async loadAllData(trace: Trace): Promise<void> {
     const engine = trace.engine;
     this.tabData = {
@@ -166,33 +114,92 @@
     m.redraw();
   }
 
-  private getTabs(): TabOption[] {
-    const tabs: TabOption[] = [{key: 'overview', title: 'Overview'}];
-    if (this.tabData?.config?.configText) {
-      tabs.push({key: 'config', title: 'Trace Config'});
+  private getTabs(trace: Trace): Tab[] {
+    if (!this.tabData) {
+      return [
+        {
+          key: 'overview',
+          title: 'Overview',
+          content: m(EmptyState, {
+            icon: 'hourglass_empty',
+            title: 'Loading trace info...',
+          }),
+        },
+      ];
     }
-    if ((this.tabData?.overview?.importErrors ?? 0) > 0) {
-      tabs.push({key: 'import_errors', title: 'Import Errors'});
+
+    const tabs: Tab[] = [
+      {
+        key: 'overview',
+        title: 'Overview',
+        content: m(OverviewTab, {
+          trace,
+          data: this.tabData.overview,
+          onTabChange: (key: TabKey) => {
+            this.currentTab = key;
+          },
+        }),
+      },
+    ];
+
+    if (this.tabData.config?.configText) {
+      tabs.push({
+        key: 'config',
+        title: 'Trace Config',
+        content: m(ConfigTab, {data: this.tabData.config}),
+      });
     }
-    if ((this.tabData?.traceErrors?.errors?.length ?? 0) > 0) {
-      tabs.push({key: 'trace_errors', title: 'Trace Errors'});
+    if ((this.tabData.overview?.importErrors ?? 0) > 0) {
+      tabs.push({
+        key: 'import_errors',
+        title: 'Import Errors',
+        content: m(ImportErrorsTab, {data: this.tabData.importErrors}),
+      });
     }
-    if ((this.tabData?.overview?.dataLosses ?? 0) > 0) {
-      tabs.push({key: 'data_losses', title: 'Data Losses'});
+    if ((this.tabData.traceErrors?.errors?.length ?? 0) > 0) {
+      tabs.push({
+        key: 'trace_errors',
+        title: 'Trace Errors',
+        content: m(TraceErrorsTab, {data: this.tabData.traceErrors}),
+      });
     }
-    if ((this.tabData?.overview?.uiLoadingErrorCount ?? 0) > 0) {
-      tabs.push({key: 'ui_loading_errors', title: 'UI Loading Errors'});
+    if ((this.tabData.overview?.dataLosses ?? 0) > 0) {
+      tabs.push({
+        key: 'data_losses',
+        title: 'Data Losses',
+        content: m(DataLossesTab, {data: this.tabData.dataLosses}),
+      });
+    }
+    if ((this.tabData.overview?.uiLoadingErrorCount ?? 0) > 0) {
+      tabs.push({
+        key: 'ui_loading_errors',
+        title: 'UI Loading Errors',
+        content: m(UiLoadingErrorsTab, {data: this.tabData.uiLoadingErrors}),
+      });
     }
     const hasAndroid =
-      (this.tabData?.android?.packageList?.length ?? 0) > 0 ||
-      (this.tabData?.android?.gameInterventions?.length ?? 0) > 0;
+      (this.tabData.android?.packageList?.length ?? 0) > 0 ||
+      (this.tabData.android?.gameInterventions?.length ?? 0) > 0;
     if (hasAndroid) {
-      tabs.push({key: 'android', title: 'Android'});
+      tabs.push({
+        key: 'android',
+        title: 'Android',
+        content: m(AndroidTab, {data: this.tabData.android}),
+      });
     }
-    if ((this.tabData?.machines?.machineCount ?? 0) > 1) {
-      tabs.push({key: 'machines', title: 'Machines'});
+    if ((this.tabData.machines?.machineCount ?? 0) > 1) {
+      tabs.push({
+        key: 'machines',
+        title: 'Machines',
+        content: m(MachinesTab, {data: this.tabData.machines}),
+      });
     }
-    tabs.push({key: 'stats', title: 'Info and Stats (advanced)'});
+    tabs.push({
+      key: 'stats',
+      title: 'Info and Stats (advanced)',
+      content: m(StatsTab, {data: this.tabData.stats}),
+    });
+
     return tabs;
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/demos/split_panel_demo.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/demos/split_panel_demo.ts
index 37ed478..690d60f 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/demos/split_panel_demo.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/demos/split_panel_demo.ts
@@ -13,8 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Button} from '../../../widgets/button';
-import {SplitPanel, Tab} from '../../../widgets/split_panel';
+import {SplitPanel} from '../../../widgets/split_panel';
 import {renderWidgetShowcase} from '../widgets_page_utils';
 
 export function renderSplitPanel(): m.Children {
@@ -24,7 +23,7 @@
       m('h1', 'SplitPanel'),
       m(
         'p',
-        'A resizable split panel container for dividing content into adjustable sections with a draggable divider.',
+        'A simple resizable split panel with a draggable handle. Supports both horizontal and vertical layouts, with percentage or fixed-pixel sizing modes.',
       ),
     ),
     renderWidgetShowcase({
@@ -33,38 +32,43 @@
           '',
           {
             style: {
-              height: '400px',
-              width: '400px',
-              border: 'solid 2px gray',
+              height: '300px',
+              width: '500px',
+              border: '1px solid var(--pf-color-border)',
             },
           },
-          m(
-            SplitPanel,
-            {
-              leftHandleContent: [
-                opts.leftContent && m(Button, {icon: 'Menu'}),
-              ],
-              drawerContent: 'Drawer Content',
-              tabs:
-                opts.tabs &&
-                m(
-                  '.pf-split-panel__tabs',
-                  m(
-                    Tab,
-                    {active: true, hasCloseButton: opts.showCloseButtons},
-                    'Foo',
-                  ),
-                  m(Tab, {hasCloseButton: opts.showCloseButtons}, 'Bar'),
-                ),
-            },
-            'Main Content',
-          ),
+          m(SplitPanel(), {
+            direction: opts.vertical ? 'vertical' : 'horizontal',
+            split: opts.fixed
+              ? {fixed: {panel: 'first', size: 150}}
+              : {percent: 50},
+            minSize: 50,
+            firstPanel: m(
+              '',
+              {
+                style: {
+                  padding: '8px',
+                  background: 'var(--pf-color-background)',
+                },
+              },
+              'First Panel',
+            ),
+            secondPanel: m(
+              '',
+              {
+                style: {
+                  padding: '8px',
+                  background: 'var(--pf-color-background-secondary)',
+                },
+              },
+              'Second Panel',
+            ),
+          }),
         );
       },
       initialOpts: {
-        leftContent: true,
-        tabs: true,
-        showCloseButtons: true,
+        vertical: false,
+        fixed: false,
       },
     }),
   ];
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/demos/tabstrip_demo.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/demos/tabs_demo.ts
similarity index 61%
rename from ui/src/plugins/dev.perfetto.WidgetsPage/demos/tabstrip_demo.ts
rename to ui/src/plugins/dev.perfetto.WidgetsPage/demos/tabs_demo.ts
index aae8c77..5a1c7bf 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/demos/tabstrip_demo.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/demos/tabs_demo.ts
@@ -13,24 +13,25 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {TabStrip} from '../../../widgets/tabs';
+import {Tabs} from '../../../widgets/tabs';
 import {renderWidgetShowcase} from '../widgets_page_utils';
 
 let currentTab: string = 'foo';
 
-export function renderTabStrip(): m.Children {
+export function renderTabs(): m.Children {
   return [
     m(
       '.pf-widget-intro',
-      m('h1', 'TabStrip'),
+      m('h1', 'Tabs'),
       m(
         'p',
-        'A horizontal tab navigation component for switching between different views or sections.',
+        'A horizontal tab navigation component for switching between different views or sections. Can optionally contain tab content.',
       ),
     ),
+    m('h2', 'Basic Tabs'),
     renderWidgetShowcase({
       renderWidget: () => {
-        return m(TabStrip, {
+        return m(Tabs, {
           tabs: [
             {key: 'foo', title: 'Foo'},
             {key: 'bar', title: 'Bar'},
@@ -44,5 +45,22 @@
       },
       initialOpts: {},
     }),
+    m('h2', 'Tabs with Content'),
+    renderWidgetShowcase({
+      renderWidget: () => {
+        return m(Tabs, {
+          tabs: [
+            {key: 'foo', title: 'Foo', content: m('p', 'Content for Foo tab')},
+            {key: 'bar', title: 'Bar', content: m('p', 'Content for Bar tab')},
+            {key: 'baz', title: 'Baz', content: m('p', 'Content for Baz tab')},
+          ],
+          currentTabKey: currentTab,
+          onTabChange: (key) => {
+            currentTab = key;
+          },
+        });
+      },
+      initialOpts: {},
+    }),
   ];
 }
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
index 2a30379..f547f92 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
@@ -47,7 +47,7 @@
 import {renderSpinner} from './demos/spinner_demo';
 import {renderSplitPanel} from './demos/split_panel_demo';
 import {renderSwitch} from './demos/switch_demo';
-import {renderTabStrip} from './demos/tabstrip_demo';
+import {renderTabs} from './demos/tabs_demo';
 import {renderTagInput} from './demos/tag_input_demo';
 import {renderTextInput} from './demos/text_input_demo';
 import {renderTextParagraph} from './demos/text_paragraph_demo';
@@ -95,9 +95,9 @@
   {id: 'segmented-buttons', label: 'SegmentedButtons', view: segmentedButtons},
   {id: 'select', label: 'Select', view: renderSelect},
   {id: 'spinner', label: 'Spinner', view: renderSpinner},
-  {id: 'split-panel', label: 'Split Panel', view: renderSplitPanel},
+  {id: 'split-panel', label: 'SplitPanel', view: renderSplitPanel},
   {id: 'switch', label: 'Switch', view: renderSwitch},
-  {id: 'tabstrip', label: 'TabStrip', view: renderTabStrip},
+  {id: 'tabs', label: 'Tabs', view: renderTabs},
   {id: 'taginput', label: 'TagInput', view: renderTagInput},
   {id: 'textinput', label: 'TextInput', view: renderTextInput},
   {id: 'textparagraph', label: 'TextParagraph', view: renderTextParagraph},
diff --git a/ui/src/test/perfetto_ui_test_helper.ts b/ui/src/test/perfetto_ui_test_helper.ts
index a93fe1a..e58e436 100644
--- a/ui/src/test/perfetto_ui_test_helper.ts
+++ b/ui/src/test/perfetto_ui_test_helper.ts
@@ -174,7 +174,9 @@
   }
 
   async switchToTab(text: string | RegExp) {
-    await this.page.locator('.pf-split-panel__tab', {hasText: text}).click();
+    await this.page
+      .locator('.pf-collapsible-split-panel__tab', {hasText: text})
+      .click();
   }
 
   async scheduleFullRedraw(): Promise<void> {
diff --git a/ui/src/widgets/chip.ts b/ui/src/widgets/chip.ts
index ac6d474..5dcd0c5 100644
--- a/ui/src/widgets/chip.ts
+++ b/ui/src/widgets/chip.ts
@@ -20,7 +20,7 @@
 
 export interface ChipAttrs extends HTMLAttrs {
   // Chips require a label.
-  readonly label: m.Children;
+  readonly label?: m.Children;
   // Chips can have an optional icon.
   readonly icon?: string;
   // Use minimal padding, reducing the overall size of the chip by a few px.
@@ -49,7 +49,7 @@
 }
 
 export class Chip implements m.ClassComponent<ChipAttrs> {
-  view({attrs}: m.CVnode<ChipAttrs>) {
+  view({attrs, children}: m.CVnode<ChipAttrs>) {
     const {
       icon,
       compact,
@@ -83,7 +83,7 @@
           icon: icon,
           filled: iconFilled,
         }),
-      m('span.pf-chip__label', label),
+      m('span.pf-chip__label', label, children),
       removable &&
         m(Button, {
           compact: true,
diff --git a/ui/src/widgets/split_panel.ts b/ui/src/widgets/split_panel.ts
index 276dffc..26c867a 100644
--- a/ui/src/widgets/split_panel.ts
+++ b/ui/src/widgets/split_panel.ts
@@ -13,269 +13,174 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {DisposableStack} from '../base/disposable_stack';
-import {toHTMLElement} from '../base/dom_utils';
-import {DragGestureHandler} from '../base/drag_gesture_handler';
-import {assertExists, assertUnreachable} from '../base/logging';
-import {Button, ButtonBar} from './button';
 import {classNames} from '../base/classnames';
-import {HTMLAttrs} from './common';
-import {Icons} from '../base/semantic_icons';
 
-export interface TabAttrs extends HTMLAttrs {
-  // Is this tab currently active?
-  readonly active?: boolean;
-  // Whether to show a close button on the tab.
-  readonly hasCloseButton?: boolean;
-  // What happens when the close button is clicked.
-  readonly onClose?: () => void;
-}
+/** Split configuration - either percentage or fixed pixel mode */
+export type SplitConfig =
+  | {readonly percent: number}
+  | {
+      readonly fixed: {
+        readonly panel: 'first' | 'second';
+        readonly size: number;
+      };
+    };
 
-export class Tab implements m.ClassComponent<TabAttrs> {
-  view({attrs, children}: m.CVnode<TabAttrs>): m.Children {
-    const {active, hasCloseButton, ...rest} = attrs;
-    return m(
-      '.pf-split-panel__tab',
-      {
-        ...rest,
-        className: classNames(active && 'pf-split-panel__tab--active'),
-        onauxclick: () => {
-          attrs.onClose?.();
-        },
-      },
-      m('.pf-split-panel__tab-title', children),
-      hasCloseButton &&
-        m(Button, {
-          compact: true,
-          icon: Icons.Close,
-          onclick: (e) => {
-            e.stopPropagation();
-            attrs.onClose?.();
-          },
-        }),
-    );
-  }
-}
-
-export enum SplitPanelDrawerVisibility {
-  VISIBLE,
-  FULLSCREEN,
-  COLLAPSED,
-}
-
-export interface TabbedSplitPanelAttrs {
-  // Content to put to the left of the tabs on the split handle.
-  readonly leftHandleContent?: m.Children;
-
-  // Tabs to display on the split handle.
-  readonly tabs?: m.Children;
-
-  // Content to display inside the drawer.
-  readonly drawerContent?: m.Children;
-
-  // Whether the drawer is currently visible or not (when in controlled mode).
-  readonly visibility?: SplitPanelDrawerVisibility;
-
-  // Extra classes applied to the root element.
+export interface SplitPanelAttrs {
+  readonly direction?: 'horizontal' | 'vertical';
+  /** Split configuration - defaults to { percent: 50 } */
+  readonly split?: SplitConfig;
+  /** Minimum size in pixels for each panel */
+  readonly minSize?: number;
   readonly className?: string;
-
-  // What height should the drawer be initially?
-  readonly startingHeight?: number;
-
-  // Called when the active tab is changed.
-  onTabChange?(key: string): void;
-
-  // Called when the drawer visibility is changed.
-  onVisibilityChange?(visibility: SplitPanelDrawerVisibility): void;
+  readonly firstPanel: m.Children;
+  readonly secondPanel: m.Children;
+  readonly onResize?: (size: number) => void;
 }
 
-/**
- * A container that fills its parent container, splitting into two adjustable
- * horizontal sections. The upper half is reserved for the main content and any
- * children are placed here, and the lower half should be considered a drawer,
- * the `drawerContent` attribute can be used to define what goes here.
- *
- * The drawer features a handle that can be dragged to adjust the height of the
- * drawer, and also features buttons to maximize and minimise the drawer.
- *
- * Content can also optionally be displayed on the handle itself to the left of
- * the buttons.
- *
- * The layout looks like this:
- *
- * ┌──────────────────────────────────────────────────────────────────┐
- * │pf-split-panel                                                    │
- * │┌────────────────────────────────────────────────────────────────┐|
- * ││pf-split-panel__main                                            ││
- * |└────────────────────────────────────────────────────────────────┘|
- * │┌────────────────────────────────────────────────────────────────┐|
- * ││pf-split-panel__handle                                          ││
- * │|┌─────────────────┐┌─────────────────────┐┌────────────────────┐||
- * |||leftHandleContent||.pf-split-panel__tabs||.pf-button-bar      |||
- * ||└─────────────────┘└─────────────────────┘└────────────────────┘||
- * |└────────────────────────────────────────────────────────────────┘|
- * │┌────────────────────────────────────────────────────────────────┐|
- * ││pf-split-panel__drawer                                          ││
- * |└────────────────────────────────────────────────────────────────┘|
- * └──────────────────────────────────────────────────────────────────┘
- */
-export class SplitPanel implements m.ClassComponent<TabbedSplitPanelAttrs> {
-  private readonly trash = new DisposableStack();
-
-  // The actual height of the vdom node. It matches resizableHeight if VISIBLE,
-  // 0 if COLLAPSED, fullscreenHeight if FULLSCREEN.
-  private height = 0;
-
-  // The height when the panel is 'VISIBLE'.
-  private resizableHeight: number;
-
-  // The height when the panel is 'FULLSCREEN'.
-  private fullscreenHeight = 0;
-
-  // Current visibility state (if not controlled).
-  private visibility = SplitPanelDrawerVisibility.VISIBLE;
-
-  constructor({attrs}: m.CVnode<TabbedSplitPanelAttrs>) {
-    this.resizableHeight = attrs.startingHeight ?? 100;
-  }
-
-  view({attrs, children}: m.CVnode<TabbedSplitPanelAttrs>) {
-    const {
-      leftHandleContent,
-      drawerContent,
-      visibility = this.visibility,
-      className,
-      onVisibilityChange,
-      tabs,
-    } = attrs;
-
-    switch (visibility) {
-      case SplitPanelDrawerVisibility.VISIBLE:
-        this.height = Math.min(
-          Math.max(this.resizableHeight, 0),
-          this.fullscreenHeight,
-        );
-        break;
-      case SplitPanelDrawerVisibility.FULLSCREEN:
-        this.height = this.fullscreenHeight;
-        break;
-      case SplitPanelDrawerVisibility.COLLAPSED:
-        this.height = 0;
-        break;
-    }
-
-    return m(
-      '.pf-split-panel',
-      {
-        className,
-      },
-      m('.pf-split-panel__main', children),
-      m('.pf-split-panel__handle', [
-        leftHandleContent,
-        m('.pf-split-panel__tabs', tabs),
-        this.renderTabResizeButtons(visibility, onVisibilityChange),
-      ]),
-      m(
-        '.pf-split-panel__drawer',
-        {
-          style: {height: `${this.height}px`},
-        },
-        drawerContent,
-      ),
-    );
-  }
-
-  oncreate(vnode: m.VnodeDOM<TabbedSplitPanelAttrs, this>) {
-    let dragStartY = 0;
-    let heightWhenDragStarted = 0;
-
-    const handle = toHTMLElement(
-      assertExists(vnode.dom.querySelector('.pf-split-panel__handle')),
-    );
-
-    this.trash.use(
-      new DragGestureHandler(
-        handle,
-        /* onDrag */ (_x, y) => {
-          const deltaYSinceDragStart = dragStartY - y;
-          this.resizableHeight = heightWhenDragStarted + deltaYSinceDragStart;
-          m.redraw();
-        },
-        /* onDragStarted */ (_x, y) => {
-          this.resizableHeight = this.height;
-          heightWhenDragStarted = this.height;
-          dragStartY = y;
-          this.updatePanelVisibility(
-            SplitPanelDrawerVisibility.VISIBLE,
-            vnode.attrs.onVisibilityChange,
-          );
-        },
-        /* onDragFinished */ () => {},
-      ),
-    );
-
-    const parent = assertExists(vnode.dom.parentElement);
-    this.fullscreenHeight = parent.clientHeight;
-    const resizeObs = new ResizeObserver(() => {
-      this.fullscreenHeight = parent.clientHeight;
-      m.redraw();
-    });
-    resizeObs.observe(parent);
-    this.trash.defer(() => resizeObs.disconnect());
-  }
-
-  onremove() {
-    this.trash.dispose();
-  }
-
-  private renderTabResizeButtons(
-    visibility: SplitPanelDrawerVisibility,
-    setVisibility?: (visibility: SplitPanelDrawerVisibility) => void,
-  ): m.Child {
-    const isClosed = visibility === SplitPanelDrawerVisibility.COLLAPSED;
-    return m(
-      ButtonBar,
-      m(Button, {
-        title: 'Open fullscreen',
-        disabled: visibility === SplitPanelDrawerVisibility.FULLSCREEN,
-        icon: 'vertical_align_top',
-        onclick: () => {
-          this.updatePanelVisibility(
-            SplitPanelDrawerVisibility.FULLSCREEN,
-            setVisibility,
-          );
-        },
-      }),
-      m(Button, {
-        onclick: () => {
-          this.updatePanelVisibility(
-            toggleVisibility(visibility),
-            setVisibility,
-          );
-        },
-        title: isClosed ? 'Show panel' : 'Hide panel',
-        icon: isClosed ? 'keyboard_arrow_up' : 'keyboard_arrow_down',
-      }),
-    );
-  }
-
-  private updatePanelVisibility(
-    visibility: SplitPanelDrawerVisibility,
-    setVisibility?: (visibility: SplitPanelDrawerVisibility) => void,
-  ) {
-    this.visibility = visibility;
-    setVisibility?.(visibility);
-  }
+// Type guard for fixed mode
+function isFixedConfig(
+  split: SplitConfig,
+): split is {fixed: {panel: 'first' | 'second'; size: number}} {
+  return 'fixed' in split;
 }
 
-export function toggleVisibility(visibility: SplitPanelDrawerVisibility) {
-  switch (visibility) {
-    case SplitPanelDrawerVisibility.COLLAPSED:
-    case SplitPanelDrawerVisibility.FULLSCREEN:
-      return SplitPanelDrawerVisibility.VISIBLE;
-    case SplitPanelDrawerVisibility.VISIBLE:
-      return SplitPanelDrawerVisibility.COLLAPSED;
-    default:
-      assertUnreachable(visibility);
-  }
+// Factory function to create SplitPanel instances with their own state
+export function SplitPanel(): m.Component<SplitPanelAttrs> {
+  let splitPercent = 50;
+  let isResizing = false;
+
+  return {
+    oninit(vnode) {
+      const split = vnode.attrs.split ?? {percent: 50};
+      if (!isFixedConfig(split) && 'percent' in split) {
+        splitPercent = split.percent;
+      }
+    },
+
+    view(vnode) {
+      const {
+        direction = 'horizontal',
+        minSize = 50,
+        split = {percent: 50},
+        firstPanel,
+        secondPanel,
+      } = vnode.attrs;
+
+      const fixedPanel = isFixedConfig(split) ? split.fixed.panel : null;
+      const fixedSize = isFixedConfig(split) ? split.fixed.size : 0;
+
+      const containerClasses = classNames(
+        'pf-split-panel',
+        `pf-split-${direction}`,
+        vnode.attrs.className,
+      );
+      const handleSize = 4;
+
+      let firstStyle: Record<string, string>;
+      let secondStyle: Record<string, string>;
+
+      if (fixedPanel === 'first') {
+        firstStyle = {flex: `0 0 ${fixedSize}px`};
+        secondStyle = {flex: '1 1 0'};
+      } else if (fixedPanel === 'second') {
+        firstStyle = {flex: '1 1 0'};
+        secondStyle = {flex: `0 0 ${fixedSize}px`};
+      } else {
+        // Percentage mode
+        firstStyle = {flex: `0 0 calc(${splitPercent}% - ${handleSize / 2}px)`};
+        secondStyle = {
+          flex: `0 0 calc(${100 - splitPercent}% - ${handleSize / 2}px)`,
+        };
+      }
+
+      const onPointerDown = (e: PointerEvent) => {
+        e.preventDefault();
+        const handle = e.currentTarget as HTMLElement;
+        handle.setPointerCapture(e.pointerId);
+        isResizing = true;
+        document.body.style.cursor =
+          direction === 'horizontal' ? 'col-resize' : 'row-resize';
+        document.body.style.userSelect = 'none';
+        handle.classList.add('pf-split-handle--active');
+      };
+
+      const onPointerMove = (e: PointerEvent) => {
+        if (!isResizing) return;
+
+        const handle = e.currentTarget as HTMLElement;
+        const container = handle.parentElement!;
+        const rect = container.getBoundingClientRect();
+        const containerSize =
+          direction === 'horizontal' ? rect.width : rect.height;
+
+        if (fixedPanel) {
+          // Fixed pixel mode
+          let pos: number;
+          if (direction === 'horizontal') {
+            pos = e.clientX - rect.left;
+          } else {
+            pos = e.clientY - rect.top;
+          }
+
+          let newSize: number;
+          if (fixedPanel === 'first') {
+            newSize = pos;
+          } else {
+            newSize = containerSize - pos;
+          }
+
+          // Clamp to min/max
+          newSize = Math.max(
+            minSize,
+            Math.min(containerSize - minSize - handleSize, newSize),
+          );
+
+          if (vnode.attrs.onResize) {
+            vnode.attrs.onResize(newSize);
+          }
+        } else {
+          // Percentage mode
+          let newPercent: number;
+          if (direction === 'horizontal') {
+            const x = e.clientX - rect.left;
+            newPercent = (x / rect.width) * 100;
+          } else {
+            const y = e.clientY - rect.top;
+            newPercent = (y / rect.height) * 100;
+          }
+
+          const minPercent = (minSize / containerSize) * 100;
+          const maxPercent = 100 - minPercent;
+          newPercent = Math.max(minPercent, Math.min(maxPercent, newPercent));
+          splitPercent = newPercent;
+
+          if (vnode.attrs.onResize) {
+            vnode.attrs.onResize(newPercent);
+          }
+        }
+      };
+
+      const onPointerUp = (e: PointerEvent) => {
+        if (isResizing) {
+          const handle = e.currentTarget as HTMLElement;
+          handle.releasePointerCapture(e.pointerId);
+          isResizing = false;
+          document.body.style.cursor = '';
+          document.body.style.userSelect = '';
+          handle.classList.remove('pf-split-handle--active');
+        }
+      };
+
+      return m('div', {class: containerClasses}, [
+        m('.pf-split-panel__first', {style: firstStyle}, firstPanel),
+        m('.pf-split-panel__handle', {
+          onpointerdown: onPointerDown,
+          onpointermove: onPointerMove,
+          onpointerup: onPointerUp,
+          onpointercancel: onPointerUp,
+        }),
+        m('.pf-split-panel__second', {style: secondStyle}, secondPanel),
+      ]);
+    },
+  };
 }
diff --git a/ui/src/widgets/tabs.ts b/ui/src/widgets/tabs.ts
index 5c2f488..f805daa 100644
--- a/ui/src/widgets/tabs.ts
+++ b/ui/src/widgets/tabs.ts
@@ -13,61 +13,101 @@
 // limitations under the License.
 
 import m from 'mithril';
+import {Icons} from '../base/semantic_icons';
+import {Button} from './button';
 import {Icon} from './icon';
+import {isEmptyVnodes} from '../base/mithril_utils';
 
-export interface TabOption {
+export interface Tab {
   readonly key: string;
   readonly title: string;
+  readonly content?: m.Children;
   readonly leftIcon?: string | m.Children;
   readonly rightIcon?: string | m.Children;
+  readonly hasCloseButton?: boolean;
+  readonly onClose?: () => void;
 }
 
-export interface TabStripAttrs {
+export interface TabsAttrs {
   readonly className?: string;
-  readonly tabs: ReadonlyArray<TabOption>;
+  readonly tabs: ReadonlyArray<Tab>;
   readonly currentTabKey: string;
   onTabChange(key: string): void;
+  // Content to render to the left of the tabs
+  readonly leftContent?: m.Children;
+  // Content to render to the right of the tabs
+  readonly rightContent?: m.Children;
+  // If true, the tabs container will fill the available height (100%)
+  readonly fillHeight?: boolean;
 }
 
-export class TabStrip implements m.ClassComponent<TabStripAttrs> {
-  view({attrs}: m.CVnode<TabStripAttrs>) {
-    const {tabs, currentTabKey, onTabChange, className} = attrs;
+export class Tabs implements m.ClassComponent<TabsAttrs> {
+  view({attrs}: m.CVnode<TabsAttrs>) {
+    const {
+      tabs,
+      currentTabKey,
+      onTabChange,
+      className,
+      leftContent,
+      rightContent,
+      fillHeight,
+    } = attrs;
+    const currentTab = tabs.find((t) => t.key === currentTabKey);
+
     return m(
       '.pf-tabs',
-      {className},
+      {
+        className,
+        style: fillHeight ? {height: '100%'} : undefined,
+      },
       m(
-        '.pf-tabs__tabs',
-        tabs.map((tab) => {
-          const {key, title, leftIcon, rightIcon} = tab;
-          const renderIcon = (
-            icon: string | m.Children | undefined,
-            className: string,
-          ) => {
-            if (icon === undefined) {
-              return undefined;
-            }
-            if (typeof icon === 'string') {
-              return m(Icon, {icon, className});
-            }
-            return m('.pf-tabs__tab-icon', {className}, icon);
-          };
-          return m(
-            '.pf-tabs__tab',
-            {
-              active: currentTabKey === key,
-              key,
-              onclick: () => {
-                onTabChange(key);
+        '.pf-tabs__header',
+        !isEmptyVnodes(leftContent) && m('.pf-tabs__left-content', leftContent),
+        m(
+          '.pf-tabs__tabs',
+          tabs.map((tab) => {
+            const {key, title, leftIcon, rightIcon, hasCloseButton, onClose} =
+              tab;
+            const isActive = currentTabKey === key;
+            const renderIcon = (
+              icon: string | m.Children | undefined,
+              cls: string,
+            ) => {
+              if (icon === undefined) return undefined;
+              if (typeof icon === 'string') {
+                return m(Icon, {icon, className: cls});
+              }
+              return m('.pf-tabs__tab-icon', {className: cls}, icon);
+            };
+            return m(
+              '.pf-tabs__tab',
+              {
+                active: isActive,
+                key,
+                onclick: () => onTabChange(key),
+                onauxclick: () => onClose?.(),
               },
-            },
-            [
-              renderIcon(leftIcon, 'pf-tabs__tab-icon--left'),
-              m('span.pf-tabs__tab-title', title),
-              renderIcon(rightIcon, 'pf-tabs__tab-icon--right'),
-            ],
-          );
-        }),
+              [
+                renderIcon(leftIcon, 'pf-tabs__tab-icon--left'),
+                m('span.pf-tabs__tab-title', title),
+                renderIcon(rightIcon, 'pf-tabs__tab-icon--right'),
+                hasCloseButton &&
+                  m(Button, {
+                    compact: true,
+                    icon: Icons.Close,
+                    onclick: (e: Event) => {
+                      e.stopPropagation();
+                      onClose?.();
+                    },
+                  }),
+              ],
+            );
+          }),
+        ),
+        !isEmptyVnodes(rightContent) &&
+          m('.pf-tabs__right-content', rightContent),
       ),
+      m('.pf-tabs__content', currentTab?.content),
     );
   }
 }