Commit 4fe493f8bcc for woocommerce

commit 4fe493f8bcc2821cc729111020706395faca9c41
Author: Jill Q. <jill.quek@automattic.com>
Date:   Thu May 14 12:13:16 2026 +0800

    Floating header: suppress duplicate H1, unify chrome to icon tabs (#64643)

    * Floating header: prototype consolidated chrome bar

    Suppress the floating-header <h1> when wp-admin already renders one
    (detected via .wrap > h1.wp-heading-inline at mount). Pages that
    register a WooHeaderPageTitle fill or have no wp-admin h1 (Settings,
    Home, Analytics) keep their floating title.

    Add a chrome-only narrow treatment (~32px) for embedded post-type
    screens so the bar reads as infrastructure rather than content. Pages
    with a title stay at the original 60px.

    Move Screen Options and Help into the floating header as icon buttons
    (cog, help). Hide wp-admin's original strip via the visually-hidden
    pattern (not display:none, so wp-admin's jQuery click handlers stay
    alive). The new icons proxy clicks to #show-settings-link and
    #contextual-help-link.

    Wire all four right-side icons (Activity, Finish setup, Screen Options,
    Help) into a single tab group with mutually-exclusive open state and
    the standard blue-underline active treatment. Activity-panel-tab and
    header-meta-icon styles are unified.

    Replace activity-panel icons with @wordpress/icons equivalents:
    IconFlag → bell, FeedbackIcon → megaphone, SetupProgress → listView.
    Hide tab labels (still in DOM for screen readers) so the bar is
    icon-only across all heights. Match wp-admin omnibar visual weight
    with 18px icons, page-grey background, and a 1px bottom border.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: unify icon sizing, page-grey bg, tab-group polish

    Bring all five right-side icons (bell / list / megaphone / gear / ?) to
    a uniform 18px size — the activity-panel had a stray svg{width:24px}
    rule that was beating the size prop on the @wordpress/icons SVGs.

    Visual treatment: bar background to wp-admin page grey (#f0f0f1) with
    a 1px #dcdcde bottom border so it reads as infrastructure rather than
    content. Buttons are transparent (inherit bar bg). Unread-dot ring
    recoloured from white to the new bar grey so it blends instead of
    showing as a white pill on grey.

    Tab-group behaviour:
    - Replace showTooltip Button prop with native HTML title attribute on
      both header-meta-icon and activity-panel-tab Buttons. Avoids the
      Tooltip wrapper component subtly interfering with the cross-system
      click-sync.
    - Replace mid-click setActiveMetaIcon side-effect with a passive
      MutationObserver on aria-expanded of #show-settings-link and
      #contextual-help-link. State now updates after the click has fully
      settled instead of mid-flight.
    - Defer the wp-admin dropdown close from the activity-panel-tab click
      listener to a setTimeout(0) macrotask.
    - triggerMetaIcon also closes the OTHER wp-admin dropdown for proper
      gear ↔ help mutual exclusion.

    panel.js: exempt clicks on a sibling activity-panel-tab from the
    useFocusOutside close handler. Without this exemption, clicking a
    sibling tab while the panel was open raced closePanel against the new
    tab's togglePanel — close won, breaking bell ↔ finish-setup switching.

    Activity-panel hover: explicit color: $gray-900 to override Button's
    default theme-color hover, so all five icons hover to the same darker
    grey instead of bell+list turning blue.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: Settings page polish + gear↔help switch fix

    Settings page renders Woo's tab nav directly below the floating header
    with no other chrome between them — treat the two strips as one
    continuous title region. Drop the floating header's bottom border and
    match the tab strip's white background to the page-grey we use
    elsewhere. Keep the tab strip's bottom border (between tabs and
    content) but recolour from legacy #EBEBEB to #dcdcde so it matches
    all the other borders in the chrome region.

    Defer the second click in triggerMetaIcon by 250ms when switching
    between gear and help. wp-admin's screen-meta.js uses jQuery
    slideUp/slideDown for the dropdown panels (~150ms each). Without the
    defer, the slideDown on the new panel races the slideUp on the
    closing one and the new panel never opens.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: visual polish round 3

    Tab indicator: 3px → 2px to match Gutenberg's TabPanel and Woo's own
    nav-tab-active indicator on Settings.

    Unread dot positioning: anchor to icon centre via calc(50% - 9px) /
    calc(50% + 2px) so the dot sits at the top-right of the 18px icon
    regardless of bar height (60px with title vs 32px chrome-only). Drops
    the responsive breakpoint overrides that assumed wider buttons.

    Icon spacing: tighten to wp-admin admin-bar style. Activity-panel-tab
    loses width:100% in favour of width:auto + padding:0 8px;
    header-meta-icon drops min-width:50px in favour of min-width:0 +
    padding:0 8px. Both subsystems now occupy ~34px (8 + 18 + 8) per icon.

    Focus state: split :focus into separate :focus / :focus-visible rules
    on activity-panel-tab and header-meta-icon. Mouse-click focus shows
    nothing extra (just the active blue underline); keyboard focus still
    shows the inset shadow ring for a11y.

    Slide-over panel: clear the floating header's 1px bottom border by
    nudging .woocommerce-layout__activity-panel-wrapper down with
    top: calc(100% + 1px). Suppress the browser's default focus outline
    on the wrapper (showed as a blue ring with rounded corners on click).

    Settings page: tab nav border-bottom recoloured from legacy #EBEBEB
    to #dcdcde so it matches the rest of the chrome-region borders.

    Activity-panel hover: explicit color: $gray-900 on activity-panel-tab
    (was inheriting Button's default theme-color blue, mismatching the
    header-meta-icon hover treatment).

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: unify Home page tabs + Gutenberg-style hover

    Remove the homescreen-specific tab styling override that gave Home
    page tabs (View store, display options, account avatar, help) blue
    text and bigger padding. They now inherit the same icon-only,
    $gray-900, 18px, 0 8px treatment as every other floating-header tab,
    so the Home header reads as visually identical to Edit Order, Settings,
    etc. — even though the underlying tab componentry differs.

    Single colour across all states. Default / hover / active all sit at
    $gray-900 — the blue ::before underline alone signals active, mirroring
    Gutenberg's icon tab group pattern. Explicit override on hover is
    needed to suppress Button's default theme-blue hover.

    Bring back showTooltip on Buttons (header-meta-icon and
    activity-panel-tab) for the Gutenberg-style instant-fade tooltip
    treatment instead of the slow native HTML title attribute. The earlier
    mutual-exclusion bugs blamed on the Tooltip wrapper turned out to be
    the panel.js useFocusOutside race condition (now fixed) plus
    mid-click setActiveMetaIcon (now passive via MutationObserver), so
    Tooltip can come back without breaking click sync.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: hide only standard SO + Help, leave the strip extensible

    First cut hid the entire #screen-meta-links strip via height: 0;
    overflow: hidden. Popular-plugin sample (PayPal, Stripe, Mollie,
    AutomateWoo, Subscriptions, Bookings) showed zero plugins injecting
    sibling buttons into the strip — but hosts and power-user installs are
    exactly where weird plugin combinations show up, so trade a small
    diff for defensive headroom.

    Now hide only the two specific standard wraps we replace
    (#screen-options-link-wrap, #contextual-help-link-wrap) via the
    visually-hidden pattern, leaving the parent #screen-meta-links and
    any third-party sibling buttons (rare .screen-meta-toggle injections)
    visible. wp-admin's jQuery delegated click handlers still fire on the
    hidden wraps, so our gear / ? icons keep working.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: sentence-case "Screen options" tooltip

    Match the sentence-case convention used on the other tooltips
    (Activity, Feedback, Finish setup, Help) for visual consistency.
    wp-admin core uses Title Case for the dropdown panel itself; the
    floating-header tooltip is a Woo surface where sentence case applies.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Add changefile(s) from automation for the following project(s): woocommerce, woocommerce/client/admin

    * Floating header: address review feedback

    Replace the magic-number setTimeout(250) in triggerMetaIcon with a
    one-shot MutationObserver that chains the new dropdown's open off the
    closing trigger's aria-expanded flip. wp-admin's screen-meta.js sets
    aria-expanded synchronously when its handler fires, so the observer
    fires the moment the close has registered — regardless of how long
    jQuery's slideUp animation actually takes. Self-disconnects on first
    flip so back-to-back gear ↔ help clicks don't accumulate observers.

    Drop the setTimeout(0) deferral inside the document-level click
    listener too — that listener doesn't update React state any more
    (state syncs reactively via the MutationObserver above), so there's
    no longer any mid-click setState to defer past.

    Add `query` to the detection useEffect's dep array so the wp-admin
    H1 / Screen Options / Help wrap detection re-runs on client-side
    route transitions. Defensive against BaseHeader staying mounted
    across Home → Settings → Customers if WC Admin's layout reuses it.

    Wrap the #screen-options-link-wrap / #contextual-help-link-wrap hide
    in a `:has(.woocommerce-layout__header-meta-icon)` guard so the hide
    only fires when the floating-header replacement icon actually
    rendered. If detection misses for any reason — or on browsers
    without :has() support (Firefox <121) — the user keeps access to the
    original wp-admin strip rather than losing it entirely.

    Extract repeated wp-admin Core greys (#f0f0f1 page background,
    #dcdcde border) to local SCSS variables in header/style.scss and
    activity-panel/style.scss. Not exposed as CSS custom properties on
    :root (verified via wp-token-check), so hardcoding is the right call,
    but a single source per file beats grepping for the next person.

    Refresh stale comments in shared.tsx (icons no longer "non-functional
    placeholders") and activity-panel/style.scss ($gray-700 → $gray-900
    in the homescreen-override-removed comment).

    Append trailing newline to the auto-generated changelog file
    (automation omits it).

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Add changefile(s) from automation for the following project(s): woocommerce, woocommerce/client/admin

    * Floating header: drop empty SCSS comment line

    Stylelint's scss/comment-no-empty rule disallows bare `//` lines.
    The empty comment was being used as a paragraph separator between
    two adjacent comment blocks; collapse them instead.

    * Floating header: address CodeRabbit review feedback

    Add aria-expanded={ activeMetaIcon === 'screen-options' } /
    aria-expanded={ activeMetaIcon === 'help' } to the gear / ? Buttons.
    Assistive tech now sees the toggled state programmatically, matching
    the wp-admin pattern users already encounter on the original
    #show-settings-link / #contextual-help-link triggers we proxy.

    Extract the duplicated visually-hidden CSS pattern to a single SCSS
    mixin (`@mixin visually-hidden`) at the top of header/style.scss.
    Both call sites — `.woocommerce-layout__activity-panel-tab-title`
    and the `#screen-options-link-wrap` / `#contextual-help-link-wrap`
    hide — replaced with `@include visually-hidden`. Single source of
    truth instead of an 11-property block duplicated twice.

    * Floating header: feedback icon megaphone → comment

    Megaphone read as outbound announcement. Speech-bubble (comment)
    matches the "give feedback" intent better — feedback is a
    conversation, not a broadcast. Both from @wordpress/icons; trivial
    swap.

    * Floating header: address CodeRabbit round 2 feedback

    Lazy-initialise the wp-admin DOM detection state (hasWpAdminH1,
    hasScreenOptions, hasContextualHelp) by reading the DOM synchronously
    in the useState initialiser instead of starting at `false` and updating
    in the useEffect commit. Without this, the first render of an embedded
    post-type screen (Edit Order, Edit Product, Add Product) had
    hasWpAdminH1 = false → shouldRenderTitle = true → floating <h1>
    rendered for one frame before the effect committed and re-rendered
    without it. That's exactly the duplicate-title flash this PR is meant
    to remove. wp-admin's <h1> and meta-link wraps are server-rendered
    before React hydrates, so the synchronous read is safe; the useEffect
    still runs on `query` changes for client-side route transitions.

    Scope decodeEntities to the string branch only. Previously the call
    wrapped the entire ternary, including the WooHeaderPageTitle.Slot JSX
    element when a fill was registered. decodeEntities is a string
    transform — passing JSX is a no-op at runtime but semantically wrong
    and confusing on review. Apply it only to the getPageTitle( sections )
    string branch where it's actually needed.

    (Skipped the regex → endsWith nitpick on activity-panel.js:223 — that
    line is pre-existing code outside the diff for this PR.)

    * Floating header: initialize meta-icon state on effect mount

    Add setActiveMetaIcon(null) to the early return and call sync()
    immediately after binding the MutationObserver so activeMetaIcon
    reflects current aria-expanded values on mount and after route
    transitions, not just on future mutations. Addresses CodeRabbit
    round 3 feedback.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: fix lint errors + brittle Orders e2e locator

    - activity-panel.js: drop unused setupTasksCount / setupTasksCompleteCount
      destructured fields (leftover from icon refactor)
    - shared.tsx: replace nested ternary in MutationObserver sync with if/else;
      prettier formatting from eslint --fix
    - order-edit.spec.ts: locator was h1.components-text only, asserting on
      the floating Woo h1 that this PR now suppresses on the Orders page in
      favor of wp-admin's own h1.wp-heading-inline. Match either source.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: fix panel switching by removing isPanelClosing race-loss

    The previous attempt to fix bell↔finish-setup panel switching exempted all
    .woocommerce-layout__activity-panel-tab clicks from the focus-outside close.
    That over-exempted: tabs like "Preview store" / "Preview site" (custom
    onClick that opens a new window, never calls togglePanel) kept the panel
    visibly open instead of closing.

    Root cause was the isPanelClosing guard in togglePanel(), which bailed when
    useFocusOutside fired closePanel() mid-click. Removing the guard (and
    clearing isPanelClosing in togglePanel so the flag can't stay stuck-true)
    fixes panel switching for every kind of tab without an over-broad exemption.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: address CodeRabbit + chihsuan review feedback

    - shared.tsx: unify the three wp-admin chrome detection states into a
      single detectWpAdminChrome() reading them together (they always change
      together), and drop the typeof document !== 'undefined' SSR guards —
      WC Admin is a webpack bundle loaded into wp-admin browser pages, there
      is no SSR boundary.
    - tab/index.js + header/style.scss: drop the visually-hidden
      .woocommerce-layout__activity-panel-tab-title span. The Button already
      has aria-label, which screen readers announce in preference to any
      descendant text, so the span was redundant.
    - activity-panel/style.scss: drop the inline-comment paragraph about
      homescreen styling removal (the rationale belongs in PR review, not
      the file), and revert the .activity-panel-toggle-bubble border colour
      — it styles dead-code ActivityPanelToggleBubble that is never
      rendered, so the change was inert.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: use @wordpress/icons bellUnread for unread state

    The previous approach overlaid a CSS ::after red dot on top of the plain
    bell icon, with hand-tuned positioning (`top: calc(50% - 9px); left: calc(50% + 2px)`)
    and a 2px ring matching the bar background.

    @wordpress/icons already ships `bellUnread` — the same bell with the dot
    baked into the SVG at the design-team-blessed coordinates. Swap to it
    when there is unread activity, drop the ::after rule and its hover override
    entirely. Keep one small `circle { fill: $alert-red }` so the dot reads
    as the urgency signal it always was; without that override the dot would
    inherit currentColor (gray-900) like the rest of the icon.

    Net: less custom CSS, no box-model math, and one source of truth for
    unread-state styling lives inside the design system instead of bolted
    on the side.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: move embed-only logic from shared.tsx into embed.tsx

    Address chihsuan's review feedback that wp-admin chrome handling (h1
    suppression, Screen Options / Help proxy icons, chrome-only narrow bar)
    only ever applies on classic admin pages, so the orchestration belongs in
    EmbedHeader rather than the BaseHeader shared between embed and React
    routes.

    - New `use-wp-admin-chrome.ts` custom hook owns the DOM detection
      (`hasH1`, `hasScreenOptions`, `hasContextualHelp`), the
      `activeMetaIcon` state synced via MutationObserver, the
      cross-system click handler that closes wp-admin dropdowns when an
      activity-panel tab is clicked, and the `triggerMetaIcon` function
      with its one-shot observer chain for gear ↔ help mutual exclusion.
    - `BaseHeader` shrinks to a dumb layout component. New props
      `suppressTitle`, `compact`, and `trailingItems` let the caller drive
      embed-specific behavior; non-embed `Header` just doesn't pass them and
      gets the default (always render title, full height, no trailing items).
    - `EmbedHeader` calls the hook, composes the gear / ? icons into
      `trailingItems`, and passes `suppressTitle` / `compact` based on
      whether wp-admin rendered its own h1.

    Behavior is unchanged across Home, Settings, Edit Order, Edit Product,
    Add Product, Products list. The seam between embed and non-embed is now
    explicit in the prop surface rather than a runtime `isEmbedded` branch
    inside the shared component.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: fix CI lint + unit tests after refactor

    - activity-panel.js: remove the now-dead isPanelClosing state. The
      togglePanel() guard that read its value was removed in the previous
      bug fix, so the state was assigned but never read — eslint flagged it
      as unused. Drop both the state and the three setIsPanelClosing()
      calls (the close/clear lifecycle works fine without the flag).
    - header/shared.tsx, header/embed.tsx: apply prettier formatting that
      eslint --fix wanted (single-line clsx() arguments and imports).
    - activity-panel/test/index.js: swap getByText/queryByText for
      getByRole('tab', { name: ... }) on the tab assertions, since we
      removed the visually-hidden title <span> in the previous commit.
      Tab buttons still expose the same accessible name via aria-label,
      so the role+name query lands on the same elements that the text
      query used to find.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: fix the second jest test that referenced the title span

    The previous test fix only covered client/activity-panel/test/index.js;
    client/activity-panel/tab/test/index.js had a separate `getByText('Hello
    World')` assertion against the title text that the visually-hidden span
    used to render. Tab buttons still expose the same title as their
    accessible name via aria-label, so swap the text query for
    `getByRole('tab', { name: 'Hello World' })`.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: address chihsuan's SCSS dedup feedback

    Apply most of round 2's polish notes:

    - internal-style-build/abstracts/_variables.scss: update outdated
      $wp-admin-background (#f1f1f1 → #f0f0f1, matching modern wp-admin),
      add $wp-admin-border (#dcdcde) and $header-border-width (1px) so
      multiple files can share these without duplicating literals.
    - header/style.scss: drop the local $wp-admin-page-bg / $wp-admin-border
      declarations (now in _variables), use $header-border-width on the
      bottom border, and move Settings-page-specific overrides out to the
      legacy admin.scss where the rest of that page's styling lives.
    - activity-panel/style.scss: drop the duplicated local $wp-admin-page-bg
      (unused after the bellUnread swap), use the shared $header-border-width
      in the slide-over panel's top offset so it stays aligned with the
      header's bottom border if that ever changes.
    - legacy admin.scss (body.woocommerce_page_wc-settings): add the floating
      header's "no bottom border" tweak here so it sits alongside the rest
      of the Settings overrides; drop the .nav-tab-wrapper white background
      so the strip reads continuously with the floating header bg above it;
      recolour the strip's bottom border to #dcdcde to match the rest of
      the chrome borders.

    Floating header background stays explicit — it's `position: fixed` so
    page content scrolls underneath, and a transparent bar would show that
    content through.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Floating header: fix same-tab close pop-back by lifting click intent

    @chihsuan caught a regression from the earlier isPanelClosing removal:
    clicking the active tab to close the panel closes for a frame and pops
    back open. Trace:

    1. Click → Panel blurs → useFocusOutside calls closePanel() → isPanelOpen=false
    2. Tabs mirrors that prop into local tabIsOpenState=false
    3. Tabs click handler computes intent from its mirror: same-tab → !false = true
    4. togglePanel(tab, true) → setIsPanelOpen(true) → panel pops back open

    The old isPanelClosing guard happened to protect against this; removing
    it for tab-switching exposed the same-tab close case.

    Apply chihsuan's suggested structural fix instead of a narrow guard:
    move click-intent decision (open / close / switch) from Tabs into
    togglePanel, so it reads from the parent's own state instead of Tabs'
    mirrored copy of it.

    - Tabs: drop the local useState/useEffect mirror. Becomes a controlled
      component — selection state is driven entirely by parent props. Click
      forwards the clicked tab via `onTabClick( tab )` (no second arg).
      Analytics event moves out (intent isn't known here anymore).
    - activity-panel.js togglePanel: handles side-effect tabs (Preview store,
      Feedback CES) up front, then derives isSameTab / isClosing / isSwitching
      from currentTab + isPanelOpen, records the analytics event for opens
      and switches, and sets all four pieces of state at once. The
      `isPanelClosing && tabName === currentTab` guard is the narrow piece
      that blocks the same-tab re-click during a pending close.
    - isPanelClosing is restored (state + closePanel setter + togglePanel
      setter) — it's a meaningful flag again, not dead.
    - Tests: rewrite tabs/test/index.js to use a small ControlledTabs wrapper
      that mirrors the parent's open/close decision, since selection class
      state lives in the parent now. Drop the "records an event" test —
      Tabs no longer records.

    Verified: same-tab close consistently closes (including rapid
    double-clicks); switching between Activity / Finish setup still works;
    side-effect tabs (Preview store, Preview site) still close the panel.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/packages/js/internal-style-build/abstracts/_variables.scss b/packages/js/internal-style-build/abstracts/_variables.scss
index da75f25635e..6eb3c5ff840 100644
--- a/packages/js/internal-style-build/abstracts/_variables.scss
+++ b/packages/js/internal-style-build/abstracts/_variables.scss
@@ -24,6 +24,7 @@ $gap-smallest: 4px;

 // Header
 $header-height: 60px;
+$header-border-width: 1px;
 $header-scroll-shadow: 0 8px 8px 0 rgba(85, 93, 102, 0.3);

 // Sidebar
@@ -45,8 +46,12 @@ $adminbar-height-mobile: 46px;
 $admin-menu-width: 160px;
 $admin-menu-width-collapsed: 36px;

-// wp-admin colors
-$wp-admin-background: #f1f1f1;
+// wp-admin colors. Hardcoded because WordPress doesn't expose them as CSS
+// custom properties on `:root` (verified via the `wp-token-check` skill).
+// Centralised here so the next person doesn't have to grep when WP shifts
+// the bar / border palette.
+$wp-admin-background: #f0f0f1; // wp-admin body background grey
+$wp-admin-border: #dcdcde; // wp-admin chrome border colour
 $wp-admin-sidebar: #24292d;

 // Muriel
diff --git a/plugins/woocommerce/changelog/64643-sprinkle-embedded-header-no-duplicate-title b/plugins/woocommerce/changelog/64643-sprinkle-embedded-header-no-duplicate-title
new file mode 100644
index 00000000000..898a971a96b
--- /dev/null
+++ b/plugins/woocommerce/changelog/64643-sprinkle-embedded-header-no-duplicate-title
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Refresh the floating WC Admin header to consolidate Screen options, Help, Activity, and Finish setup into a single icon-only tab group, removing the duplicate page title that previously appeared on top of wp-admin's own H1 on classic admin screens.
\ No newline at end of file
diff --git a/plugins/woocommerce/client/admin/client/activity-panel/activity-panel.js b/plugins/woocommerce/client/admin/client/activity-panel/activity-panel.js
index d5c602a323f..4996024ccda 100644
--- a/plugins/woocommerce/client/admin/client/activity-panel/activity-panel.js
+++ b/plugins/woocommerce/client/admin/client/activity-panel/activity-panel.js
@@ -5,7 +5,15 @@ import { __ } from '@wordpress/i18n';
 import { lazy, useState, useEffect, useCallback } from '@wordpress/element';
 import { useSelect, useDispatch } from '@wordpress/data';
 import { uniqueId, find } from 'lodash';
-import { Icon, help as helpIcon, external } from '@wordpress/icons';
+import {
+	Icon,
+	help as helpIcon,
+	external,
+	bell,
+	bellUnread,
+	listView,
+	comment,
+} from '@wordpress/icons';
 import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score';
 import { H, Section } from '@woocommerce/components';
 import { onboardingStore, optionsStore, useUser } from '@woocommerce/data';
@@ -21,10 +29,8 @@ import {
  * Internal dependencies
  */
 import './style.scss';
-import { IconFlag } from './icon-flag';
 import { hasUnreadNotes as checkIfHasUnreadNotes } from './unread-indicators';
 import { Tabs } from './tabs';
-import { SetupProgress } from './setup-progress';
 import { DisplayOptions } from './display-options';
 import { Panel } from './panel';
 import {
@@ -37,7 +43,6 @@ import { ABBREVIATED_NOTIFICATION_SLOT_NAME } from './panels/inbox/abbreviated-n
 import { getAdminSetting } from '~/utils/admin-settings';
 import { getUrlParams } from '~/utils';
 import { getSegmentsFromPath } from '~/utils/url-helpers';
-import { FeedbackIcon } from '~/products/images/feedback-icon';
 import { useLaunchYourStore } from '~/launch-your-store';
 import { useTaskListsState } from '~/hooks/use-tasklists-state';
 import HeaderAccount from '../marketplace/components/header-account/header-account';
@@ -150,8 +155,6 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {
 		requestingTaskListOptions,
 		setupTaskListComplete,
 		setupTaskListHidden,
-		setupTasksCount,
-		setupTasksCompleteCount,
 		thingsToDoNextCount,
 	} = useTaskListsState();

@@ -188,20 +191,40 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {

 	const { currentUserCan } = useUser();

-	const togglePanel = ( { name: tabName }, isTabOpen ) => {
-		const panelSwitching =
-			tabName !== currentTab &&
-			currentTab !== '' &&
-			isTabOpen &&
-			isPanelOpen;
+	// Single decision point for a tab click. Side-effect tabs (Preview store,
+	// Feedback CES modal) bail out before any panel state is touched. The
+	// rest of the logic decides open / close / switch from the parent's own
+	// state (currentTab, isPanelOpen) rather than the click target's intent
+	// — that way a focus-outside close racing with a same-tab click can't
+	// flip the panel back open after blur fires closePanel().
+	const togglePanel = ( tab ) => {
+		if ( tab.onClick ) {
+			tab.onClick();
+			return;
+		}

-		if ( isPanelClosing ) {
+		const tabName = tab.name;
+		// Same-tab re-click during a pending close: do nothing. The close
+		// from useFocusOutside is already in flight; let it finish.
+		if ( isPanelClosing && tabName === currentTab ) {
 			return;
 		}

+		const isSameTab = tabName === currentTab;
+		const isClosing = isSameTab && isPanelOpen;
+		const isSwitching = ! isSameTab && currentTab !== '' && isPanelOpen;
+
+		// Record a Tracks event when a panel is being opened or switched in
+		// (not when closing). Previously the Tabs child fired this — moved
+		// here so it stays consistent with the rest of the intent logic.
+		if ( ! isClosing ) {
+			recordEvent( 'activity_panel_open', { tab: tabName } );
+		}
+
 		setCurrentTab( tabName );
-		setIsPanelOpen( isTabOpen );
-		setIsPanelSwitching( panelSwitching );
+		setIsPanelOpen( ! isClosing );
+		setIsPanelSwitching( isSwitching );
+		setIsPanelClosing( isClosing );
 	};

 	const isProductScreen = () => {
@@ -236,7 +259,20 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {
 		const activity = {
 			name: 'activity',
 			title: __( 'Activity', 'woocommerce' ),
-			icon: <IconFlag />,
+			// Use bellUnread (bell + dot baked into the SVG) when there is
+			// unread activity so the unread state lives in one source of truth
+			// inside @wordpress/icons rather than a separately-positioned CSS
+			// pseudo-element on top of the plain bell.
+			icon: (
+				<Icon
+					icon={
+						hasUnreadNotes || hasAbbreviatedNotifications
+							? bellUnread
+							: bell
+					}
+					size={ 18 }
+				/>
+			),
 			unread: hasUnreadNotes || hasAbbreviatedNotifications,
 			visible:
 				( isEmbedded || ! isHomescreen ) &&
@@ -248,7 +284,7 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {
 		const feedback = {
 			name: 'feedback',
 			title: __( 'Feedback', 'woocommerce' ),
-			icon: <FeedbackIcon />,
+			icon: <Icon icon={ comment } size={ 18 } />,
 			onClick: () => {
 				setCurrentTab( 'feedback' );
 				setIsPanelOpen( true );
@@ -290,14 +326,7 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {
 		const setup = {
 			name: 'setup',
 			title: __( 'Finish setup', 'woocommerce' ),
-			icon: (
-				<SetupProgress
-					setupTasksComplete={ setupTasksCompleteCount }
-					setupCompletePercent={ Math.ceil(
-						( setupTasksCompleteCount / setupTasksCount ) * 100
-					) }
-				/>
-			),
+			icon: <Icon icon={ listView } size={ 18 } />,
 			visible:
 				currentUserCan( 'manage_woocommerce' ) &&
 				! requestingTaskListOptions &&
@@ -413,14 +442,7 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {
 						tabs={ tabs }
 						tabOpen={ isPanelOpen }
 						selectedTab={ currentTab }
-						onTabClick={ ( tab, tabOpen ) => {
-							if ( tab.onClick ) {
-								tab.onClick();
-								return;
-							}
-
-							togglePanel( tab, tabOpen );
-						} }
+						onTabClick={ togglePanel }
 					/>
 					<Panel
 						currentTab
diff --git a/plugins/woocommerce/client/admin/client/activity-panel/style.scss b/plugins/woocommerce/client/admin/client/activity-panel/style.scss
index 781b42edc4f..f99e629b267 100644
--- a/plugins/woocommerce/client/admin/client/activity-panel/style.scss
+++ b/plugins/woocommerce/client/admin/client/activity-panel/style.scss
@@ -17,8 +17,8 @@
 	}

 	svg {
-		width: 24px;
-		height: 24px;
+		width: 18px;
+		height: 18px;

 		&.woocommerce-layout__activity-panel-tab-icon {
 			fill: none;
@@ -69,10 +69,16 @@
 		border: none;
 		outline: none;
 		cursor: pointer;
-		background-color: $studio-white;
-		width: 100%;
+		background-color: transparent;
+		// Match wp-admin admin-bar item padding so all five icons in the bar
+		// (bell / list / megaphone / gear / ?) sit at the same tight spacing.
+		width: auto;
+		min-width: 0;
+		padding: 0 8px;
 		height: $header-height;
-		color: $gray-700;
+		// $gray-900 across all states (default, hover, active) — the blue
+		// underline alone signals active, matching Gutenberg's icon tab group.
+		color: $gray-900;
 		white-space: nowrap;

 		&::before {
@@ -91,54 +97,41 @@

 		&.is-active,
 		&.is-opened {
-			color: $gray-900;
 			box-shadow: none;

 			&::before {
-				height: 3px;
+				// 2px to match Gutenberg's TabPanel + Woo's own
+				// nav-tab-active indicator on Settings.
+				height: 2px;
 				opacity: 1;
 			}
 		}

-		&.has-unread::after,
-		&.woocommerce-layout__activity-panel-tab-wordpress-notices::after {
-			content: " ";
-			position: absolute;
-			padding: 1px;
-			background: $alert-red;
-			border: 2px solid $studio-white;
-			width: 4px;
-			height: 4px;
-			display: inline-block;
-			border-radius: 50%;
-			top: 8px;
-			left: 50%;
-
-			@include breakpoint( "782px-960px" ) {
-				right: 18px;
-				left: initial;
-				margin-left: 0;
-			}
-
-			@include breakpoint( ">960px" ) {
-				right: 28px;
-				left: initial;
-				margin-left: 0;
-			}
+		// The unread state ships as `bellUnread` from @wordpress/icons — a
+		// single SVG with the dot baked in. We just colour the dot red so the
+		// urgency signal reads, instead of inheriting currentColor like the
+		// rest of the icon.
+		&.has-unread svg circle {
+			fill: $alert-red;
 		}

 		&:hover,
 		&.components-button:not(:disabled):not([aria-disabled="true"]):hover {
 			box-shadow: none;
-
-			&.has-unread::after,
-			&.woocommerce-layout__activity-panel-tab-wordpress-notices::after {
-				border-color: $gray-200;
-			}
+			// Override Button's default theme-blue hover — Gutenberg's own
+			// icon tab groups keep the same dark-grey across all states.
+			color: $gray-900;
 		}

+		// Suppress the inset focus shadow on mouse-click focus (it conflicts
+		// with the blue underline). Keyboard-only focus still shows via
+		// :focus-visible.
 		&:focus,
 		&.components-button:not(:disabled):not([aria-disabled="true"]):focus {
+			box-shadow: none;
+		}
+		&:focus-visible,
+		&.components-button:not(:disabled):not([aria-disabled="true"]):focus-visible {
 			box-shadow: inset -1px -1px 0 $gray-700, inset 1px 1px 0 $gray-700;
 		}

@@ -160,28 +153,6 @@
 	}
 }

-.woocommerce-layout:has(.woocommerce-homescreen) {
-	.woocommerce-layout__activity-panel-tabs {
-		.woocommerce-layout__activity-panel-tab {
-			color: var(--wp-admin-theme-color) !important;
-			font-size: 13px;
-			font-style: normal;
-			font-weight: 400;
-			line-height: 20px; /* 153.846% */
-
-
-			&.has-icon {
-				padding: 6px 12px;
-			}
-
-			svg {
-				fill: #1e1e1e;
-			}
-		}
-	}
-}
-
-
 .woocommerce-layout__activity-panel-toggle-bubble.has-unread::after {
 	content: " ";
 	position: absolute;
@@ -216,6 +187,13 @@
 }

 .woocommerce-layout__activity-panel-wrapper {
+	// Suppress the browser's default focus outline on click focus (the blue
+	// ring with slight rounded corners). Keyboard nav still gets it via
+	// :focus-visible so a11y is preserved.
+	&:focus {
+		outline: none;
+	}
+
 	height: calc(100vh - #{$header-height + $adminbar-height-mobile});
 	background: $gray-100;
 	width: 430px;
@@ -226,7 +204,10 @@
 	@include activity-panel-slide();
 	position: absolute;
 	right: 0;
-	top: 100%;
+	// Clear the floating header's bottom border so it stays visible when the
+	// slide-over panel is open (otherwise the panel covers it). Shares the
+	// border width with the header — if that ever changes, this stays aligned.
+	top: calc(100% + #{$header-border-width});
 	z-index: 1000;
 	overflow-x: hidden;
 	overflow-y: auto;
diff --git a/plugins/woocommerce/client/admin/client/activity-panel/tab/index.js b/plugins/woocommerce/client/admin/client/activity-panel/tab/index.js
index 3fbd049570e..d00eb2175da 100644
--- a/plugins/woocommerce/client/admin/client/activity-panel/tab/index.js
+++ b/plugins/woocommerce/client/admin/client/activity-panel/tab/index.js
@@ -36,13 +36,13 @@ export const Tab = ( {
 			key={ tabKey }
 			id={ tabKey }
 			data-testid={ tabKey }
-			aria-label={ ariaLabel }
+			label={ title || ariaLabel }
+			showTooltip
 			onClick={ () => {
 				onTabClick( name );
 			} }
 		>
 			{ icon }
-			{ title }{ ' ' }
 			{ unread && (
 				<span className="screen-reader-text">
 					{ __( 'unread activity', 'woocommerce' ) }
diff --git a/plugins/woocommerce/client/admin/client/activity-panel/tab/test/index.js b/plugins/woocommerce/client/admin/client/activity-panel/tab/test/index.js
index 10244a319e6..1d2a1fc332b 100644
--- a/plugins/woocommerce/client/admin/client/activity-panel/tab/test/index.js
+++ b/plugins/woocommerce/client/admin/client/activity-panel/tab/test/index.js
@@ -25,7 +25,7 @@ const renderTab = () =>

 describe( 'ActivityPanel Tab', () => {
 	it( 'displays a title and unread status based on props', () => {
-		const { getByText } = render(
+		const { getByRole, getByText } = render(
 			<Tab
 				icon={ null }
 				title={ 'Hello World' }
@@ -38,7 +38,12 @@ describe( 'ActivityPanel Tab', () => {
 			/>
 		);

-		expect( getByText( 'Hello World' ) ).not.toBeNull();
+		// Title is exposed as the button's accessible name (aria-label) so
+		// screen readers announce it, even though icon-only tabs no longer
+		// render the title text visually.
+		expect(
+			getByRole( 'tab', { name: 'Hello World' } )
+		).toBeInTheDocument();
 		expect( getByText( 'unread activity' ) ).not.toBeNull();
 	} );

diff --git a/plugins/woocommerce/client/admin/client/activity-panel/tabs/index.js b/plugins/woocommerce/client/admin/client/activity-panel/tabs/index.js
index 006df7e9010..94ab397b478 100644
--- a/plugins/woocommerce/client/admin/client/activity-panel/tabs/index.js
+++ b/plugins/woocommerce/client/admin/client/activity-panel/tabs/index.js
@@ -2,33 +2,26 @@
  * External dependencies
  */
 import { NavigableMenu } from '@wordpress/components';
-import { useEffect, useState } from '@wordpress/element';
-import { recordEvent } from '@woocommerce/tracks';

 /**
  * Internal dependencies
  */
 import { Tab } from '../tab';

+/**
+ * Renders the activity-panel tab strip. Decisions about what a click means
+ * (open / close / switch) live in the parent's `onTabClick` handler, not
+ * here — Tabs just forwards the clicked tab and renders selection state
+ * from props. Previously this component maintained its own mirror of the
+ * panel's open state and computed click intent from it, which created a
+ * one-frame gap where a same-tab close could pop back open mid-animation.
+ */
 export const Tabs = ( {
 	tabs,
 	onTabClick,
 	selectedTab: selectedTabName,
 	tabOpen = false,
 } ) => {
-	const [ { tabOpen: tabIsOpenState, currentTab }, setTabState ] = useState( {
-		tabOpen,
-		currentTab: selectedTabName,
-	} );
-
-	// Keep state synced with props
-	useEffect( () => {
-		setTabState( {
-			tabOpen,
-			currentTab: selectedTabName,
-		} );
-	}, [ tabOpen, selectedTabName ] );
-
 	return (
 		<NavigableMenu
 			role="tablist"
@@ -45,28 +38,10 @@ export const Tabs = ( {
 						<Tab
 							key={ i }
 							index={ i }
-							isPanelOpen={ tabIsOpenState }
-							selected={ currentTab === tab.name }
+							isPanelOpen={ tabOpen }
+							selected={ selectedTabName === tab.name }
 							{ ...tab }
-							onTabClick={ () => {
-								const isTabOpen =
-									currentTab === tab.name || currentTab === ''
-										? ! tabIsOpenState
-										: true;
-
-								// If a panel is being opened, or if an existing panel is already open and a different one is being opened, record a track.
-								if ( ! isTabOpen || currentTab !== tab.name ) {
-									recordEvent( 'activity_panel_open', {
-										tab: tab.name,
-									} );
-								}
-
-								setTabState( {
-									tabOpen: isTabOpen,
-									currentTab: tab.name,
-								} );
-								onTabClick( tab, isTabOpen );
-							} }
+							onTabClick={ () => onTabClick( tab ) }
 						/>
 					);
 				} ) }
diff --git a/plugins/woocommerce/client/admin/client/activity-panel/tabs/test/index.js b/plugins/woocommerce/client/admin/client/activity-panel/tabs/test/index.js
index ca0f6afef62..c95f0fca148 100644
--- a/plugins/woocommerce/client/admin/client/activity-panel/tabs/test/index.js
+++ b/plugins/woocommerce/client/admin/client/activity-panel/tabs/test/index.js
@@ -2,14 +2,13 @@
  * External dependencies
  */
 import { render, fireEvent } from '@testing-library/react';
-import { recordEvent } from '@woocommerce/tracks';
+import { useState } from '@wordpress/element';

 /**
  * Internal dependencies
  */
 import { Tabs } from '../';

-jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
 const generateTabs = () => {
 	return [ '0', '1', '2', '3' ].map( ( name ) => ( {
 		name,
@@ -23,35 +22,47 @@ const CustomTab = () => {
 	return <div>Custom Tab</div>;
 };

+// Test wrapper that mirrors the parent ActivityPanel's open/close/switch
+// intent decisions. Tabs is now a controlled component — selection state
+// lives in the parent — so its rendered selection class is only correct
+// when the parent updates `selectedTab` / `tabOpen` in response to clicks.
+const ControlledTabs = ( { initialSelected = '', tabs } ) => {
+	const [ selected, setSelected ] = useState( initialSelected );
+	const [ open, setOpen ] = useState( !! initialSelected );
+	return (
+		<Tabs
+			selectedTab={ selected }
+			tabOpen={ open }
+			tabs={ tabs }
+			onTabClick={ ( tab ) => {
+				const isSameTab = tab.name === selected;
+				const isClosing = isSameTab && open;
+				setSelected( tab.name );
+				setOpen( ! isClosing );
+			} }
+		/>
+	);
+};
+
 describe( 'Activity Panel Tabs', () => {
-	it( 'correctly tracks the selected tab', () => {
+	it( 'renders the selected tab as active when both selectedTab and tabOpen prop in', () => {
 		const { getAllByRole } = render(
-			<Tabs
-				selectedTab={ '3' }
-				tabs={ generateTabs() }
-				onTabClick={ () => {} }
-			/>
+			<ControlledTabs initialSelected="3" tabs={ generateTabs() } />
 		);

 		const tabs = getAllByRole( 'tab' );

 		fireEvent.click( tabs[ 2 ] );
-
 		expect( tabs[ 2 ] ).toHaveClass( 'is-active' );

 		fireEvent.click( tabs[ 3 ] );
-
 		expect( tabs[ 2 ] ).not.toHaveClass( 'is-active' );
 		expect( tabs[ 3 ] ).toHaveClass( 'is-active' );
 	} );

-	it( 'closes a tab if its the same one last opened', () => {
+	it( 'unsets is-active when the same tab is clicked twice in a row', () => {
 		const { getAllByRole } = render(
-			<Tabs
-				selectedTab={ '3' }
-				tabs={ generateTabs() }
-				onTabClick={ () => {} }
-			/>
+			<ControlledTabs initialSelected="3" tabs={ generateTabs() } />
 		);

 		const tabs = getAllByRole( 'tab' );
@@ -62,7 +73,7 @@ describe( 'Activity Panel Tabs', () => {
 		expect( tabs[ 2 ] ).not.toHaveClass( 'is-active' );
 	} );

-	it( 'triggers onTabClick with the selected when a tab is clicked', () => {
+	it( 'forwards the clicked tab to onTabClick', () => {
 		const tabClickSpy = jest.fn();
 		const generatedTabs = generateTabs();

@@ -78,27 +89,7 @@ describe( 'Activity Panel Tabs', () => {

 		fireEvent.click( tabs[ 3 ] );

-		expect( tabClickSpy ).toHaveBeenCalledWith( generatedTabs[ 3 ], true );
-	} );
-
-	it( 'records an event when panels are being opened and when the open panel changes', () => {
-		const generatedTabs = generateTabs();
-
-		const { getAllByRole } = render(
-			<Tabs
-				selectedTab={ '3' }
-				tabs={ generatedTabs }
-				onTabClick={ () => {} }
-			/>
-		);
-
-		const tabs = getAllByRole( 'tab' );
-
-		fireEvent.click( tabs[ 3 ] );
-
-		expect( recordEvent ).toHaveBeenCalledWith( 'activity_panel_open', {
-			tab: generatedTabs[ 3 ].name,
-		} );
+		expect( tabClickSpy ).toHaveBeenCalledWith( generatedTabs[ 3 ] );
 	} );

 	it( 'should render tabs with a custom component defined in tab config', () => {
diff --git a/plugins/woocommerce/client/admin/client/activity-panel/test/index.js b/plugins/woocommerce/client/admin/client/activity-panel/test/index.js
index 06db14fb6ab..60ff1e7a5f9 100644
--- a/plugins/woocommerce/client/admin/client/activity-panel/test/index.js
+++ b/plugins/woocommerce/client/admin/client/activity-panel/test/index.js
@@ -95,7 +95,7 @@ describe( 'Activity Panel', () => {
 	it( 'should render inbox tab on embedded pages', () => {
 		render( <ActivityPanel isEmbedded query={ {} } /> );

-		expect( screen.getByText( 'Activity' ) ).toBeDefined();
+		expect( screen.getByRole( 'tab', { name: 'Activity' } ) ).toBeDefined();
 	} );

 	it( 'should render inbox tab if not on home screen', () => {
@@ -103,19 +103,21 @@ describe( 'Activity Panel', () => {
 			<ActivityPanel query={ { page: 'wc-admin', path: '/customers' } } />
 		);

-		expect( screen.getByText( 'Activity' ) ).toBeDefined();
+		expect( screen.getByRole( 'tab', { name: 'Activity' } ) ).toBeDefined();
 	} );

 	it( 'should not render inbox tab on home screen', () => {
 		render( <ActivityPanel query={ { page: 'wc-admin' } } /> );

-		expect( screen.queryByText( 'Inbox' ) ).toBeNull();
+		expect( screen.queryByRole( 'tab', { name: 'Inbox' } ) ).toBeNull();
 	} );

 	it( 'should render preview store tab on home screen', () => {
 		render( <ActivityPanel query={ { page: 'wc-admin' } } /> );

-		expect( screen.getByText( 'Preview store' ) ).toBeDefined();
+		expect(
+			screen.getByRole( 'tab', { name: 'Preview store' } )
+		).toBeDefined();
 	} );

 	it( 'should not render help tab if not on home screen', () => {
@@ -167,7 +169,7 @@ describe( 'Activity Panel', () => {
 		);

 		// Expect that "Help" tab is absent.
-		expect( screen.queryByText( 'Help' ) ).toBeNull();
+		expect( screen.queryByRole( 'tab', { name: 'Help' } ) ).toBeNull();
 	} );

 	it( 'should render display options if on home screen', () => {
@@ -183,7 +185,7 @@ describe( 'Activity Panel', () => {
 	} );

 	it( 'should only render the finish setup link when TaskList is not complete', () => {
-		const { queryByText, rerender } = render(
+		const { queryByRole, rerender } = render(
 			<ActivityPanel
 				query={ {
 					task: 'products',
@@ -191,7 +193,7 @@ describe( 'Activity Panel', () => {
 			/>
 		);

-		expect( queryByText( 'Finish setup' ) ).toBeDefined();
+		expect( queryByRole( 'tab', { name: 'Finish setup' } ) ).toBeDefined();

 		useSelect.mockImplementation( () => ( {
 			requestingTaskListOptions: false,
@@ -207,11 +209,11 @@ describe( 'Activity Panel', () => {
 			/>
 		);

-		expect( queryByText( 'Finish setup' ) ).toBeNull();
+		expect( queryByRole( 'tab', { name: 'Finish setup' } ) ).toBeNull();
 	} );

 	it( 'should not render the finish setup link when on the home screen and TaskList is not complete', () => {
-		const { queryByText } = render(
+		const { queryByRole } = render(
 			<ActivityPanel
 				query={ {
 					page: 'wc-admin',
@@ -220,15 +222,17 @@ describe( 'Activity Panel', () => {
 			/>
 		);

-		expect( queryByText( 'Finish setup' ) ).toBeNull();
+		expect( queryByRole( 'tab', { name: 'Finish setup' } ) ).toBeNull();
 	} );

 	it( 'should render the finish setup link when on embedded pages and TaskList is not complete', () => {
-		const { getByText } = render(
+		const { getByRole } = render(
 			<ActivityPanel isEmbedded query={ {} } />
 		);

-		expect( getByText( 'Finish setup' ) ).toBeInTheDocument();
+		expect(
+			getByRole( 'tab', { name: 'Finish setup' } )
+		).toBeInTheDocument();
 	} );

 	it( 'should not render the finish setup link when a user does not have capabilities', () => {
@@ -236,7 +240,7 @@ describe( 'Activity Panel', () => {
 			currentUserCan: () => false,
 		} ) );

-		const { queryByText } = render(
+		const { queryByRole } = render(
 			<ActivityPanel
 				query={ {
 					task: 'products',
@@ -244,7 +248,7 @@ describe( 'Activity Panel', () => {
 			/>
 		);

-		expect( queryByText( 'Finish setup' ) ).toBeDefined();
+		expect( queryByRole( 'tab', { name: 'Finish setup' } ) ).toBeDefined();
 	} );

 	describe( 'panel', () => {
diff --git a/plugins/woocommerce/client/admin/client/header/embed.tsx b/plugins/woocommerce/client/admin/client/header/embed.tsx
index ca73a1b6a9c..e76b74d0208 100644
--- a/plugins/woocommerce/client/admin/client/header/embed.tsx
+++ b/plugins/woocommerce/client/admin/client/header/embed.tsx
@@ -1,9 +1,18 @@
+/**
+ * External dependencies
+ */
+import clsx from 'clsx';
+import { __ } from '@wordpress/i18n';
+import { Button, Icon } from '@wordpress/components';
+import { cog, help } from '@wordpress/icons';
+
 /**
  * Internal dependencies
  */
 import './style.scss';
 import { isTaskListActive } from '~/hooks/use-tasklists-state';
 import { BaseHeader } from './shared';
+import { useWpAdminChrome } from './use-wp-admin-chrome';

 export const EmbedHeader = ( {
 	sections,
@@ -19,12 +28,70 @@ export const EmbedHeader = ( {
 		isTaskListActive( 'setup' ) && ! isReactifyPaymentsSettingsScreen
 	);

+	// Embed pages live on top of classic wp-admin screens. Detect the wp-admin
+	// chrome wp-admin already rendered so we can suppress the duplicate <h1>,
+	// proxy the Screen Options / Help dropdowns through floating-header icons,
+	// and collapse the bar to chrome-only height when there is no title to show.
+	const {
+		hasH1: hasWpAdminH1,
+		hasScreenOptions,
+		hasContextualHelp,
+		activeMetaIcon,
+		triggerMetaIcon,
+	} = useWpAdminChrome( query );
+
+	const trailingItems = (
+		<>
+			{ /* Screen Options + Help icons consolidated into the floating
+			header. Only rendered when wp-admin would have rendered the
+			corresponding entry point. The original wp-admin wraps are
+			visually hidden via CSS and these icons proxy clicks into them
+			through triggerMetaIcon. */ }
+			{ hasScreenOptions && (
+				<Button
+					className={ clsx( 'woocommerce-layout__header-meta-icon', {
+						'is-active': activeMetaIcon === 'screen-options',
+					} ) }
+					label={ __( 'Screen options', 'woocommerce' ) }
+					aria-expanded={ activeMetaIcon === 'screen-options' }
+					showTooltip
+					onClick={ () =>
+						triggerMetaIcon(
+							'screen-options',
+							'#show-settings-link'
+						)
+					}
+				>
+					<Icon icon={ cog } size={ 18 } />
+				</Button>
+			) }
+			{ hasContextualHelp && (
+				<Button
+					className={ clsx( 'woocommerce-layout__header-meta-icon', {
+						'is-active': activeMetaIcon === 'help',
+					} ) }
+					label={ __( 'Help', 'woocommerce' ) }
+					aria-expanded={ activeMetaIcon === 'help' }
+					showTooltip
+					onClick={ () =>
+						triggerMetaIcon( 'help', '#contextual-help-link' )
+					}
+				>
+					<Icon icon={ help } size={ 18 } />
+				</Button>
+			) }
+		</>
+	);
+
 	return (
 		<BaseHeader
 			isEmbedded={ true }
 			query={ query }
 			sections={ sections }
 			showReminderBar={ showReminderBar }
+			suppressTitle={ hasWpAdminH1 }
+			compact={ hasWpAdminH1 }
+			trailingItems={ trailingItems }
 		/>
 	);
 };
diff --git a/plugins/woocommerce/client/admin/client/header/shared.tsx b/plugins/woocommerce/client/admin/client/header/shared.tsx
index b1cb5a2b981..00a705c29fb 100644
--- a/plugins/woocommerce/client/admin/client/header/shared.tsx
+++ b/plugins/woocommerce/client/admin/client/header/shared.tsx
@@ -84,6 +84,15 @@ export const getPageTitle = ( sections: string[] ) => {
 	return pageTitle;
 };

+/**
+ * BaseHeader is a dumb layout component shared by Header (non-embedded WC
+ * admin pages) and EmbedHeader (overlay on top of classic wp-admin pages).
+ * It owns the fixed-position bar, body-margin sync, slot rendering, and
+ * the optional reminder bar. Anything wp-admin-specific (h1 suppression,
+ * compact-bar mode, Screen Options / Help proxy icons) is the caller's
+ * responsibility — passed in via `suppressTitle`, `compact`, and the
+ * `trailingItems` slot.
+ */
 export const BaseHeader = ( {
 	isEmbedded,
 	query,
@@ -91,6 +100,9 @@ export const BaseHeader = ( {
 	sections,
 	children,
 	leftAlign = true,
+	suppressTitle = false,
+	compact = false,
+	trailingItems,
 }: {
 	isEmbedded: boolean;
 	query: Record< string, string >;
@@ -98,6 +110,24 @@ export const BaseHeader = ( {
 	sections: string[];
 	children?: React.ReactNode;
 	leftAlign?: boolean;
+	/**
+	 * When true, render a spacer instead of the title. Caller (EmbedHeader)
+	 * sets this on classic post-type screens where wp-admin already renders
+	 * its own <h1>. Page-title slot fills always win over this flag — if a
+	 * fill is registered, it renders regardless.
+	 */
+	suppressTitle?: boolean;
+	/**
+	 * When true, collapse the bar to admin-bar height. Used in tandem with
+	 * `suppressTitle` to give the bar a chrome-only treatment.
+	 */
+	compact?: boolean;
+	/**
+	 * Items rendered at the right edge, after WooHeaderItem.Slot. EmbedHeader
+	 * uses this for the gear / ? icons that proxy clicks into wp-admin's
+	 * Screen Options and Help dropdowns.
+	 */
+	trailingItems?: React.ReactNode;
 } ) => {
 	const { isScrolled } = useIsScrolled();

@@ -110,10 +140,16 @@ export const BaseHeader = ( {
 		headerItemSlot,
 	} );

+	const shouldRenderTitle = hasPageTitleFills || ! suppressTitle;
+
 	return (
 		<div
 			className={ clsx( 'woocommerce-layout__header', {
 				'is-scrolled': isScrolled,
+				// Chrome-only treatment: bar collapses to admin-bar height when
+				// the caller requests it (e.g. Edit Order, Edit Product, Add
+				// Product, where wp-admin renders its own title below).
+				'is-chrome-only': compact,
 			} ) }
 			ref={ headerElement }
 		>
@@ -128,25 +164,37 @@ export const BaseHeader = ( {
 					fillProps={ { isEmbedded, query } }
 				/>

-				<Text
-					className={ clsx( 'woocommerce-layout__header-heading', {
-						'woocommerce-layout__header-left-align': leftAlign,
-					} ) }
-					as="h1"
-				>
-					{ decodeEntities(
-						hasPageTitleFills ? (
+				{ shouldRenderTitle ? (
+					<Text
+						className={ clsx(
+							'woocommerce-layout__header-heading',
+							{
+								'woocommerce-layout__header-left-align':
+									leftAlign,
+							}
+						) }
+						as="h1"
+					>
+						{ hasPageTitleFills ? (
 							<WooHeaderPageTitle.Slot
 								fillProps={ { isEmbedded, query } }
 							/>
 						) : (
-							getPageTitle( sections )
-						)
-					) }
-				</Text>
+							decodeEntities( getPageTitle( sections ) )
+						) }
+					</Text>
+				) : (
+					// Spacer keeps WooHeaderItem.Slot pinned right when no
+					// title renders.
+					<div
+						className="woocommerce-layout__header-spacer"
+						aria-hidden="true"
+					/>
+				) }

 				{ children }
 				<WooHeaderItem.Slot fillProps={ { isEmbedded, query } } />
+				{ trailingItems }
 			</div>
 		</div>
 	);
diff --git a/plugins/woocommerce/client/admin/client/header/style.scss b/plugins/woocommerce/client/admin/client/header/style.scss
index 921e6043767..48c5be76a7f 100644
--- a/plugins/woocommerce/client/admin/client/header/style.scss
+++ b/plugins/woocommerce/client/admin/client/header/style.scss
@@ -1,5 +1,56 @@
+// Standard visually-hidden pattern — keeps the element in the layout tree
+// (so jQuery delegated handlers still fire on it) but moves it off-screen
+// and clips its rendered area to nothing. Used twice in this file for the
+// activity-panel tab title spans and the wp-admin meta-toggle wraps.
+@mixin visually-hidden {
+	position: absolute;
+	width: 1px;
+	height: 1px;
+	margin: -1px;
+	padding: 0;
+	border: 0;
+	overflow: hidden;
+	clip: rect(0 0 0 0);
+	clip-path: inset(50%);
+	white-space: nowrap;
+}
+
+// Hide ONLY the two standard wp-admin meta-toggle wraps (Screen Options +
+// Help) that we proxy via the floating header's gear / ? icons. The parent
+// #screen-meta-links container is left alone so any third-party sibling
+// buttons (the rare plugin that injects its own `.screen-meta-toggle` div
+// directly into the strip) remain visible. Visually-hidden pattern (rather
+// than display:none) keeps the elements in the layout tree so wp-admin's
+// jQuery delegated click handlers still fire when we proxy .click() into
+// #show-settings-link / #contextual-help-link.
+// Defensive guard: only hide when the floating header's meta-icon button
+// has actually rendered (`:has(.woocommerce-layout__header-meta-icon)`).
+// If hasScreenOptions / hasContextualHelp detection misses for any reason,
+// :has() doesn't match and the user keeps access to the original wp-admin
+// strip rather than losing it entirely. Browsers without :has() support
+// (Firefox <121) also fall through to the safe state.
+.woocommerce-admin-page:has(.woocommerce-layout__header-meta-icon),
+body.woocommerce_page_wc-admin:has(.woocommerce-layout__header-meta-icon) {
+	#screen-options-link-wrap,
+	#contextual-help-link-wrap {
+		@include visually-hidden;
+	}
+}
+
+// Chrome-only narrow-bar height (Edit Order, Edit Product, Add Product, etc.).
+// Now that activity-panel tabs are icon-only globally, they fit cleanly in 32px.
+$woo-chrome-only-header-height: 32px;
+
 .woocommerce-layout__header {
-	background: $studio-white;
+	// Background matches the wp-admin body grey so the bar reads as
+	// infrastructure rather than content. Needs to be explicit (not inherited)
+	// because `position: fixed` lets page content scroll behind the bar — a
+	// transparent background would show that content through. Bottom border
+	// anchors the bar visually against the page below. Settings-page tweaks
+	// (drop the border so the bar reads as one strip with the tab nav)
+	// live in the legacy admin.scss alongside the rest of that page's overrides.
+	background: $wp-admin-background;
+	border-bottom: $header-border-width solid $wp-admin-border;
 	box-sizing: border-box;
 	padding: 0;
 	position: fixed;
@@ -39,7 +90,7 @@
 		padding: 0 0 0 $fallback-gutter-large;
 		padding: 0 0 0 $gutter-large;
 		height: $header-height;
-		background: $studio-white;
+		background: transparent;
 		font-weight: 600;
 		font-size: 14px;

@@ -47,6 +98,108 @@
 			flex: 1 auto;
 		}
 	}
+
+	// Spacer that takes up the title's flex space when no h1 renders
+	// (embedded post-type screens), so right-side items stay pinned right.
+	.woocommerce-layout__header-spacer {
+		flex: 1 auto;
+	}
+
+	// Screen Options + Help icon buttons — visually mirror the
+	// activity-panel-tab pattern so bell / list / megaphone / gear / ? all
+	// look identical regardless of which subsystem renders them, including
+	// the blue ::before underline on the active tab.
+	.woocommerce-layout__header-meta-icon {
+		display: flex;
+		flex-direction: column;
+		justify-content: center;
+		align-items: center;
+		position: relative;
+		border: none;
+		outline: none;
+		cursor: pointer;
+		background-color: transparent;
+		height: $header-height;
+		// wp-admin admin-bar items use ~0 8px horizontal padding each side;
+		// match it for a tight, infrastructure-y spacing.
+		min-width: 0;
+		padding: 0 8px;
+		// $gray-900 across all states (default, hover, active) — the blue
+		// underline alone signals active, matching Gutenberg's icon tab group.
+		color: $gray-900;
+
+		&::before {
+			background-color: var(--wp-admin-theme-color);
+			bottom: 0;
+			content: "";
+			height: 0;
+			opacity: 0;
+			transition-property: height, opacity;
+			transition-duration: 300ms;
+			transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
+			left: 0;
+			position: absolute;
+			right: 0;
+		}
+
+		&.is-active {
+			box-shadow: none;
+
+			&::before {
+				// 2px to match Gutenberg's TabPanel + Woo's own
+				// nav-tab-active indicator on Settings.
+				height: 2px;
+				opacity: 1;
+			}
+		}
+
+		&:hover,
+		&.components-button:not(:disabled):not([aria-disabled="true"]):hover {
+			box-shadow: none;
+			background-color: transparent;
+			// Override Button's default theme-blue hover — Gutenberg's own
+			// icon tab groups keep the same dark-grey across all states.
+			color: $gray-900;
+		}
+
+		// Suppress the inset focus shadow on mouse-click focus (it conflicts
+		// with the blue underline). Keyboard-only focus still shows a ring
+		// via :focus-visible, preserving accessibility.
+		&:focus,
+		&.components-button:not(:disabled):not([aria-disabled="true"]):focus {
+			box-shadow: none;
+		}
+		&:focus-visible,
+		&.components-button:not(:disabled):not([aria-disabled="true"]):focus-visible {
+			box-shadow: inset -1px -1px 0 $gray-700,
+				inset 1px 1px 0 $gray-700;
+		}
+
+		svg {
+			fill: currentColor;
+		}
+	}
+
+	// Chrome-only treatment: bar collapses to 32px on pages without a title.
+	// Hard height + max-height + overflow:hidden so the wrapper can't be pushed
+	// taller by any naturally-sized child (Finish setup widget, plugin fills, etc.).
+	// Inner activity-panel and meta-icon items also collapse so they fit cleanly.
+	&.is-chrome-only {
+		.woocommerce-layout__header-wrapper {
+			height: $woo-chrome-only-header-height;
+			min-height: $woo-chrome-only-header-height;
+			max-height: $woo-chrome-only-header-height;
+			overflow: hidden;
+		}
+
+		.woocommerce-layout__activity-panel-tabs,
+		.woocommerce-layout__activity-panel-tab,
+		.woocommerce-layout__header-meta-icon {
+			height: $woo-chrome-only-header-height;
+			min-height: $woo-chrome-only-header-height;
+			max-height: $woo-chrome-only-header-height;
+		}
+	}
 }

 .folded .woocommerce-layout__header {
diff --git a/plugins/woocommerce/client/admin/client/header/use-wp-admin-chrome.ts b/plugins/woocommerce/client/admin/client/header/use-wp-admin-chrome.ts
new file mode 100644
index 00000000000..8b6240a2efa
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/header/use-wp-admin-chrome.ts
@@ -0,0 +1,179 @@
+/**
+ * External dependencies
+ */
+import { useEffect, useState } from '@wordpress/element';
+
+type ActiveMetaIcon = 'screen-options' | 'help' | null;
+
+type WpAdminChrome = {
+	hasH1: boolean;
+	hasScreenOptions: boolean;
+	hasContextualHelp: boolean;
+	activeMetaIcon: ActiveMetaIcon;
+	triggerMetaIcon: (
+		which: Exclude< ActiveMetaIcon, null >,
+		triggerId: string
+	) => void;
+};
+
+/**
+ * Detect and orchestrate wp-admin chrome that's already rendered on classic
+ * admin pages (Edit Order, Edit Product, Settings, etc.). On a non-embedded
+ * Woo-custom page the queried elements aren't in the DOM and every return
+ * value falls back to a no-op default.
+ *
+ * Three signals are read synchronously on mount and re-read on route changes:
+ *   - `.wrap > h1.wp-heading-inline` — wp-admin's own page title. When present
+ *     the embed header should suppress its own <h1> to avoid duplicating it.
+ *   - `#screen-options-link-wrap` and `#contextual-help-link-wrap` — the
+ *     wp-admin meta-toggle wraps. When present we render proxy gear / ? icons
+ *     in the floating header and hide the originals via CSS.
+ *
+ * The hook also owns the meta-icon orchestration:
+ *   - `activeMetaIcon` stays in sync with each trigger's `aria-expanded` via a
+ *     `MutationObserver`, so React state updates only happen *after* a click
+ *     has fully settled — never during.
+ *   - `triggerMetaIcon` closes any open activity-panel tab and the other
+ *     wp-admin dropdown before opening the target one (mutual exclusion).
+ *   - A document-level capture-phase click listener closes any open wp-admin
+ *     dropdown when an activity-panel tab is clicked. (Sync goes both ways.)
+ *
+ * Lazy initial state reads the DOM on first render so embed pages don't flash
+ * a duplicate-title frame before the first effect commits.
+ */
+export const useWpAdminChrome = (
+	query: Record< string, string >
+): WpAdminChrome => {
+	const detectWpAdminChrome = () => ( {
+		hasH1: !! document.querySelector( '.wrap > h1.wp-heading-inline' ),
+		hasScreenOptions: !! document.querySelector(
+			'#screen-options-link-wrap'
+		),
+		hasContextualHelp: !! document.querySelector(
+			'#contextual-help-link-wrap'
+		),
+	} );
+	const [ chrome, setChrome ] = useState( detectWpAdminChrome );
+	useEffect( () => {
+		setChrome( detectWpAdminChrome() );
+	}, [ query ] );
+	const { hasH1, hasScreenOptions, hasContextualHelp } = chrome;
+
+	const [ activeMetaIcon, setActiveMetaIcon ] =
+		useState< ActiveMetaIcon >( null );
+
+	// Reverse-direction sync: when an activity-panel tab is clicked AND a
+	// wp-admin dropdown is currently open, close the dropdown. We don't update
+	// React state from this handler (state syncs reactively via the
+	// MutationObserver below), so no setTimeout deferral is needed.
+	useEffect( () => {
+		const handler = ( e: Event ) => {
+			const target = e.target as HTMLElement | null;
+			if (
+				! target?.closest( '.woocommerce-layout__activity-panel-tab' )
+			) {
+				return;
+			}
+			document
+				.querySelector< HTMLButtonElement >(
+					'#show-settings-link[aria-expanded="true"]'
+				)
+				?.click();
+			document
+				.querySelector< HTMLButtonElement >(
+					'#contextual-help-link[aria-expanded="true"]'
+				)
+				?.click();
+		};
+		document.addEventListener( 'click', handler, true );
+		return () => document.removeEventListener( 'click', handler, true );
+	}, [] );
+
+	// Keep activeMetaIcon in sync with the actual wp-admin dropdown state by
+	// observing aria-expanded changes on the trigger buttons.
+	useEffect( () => {
+		if ( ! hasScreenOptions && ! hasContextualHelp ) {
+			setActiveMetaIcon( null );
+			return;
+		}
+		const screenOptBtn = document.querySelector< HTMLButtonElement >(
+			'#show-settings-link'
+		);
+		const helpBtn = document.querySelector< HTMLButtonElement >(
+			'#contextual-help-link'
+		);
+		const sync = () => {
+			const screenOpen =
+				screenOptBtn?.getAttribute( 'aria-expanded' ) === 'true';
+			const helpOpen =
+				helpBtn?.getAttribute( 'aria-expanded' ) === 'true';
+			let next: ActiveMetaIcon = null;
+			if ( screenOpen ) {
+				next = 'screen-options';
+			} else if ( helpOpen ) {
+				next = 'help';
+			}
+			setActiveMetaIcon( next );
+		};
+		sync();
+		const observer = new MutationObserver( sync );
+		const opts = {
+			attributes: true,
+			attributeFilter: [ 'aria-expanded' ],
+		};
+		if ( screenOptBtn ) observer.observe( screenOptBtn, opts );
+		if ( helpBtn ) observer.observe( helpBtn, opts );
+		return () => observer.disconnect();
+	}, [ hasScreenOptions, hasContextualHelp ] );
+
+	const triggerMetaIcon = (
+		which: Exclude< ActiveMetaIcon, null >,
+		triggerId: string
+	) => {
+		// Close any open activity-panel tab so the five icons act as one group.
+		document
+			.querySelector< HTMLButtonElement >(
+				'.woocommerce-layout__activity-panel-tab.is-active'
+			)
+			?.click();
+		// Close the OTHER wp-admin dropdown if open (mutual exclusion between
+		// gear ↔ help). Chain the new open off the closing trigger's
+		// aria-expanded flip rather than a magic-number setTimeout — wp-admin's
+		// screen-meta.js sets aria-expanded synchronously when its handler fires,
+		// so the observer fires as soon as the close has registered, regardless
+		// of however long the slideUp animation takes. Self-disconnects on first
+		// flip so back-to-back clicks don't accumulate observers.
+		const otherTriggerId =
+			which === 'screen-options'
+				? '#contextual-help-link'
+				: '#show-settings-link';
+		const otherOpen = document.querySelector< HTMLButtonElement >(
+			`${ otherTriggerId }[aria-expanded="true"]`
+		);
+		const openTarget = () =>
+			document.querySelector< HTMLButtonElement >( triggerId )?.click();
+		if ( otherOpen ) {
+			const chain = new MutationObserver( () => {
+				if ( otherOpen.getAttribute( 'aria-expanded' ) !== 'true' ) {
+					chain.disconnect();
+					openTarget();
+				}
+			} );
+			chain.observe( otherOpen, {
+				attributes: true,
+				attributeFilter: [ 'aria-expanded' ],
+			} );
+			otherOpen.click();
+		} else {
+			openTarget();
+		}
+	};
+
+	return {
+		hasH1,
+		hasScreenOptions,
+		hasContextualHelp,
+		activeMetaIcon,
+		triggerMetaIcon,
+	};
+};
diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss
index 5783efacd96..1f94df85062 100644
--- a/plugins/woocommerce/client/legacy/css/admin.scss
+++ b/plugins/woocommerce/client/legacy/css/admin.scss
@@ -9329,6 +9329,15 @@ body.woocommerce_page_wc-settings {
 		padding-bottom: 0px;
 	}

+	// Floating header on Settings sits directly above the tab nav with no
+	// content between — treat both strips as one continuous title region
+	// by dropping the header's bottom border. The tab strip keeps its own
+	// bottom border (separating tabs from content); we just colour it to
+	// match the rest of the chrome.
+	.woocommerce-layout__header {
+		border-bottom: 0;
+	}
+
 	p.submit {
 		margin-bottom: 0;
 	}
@@ -9435,8 +9444,13 @@ body.woocommerce_page_wc-settings {
 			}
 		}
 		margin: 0;
-		background: #FFF;
-		border-bottom: 1px solid #EBEBEB;
+		// Tab strip inherits the wp-admin body bg so it reads as one
+		// continuous title region with the floating header above. Bottom
+		// border colour matches the rest of the wp-admin chrome borders
+		// ($wp-admin-border in packages/js/internal-style-build); kept as
+		// a literal here because legacy admin.scss doesn't import the
+		// shared variables file.
+		border-bottom: 1px solid #dcdcde;

 		.nav-tab-active, .nav-tab-active:focus, .nav-tab-active:focus:active, .nav-tab-active:hover {
 			border-bottom: 2px solid var(--wp-admin-theme-color, #3858e9);
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/order/order-edit.spec.ts b/plugins/woocommerce/tests/e2e-pw/tests/order/order-edit.spec.ts
index 78bada4620f..a485af0204a 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/order/order-edit.spec.ts
+++ b/plugins/woocommerce/tests/e2e-pw/tests/order/order-edit.spec.ts
@@ -101,9 +101,9 @@ test.describe( 'Edit order', { tag: [ tags.SERVICES, tags.HPOS ] }, () => {
 		}

 		// confirm we're on the orders page
-		await expect( page.locator( 'h1.components-text' ) ).toContainText(
-			'Orders'
-		);
+		await expect(
+			page.locator( 'h1.components-text, h1.wp-heading-inline' )
+		).toContainText( 'Orders' );
 		// open order we created
 		await page.goto(
 			`wp-admin/admin.php?page=wc-orders&action=edit&id=${ orderId }`