diff --git a/DEPS b/DEPS
index c7d4e89..86ad5a7 100644
--- a/DEPS
+++ b/DEPS
@@ -312,15 +312,15 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
-  'skia_revision': 'ddff74ea6faeb0590bdab8ff99ec9c57a0dd5281',
+  'skia_revision': 'c16d0e9f30b1a1613401c0db3c93a3c2aa37c8ba',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'v8_revision': '8fd9b68b176e8b623c40d907940be52146424b3f',
+  'v8_revision': '78c603b9859d310dd919660859a29d5afe88b165',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
-  'angle_revision': '83a9513da745fe45774bab896af6937d0cd00b64',
+  'angle_revision': 'f4d31298079144b856580892285f4a1244168325',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -332,7 +332,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling BoringSSL
   # and whatever else without interference from each other.
-  'boringssl_revision': 'd0fcf49574b06de7090cba4ad8124df638a0134e',
+  'boringssl_revision': '16d1a81e475be0b3f859eb2264642bc1b6501f6e',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Fuchsia sdk
   # and whatever else without interference from each other.
@@ -400,7 +400,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling devtools-frontend
   # and whatever else without interference from each other.
-  'devtools_frontend_revision': 'eb79da005f6e25c84235e6f077d25a12d8533a57',
+  'devtools_frontend_revision': '0d56199f472ca4a345cc7e2d364b241c7ec01a66',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libprotobuf-mutator
   # and whatever else without interference from each other.
@@ -424,7 +424,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'dawn_revision': '56b2aa287dbb7ce4f73aace630ace091db714f96',
+  'dawn_revision': 'ebc5ca4df7447bb12bce11975a9f9fed8e379131',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -1198,7 +1198,7 @@
       'packages': [
           {
               'package': 'chromium/chrome/android/orderfiles/arm',
-              'version': 'oOwt8NJkRI98HOVzvkkk7hObFoYjdmhNBhIcbUHgC7MC',
+              'version': 'xk-C1pnkiPjborb84hke8cTBEeFv_zeiT1RwsjVhXdwC',
           },
       ],
       'condition': 'checkout_android and non_git_source',
@@ -1209,7 +1209,7 @@
       'packages': [
           {
               'package': 'chromium/chrome/android/orderfiles/arm64',
-              'version': 'LmjaoiI8kPBPL_69zrl2CKTBaDCeE7XRsOasEojbxuwC',
+              'version': 'tOqZFlF4SoG43bm_-Q4SoPdi7jPqfOGQgv6YgilIj-YC',
           },
       ],
       'condition': 'checkout_android and non_git_source',
@@ -1220,7 +1220,7 @@
       'packages': [
           {
               'package': 'chromium/android_webview/tools/orderfiles/arm',
-              'version': 'XKuBhtoiAmaI8syAQH97ZjecMa-ROxGXa_IJFOT0Ab8C',
+              'version': 'CizcjY9a7vFFUMze3SVWz4c0GE4F2Z0iLvAUqrdVdp8C',
           },
       ],
       'condition': 'checkout_android and non_git_source',
@@ -1231,7 +1231,7 @@
       'packages': [
           {
               'package': 'chromium/android_webview/tools/orderfiles/arm64',
-              'version': 'i-yNYyrVBqRgXJn5WTA9CGUK6MHZKkTkwnRNfYScc9QC',
+              'version': 'wlDQG8kkav46Xf535ptLp3F_ZXiaS74qjzrl4Jx84AcC',
           },
       ],
       'condition': 'checkout_android and non_git_source',
@@ -1616,7 +1616,7 @@
     'packages': [
       {
         'package': 'chromium/chrome/test/data/variations/cipd',
-        'version': 'zwxNEJsIrQmOfZDtCrxpkXTjHjTkRHKUw1bkFO32pQQC',
+        'version': 'uN364SdJaY-1qBGcLtG6HJHtxur2eixzaSVcLAV6rwgC',
       },
     ],
     'condition': 'non_git_source',
@@ -1628,7 +1628,7 @@
 
   'src/clank': {
     'url': Var('chrome_git') + '/clank/internal/apps.git' + '@' +
-    '3d95afd50da9d85ff51c766764cdc068ad987408',
+    'cb2add85959c63a84544feea0f82b211261ab582',
     'condition': 'checkout_android and checkout_src_internal',
   },
 
@@ -2463,7 +2463,7 @@
     Var('chromium_git') + '/chromiumos/platform/libva-fake-driver.git' + '@' + 'a9bcab9cd6b15d4e3634ca44d5e5f7652c612194',
 
   'src/third_party/libvpx/source/libvpx':
-    Var('chromium_git') + '/webm/libvpx.git' + '@' +  '4fcebeabe58e79255d291acb4cead4ed7953149e',
+    Var('chromium_git') + '/webm/libvpx.git' + '@' +  '9a2d3d1f46afbdfa9b9820a9fd3aacb084e65e2f',
 
   'src/third_party/libwebm/source':
     Var('chromium_git') + '/webm/libwebm.git' + '@' + 'f2a982d748b80586ae53b89a2e6ebbc305848b8c',
@@ -2617,7 +2617,7 @@
     Var('pdfium_git') + '/pdfium.git' + '@' +  Var('pdfium_revision'),
 
   'src/third_party/perfetto':
-    Var('chromium_git') + '/external/github.com/google/perfetto.git' + '@' + '21c77783aa90ba9b46e79949c797ca6bc516c124',
+    Var('chromium_git') + '/external/github.com/google/perfetto.git' + '@' + '05b8b9cf93e16e960e71f38bf2e41944b4611f22',
 
   'src/base/tracing/test/data': {
     'bucket': 'perfetto',
@@ -2958,16 +2958,16 @@
       'dep_type': 'cipd',
   },
 
-  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@158abcd52e365129a0b81b455947e88a646e5691',
+  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@4267dde11f3f6bef6b3aad0c5c3ba69723224ef6',
   'src/third_party/glslang/src': '{chromium_git}/external/github.com/KhronosGroup/glslang@e966816ab28ab7cb448d5b33270b43c941b343d4',
   'src/third_party/spirv-cross/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Cross@b8fcf307f1f347089e3c46eb4451d27f32ebc8d3',
   'src/third_party/spirv-headers/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Headers@f88a2d766840fc825af1fc065977953ba1fa4a91',
-  'src/third_party/spirv-tools/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Tools@4972c69eb50255b314fc0925ca757c4417e6b6c0',
+  'src/third_party/spirv-tools/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Tools@c28f5937bce369dde1d645299a8c9873da43dc72',
   'src/third_party/vulkan-headers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Headers@ad9ce1235e88dc09287e19171dfac384db8ec32c',
   'src/third_party/vulkan-loader/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Loader@e0e501b0ba42df7b3af023470ad068c48a3ac4de',
   'src/third_party/vulkan-tools/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Tools@7f423e2b242c154e6ace85c804c65462a7d41870',
   'src/third_party/vulkan-utility-libraries/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Utility-Libraries@738ec97a3f659dd6469bff3c4078ef981b0a343f',
-  'src/third_party/vulkan-validation-layers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-ValidationLayers@f020266adee4bb87e8fde219f6fb31f8f141213e',
+  'src/third_party/vulkan-validation-layers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-ValidationLayers@c363aa381dedb3164e7cf5a22c4744b179cd16fe',
 
   'src/third_party/vulkan_memory_allocator':
     Var('chromium_git') + '/external/github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git' + '@' + 'cb0597213b0fcb999caa9ed08c2f88dc45eb7d50',
@@ -3056,7 +3056,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/linux-amd64',
-          'version': 'dDJy-uNkXWQn3xmZRuEI7BcmlkN3K9ylNybgAASX4BEC',
+          'version': 'ZXSSW0FBo2-2jFEQ-b_NgfeyDEpiaF_BkFdGs9VbeRIC',
         },
       ],
       'dep_type': 'cipd',
@@ -3066,7 +3066,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/windows-amd64',
-          'version': 'cxA88QPdbxNnjIZmW04DeNnKaRRKswsooAFs0Zugn1oC',
+          'version': 't6fiuykcHm5BKtcB_anI4X3FYtu6ug598Kw_C3hRYeAC',
         },
       ],
       'dep_type': 'cipd',
@@ -3077,7 +3077,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/mac-amd64',
-          'version': 'jU-KEixlKctSlxyVNoVzfVux5hjl7aCEqMBDXv7F9gkC',
+          'version': 'HPbJssh2NyhlgvBKZBOhbXLzjKkm7F7ufw7FvAR6XxkC',
         },
       ],
       'dep_type': 'cipd',
@@ -3088,7 +3088,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/mac-arm64',
-          'version': 'X8cxk6coGe7XqqQcTSNT2vYSF9zb3ntBg_1jBMd9e1MC',
+          'version': 'vV3LO-qpzoTA8HlYXlNVXAJWD9QLL2aCRAkSNx98gS0C',
         },
       ],
       'dep_type': 'cipd',
@@ -3793,7 +3793,7 @@
 
   'src/ios_internal':  {
       'url': Var('chrome_git') + '/chrome/ios_internal.git' + '@' +
-        '7001a5dce04cd3827b70be178d4afc858dd49c1a',
+        'bc858cb1047c92e4ed92fe0d82dc28f11b6be51c',
       'condition': 'checkout_ios and checkout_src_internal',
   },
 
diff --git a/agents/extensions/build-information/tests/host_arch.promptfoo.yaml b/agents/extensions/build-information/tests/host_arch.promptfoo.yaml
index 4c914f8..07226db 100644
--- a/agents/extensions/build-information/tests/host_arch.promptfoo.yaml
+++ b/agents/extensions/build-information/tests/host_arch.promptfoo.yaml
@@ -1,5 +1,5 @@
 prompts:
-  - 'What is the architecture of the current host?'
+  - 'What target_cpu value should I set if I want to compile for the host I am currently on?'
 
 # The provider is our custom Python script.
 providers:
diff --git a/agents/testing/gemini_provider.py b/agents/testing/gemini_provider.py
index 9fb7e78a..e9f6d77a 100644
--- a/agents/testing/gemini_provider.py
+++ b/agents/testing/gemini_provider.py
@@ -286,6 +286,11 @@
     else:
         settings_json = {}
 
+    settings_json.setdefault('general', {})
+    # Retry flaky connection timeouts, which happen on occasion when running
+    # prompt eval tests.
+    settings_json['general']['retryFetchErrors'] = True
+
     settings_json.setdefault('telemetry', {})
     settings_json['telemetry']['enabled'] = True
     settings_json['telemetry']['outfile'] = str(telemetry_outfile)
diff --git a/agents/testing/gemini_provider_unittest.py b/agents/testing/gemini_provider_unittest.py
index 61d24d6..0ae9fb3 100755
--- a/agents/testing/gemini_provider_unittest.py
+++ b/agents/testing/gemini_provider_unittest.py
@@ -163,12 +163,16 @@
         self.assertTrue(os.path.exists(settings_file))
         with open(settings_file, 'r', encoding='utf-8') as f:
             settings = json.load(f)
-        self.assertEqual(settings, {
-            'telemetry': {
-                'enabled': True,
-                'outfile': str(telemetry_outfile),
-            },
-        })
+        self.assertEqual(
+            settings, {
+                'general': {
+                    'retryFetchErrors': True,
+                },
+                'telemetry': {
+                    'enabled': True,
+                    'outfile': str(telemetry_outfile),
+                },
+            })
 
     def test_updates_existing_settings_file(self):
         """Tests that an existing settings file is updated."""
@@ -186,6 +190,9 @@
             settings = json.load(f)
         self.assertEqual(
             settings, {
+                'general': {
+                    'retryFetchErrors': True,
+                },
                 'other_setting': 'value',
                 'telemetry': {
                     'enabled': True,
@@ -193,6 +200,38 @@
                 }
             })
 
+    def test_updates_existing_general_settings(self):
+        """Tests that existing general settings are updated."""
+        home_dir = pathlib.Path('/fake/home')
+        telemetry_outfile = pathlib.Path('/fake/telemetry.json')
+        gemini_dir = home_dir / '.gemini'
+        os.makedirs(gemini_dir)
+        settings_file = gemini_dir / 'settings.json'
+        with open(settings_file, 'w', encoding='utf-8') as f:
+            json.dump(
+                {
+                    'general': {
+                        'retryFetchErrors': False,
+                        'someOtherSetting': True,
+                    },
+                }, f)
+
+        gemini_provider._configure_gemini_cli(home_dir, telemetry_outfile)
+
+        with open(settings_file, 'r', encoding='utf-8') as f:
+            settings = json.load(f)
+        self.assertEqual(
+            settings, {
+                'general': {
+                    'retryFetchErrors': True,
+                    'someOtherSetting': True,
+                },
+                'telemetry': {
+                    'enabled': True,
+                    'outfile': str(telemetry_outfile),
+                },
+            })
+
     def test_updates_existing_telemetry_settings(self):
         """Tests that existing telemetry settings are updated."""
         home_dir = pathlib.Path('/fake/home')
@@ -213,12 +252,16 @@
 
         with open(settings_file, 'r', encoding='utf-8') as f:
             settings = json.load(f)
-        self.assertEqual(settings, {
-            'telemetry': {
-                'enabled': True,
-                'outfile': str(telemetry_outfile),
-            },
-        })
+        self.assertEqual(
+            settings, {
+                'general': {
+                    'retryFetchErrors': True,
+                },
+                'telemetry': {
+                    'enabled': True,
+                    'outfile': str(telemetry_outfile),
+                },
+            })
 
     def test_creates_trusted_folders_file(self):
         """Tests that a new trusted folders file is created."""
diff --git a/android_webview/browser/aw_field_trials.cc b/android_webview/browser/aw_field_trials.cc
index 8eb7a16..cd77dcfb 100644
--- a/android_webview/browser/aw_field_trials.cc
+++ b/android_webview/browser/aw_field_trials.cc
@@ -329,4 +329,9 @@
   // Disable No-Vary-Search in disk cache on WebView.
   // See https://crbug.com/382394774.
   aw_feature_overrides.DisableFeature(net::features::kHttpCacheNoVarySearch);
+
+  // TODO(crbug.com/489450060): Disable DirectReceiver on Viz for WebView until
+  // its Viz thread is updated to handle IO.
+  aw_feature_overrides.DisableFeature(
+      ::features::kVizDirectCompositorThreadIpcFrameSinkManager);
 }
diff --git a/android_webview/common/crash_reporter/crash_keys.cc b/android_webview/common/crash_reporter/crash_keys.cc
index 01ba814d..ff958cc8 100644
--- a/android_webview/common/crash_reporter/crash_keys.cc
+++ b/android_webview/common/crash_reporter/crash_keys.cc
@@ -216,6 +216,7 @@
     "SIFactory-Size",
 
     // crbug.com/453113611
+    "SubprocessMetricsProvider-merge_result",
     "SubprocessMetricsProvider-histogram",
 
     // crbug.com/456871291
diff --git a/android_webview/java/src/org/chromium/android_webview/AwContentRestrictionManagerBridge.java b/android_webview/java/src/org/chromium/android_webview/AwContentRestrictionManagerBridge.java
index bc60a701..6218d55 100644
--- a/android_webview/java/src/org/chromium/android_webview/AwContentRestrictionManagerBridge.java
+++ b/android_webview/java/src/org/chromium/android_webview/AwContentRestrictionManagerBridge.java
@@ -25,7 +25,9 @@
     @CalledByNative
     public static boolean isContentRestrictionEnabled() {
         if (!AwFeatureMap.isEnabled(AwFeatures.WEBVIEW_CONTENT_RESTRICTION_SUPPORT)) {
-            Log.w(TAG, "Content restriction feature support is disabled.");
+            return false;
+        }
+        if (!Boolean.TRUE.equals(ManifestMetadataUtil.getContentRestrictionAppOptInPreference())) {
             return false;
         }
         AconfigFlaggedApiDelegate delegate =
diff --git a/android_webview/java/src/org/chromium/android_webview/ManifestMetadataUtil.java b/android_webview/java/src/org/chromium/android_webview/ManifestMetadataUtil.java
index d146ef8..30f4506 100644
--- a/android_webview/java/src/org/chromium/android_webview/ManifestMetadataUtil.java
+++ b/android_webview/java/src/org/chromium/android_webview/ManifestMetadataUtil.java
@@ -18,6 +18,8 @@
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.build.annotations.Nullable;
 
+import javax.annotation.concurrent.GuardedBy;
+
 /**
  * Utility class to fetch metadata declared in the ApplicationManifest.xml file of the embedding
  * app.
@@ -60,7 +62,11 @@
     private static final String XRW_ALLOWLIST_METADATA_NAME =
             "REQUESTED_WITH_HEADER_ORIGIN_ALLOW_LIST";
 
-    @Nullable private static volatile MetadataCache sMetadataCache;
+    private static final Object sLock = new Object();
+
+    @GuardedBy("sLock")
+    @Nullable
+    private static volatile MetadataCache sMetadataCache;
 
     /**
      * Cache for all AndroidManifest.xml meta-data. All meta-data should be fetched at the time this
@@ -95,17 +101,28 @@
      * initialize the cache with the given context otherwise.
      */
     public static void ensureMetadataCacheInitialized(Context context) {
-        if (sMetadataCache == null) {
-            sMetadataCache = new MetadataCache(context);
+        synchronized (sLock) {
+            if (sMetadataCache == null) {
+                sMetadataCache = new MetadataCache(context);
+            }
         }
     }
 
     @VisibleForTesting
     public static MetadataCache getMetadataCache() {
-        if (sMetadataCache == null) {
-            sMetadataCache = new MetadataCache(ContextUtils.getApplicationContext());
+        synchronized (sLock) {
+            if (sMetadataCache == null) {
+                sMetadataCache = new MetadataCache(ContextUtils.getApplicationContext());
+            }
+            return sMetadataCache;
         }
-        return sMetadataCache;
+    }
+
+    @VisibleForTesting
+    public static void clearMetadataCache() {
+        synchronized (sLock) {
+            sMetadataCache = null;
+        }
     }
 
     /**
diff --git a/android_webview/junit/src/org/chromium/android_webview/robolectric/AwContentRestrictionManagerBridgeTest.java b/android_webview/junit/src/org/chromium/android_webview/robolectric/AwContentRestrictionManagerBridgeTest.java
index d9d8316..6df596be 100644
--- a/android_webview/junit/src/org/chromium/android_webview/robolectric/AwContentRestrictionManagerBridgeTest.java
+++ b/android_webview/junit/src/org/chromium/android_webview/robolectric/AwContentRestrictionManagerBridgeTest.java
@@ -9,6 +9,9 @@
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
+import android.content.ComponentName;
+import android.os.Bundle;
+
 import androidx.test.filters.SmallTest;
 
 import org.junit.Assert;
@@ -19,11 +22,15 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
+import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
 import org.chromium.android_webview.AwContentRestrictionManagerBridge;
+import org.chromium.android_webview.ManifestMetadataUtil;
 import org.chromium.android_webview.common.AwFeatures;
+import org.chromium.android_webview.test.util.ManifestMetadataMockApplicationContext;
 import org.chromium.base.AconfigFlaggedApiDelegate;
+import org.chromium.base.ContextUtils;
 import org.chromium.base.test.BaseRobolectricTestRunner;
 import org.chromium.base.test.util.Feature;
 import org.chromium.base.test.util.Features.DisableFeatures;
@@ -36,9 +43,28 @@
     @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
     @Mock AconfigFlaggedApiDelegate mFlaggedApiDelegate;
 
+    private static final String ENABLE_CONTENT_RESTRICTION_METADATA_NAME =
+            "android.webkit.WebView.EnableContentRestriction";
+    private static final String METADATA_HOLDER_SERVICE_NAME =
+            "android.webkit.MetaDataHolderService";
+
+    private ManifestMetadataMockApplicationContext mContext;
+    private ComponentName mMetadataServiceName;
+
     @Before
     public void setUp() {
+        mContext = new ManifestMetadataMockApplicationContext(RuntimeEnvironment.application);
+        mMetadataServiceName = new ComponentName(mContext, METADATA_HOLDER_SERVICE_NAME);
+        ContextUtils.initApplicationContextForTests(mContext);
         AconfigFlaggedApiDelegate.setInstanceForTesting(mFlaggedApiDelegate);
+        setEnableContentRestrictionMetadata(true);
+    }
+
+    private void setEnableContentRestrictionMetadata(boolean enabled) {
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(ENABLE_CONTENT_RESTRICTION_METADATA_NAME, enabled);
+        mContext.putServiceMetadata(mMetadataServiceName, bundle);
+        ManifestMetadataUtil.clearMetadataCache();
     }
 
     @Test
@@ -63,4 +89,14 @@
         Assert.assertFalse(AwContentRestrictionManagerBridge.isContentRestrictionEnabled());
         verify(mFlaggedApiDelegate, times(2)).isContentRestrictionEnabled();
     }
+
+    @Test
+    @SmallTest
+    @Feature({"AndroidWebView"})
+    @EnableFeatures({AwFeatures.WEBVIEW_CONTENT_RESTRICTION_SUPPORT})
+    public void testIsContentRestrictionEnabled_appOptOut() {
+        setEnableContentRestrictionMetadata(false);
+        Assert.assertFalse(AwContentRestrictionManagerBridge.isContentRestrictionEnabled());
+        verifyNoInteractions(mFlaggedApiDelegate);
+    }
 }
diff --git a/android_webview/test/BUILD.gn b/android_webview/test/BUILD.gn
index a33c65e..85cd24a 100644
--- a/android_webview/test/BUILD.gn
+++ b/android_webview/test/BUILD.gn
@@ -946,6 +946,7 @@
 
   deps = [
     ":crash_test_utils_java",
+    ":webview_instrumentation_test_utils_java",
     "//android_webview:android_webview_java",
     "//android_webview/nonembedded:crash_java",
     "//android_webview/nonembedded:services_java",
diff --git a/ash/system/input_device_settings/OWNERS b/ash/system/input_device_settings/OWNERS
index a0c1f0b..b5b14a6 100644
--- a/ash/system/input_device_settings/OWNERS
+++ b/ash/system/input_device_settings/OWNERS
@@ -1 +1,2 @@
 michaelcheco@google.com
+wangdanny@google.com
diff --git a/base/BUILD.gn b/base/BUILD.gn
index 9b6cdba4..902ad8a 100644
--- a/base/BUILD.gn
+++ b/base/BUILD.gn
@@ -2893,7 +2893,6 @@
 static_library("base_static") {
   sources = [
     "base_export.h",
-    "base_switches.cc",
     "base_switches.h",
     "immediate_crash.h",
   ]
@@ -4679,7 +4678,7 @@
   java_cpp_strings("java_switches_srcjar") {
     # External code should depend on ":base_java" instead.
     visibility = [ ":*" ]
-    sources = [ "base_switches.cc" ]
+    sources = [ "base_switches.h" ]
     template = "android/java/src/org/chromium/base/BaseSwitches.java.tmpl"
   }
 
@@ -5513,7 +5512,7 @@
     sources = [
       "android/java/src/org/chromium/base/AconfigFlaggedApiDelegate.java",
       "android/java/src/org/chromium/base/BindingRequestQueue.java",
-      "android/java/src/org/chromium/base/PasswordEchoSplitSettingDelegate.java",
+      "android/java/src/org/chromium/base/PasswordEchoSettingDelegate.java",
       "android/java/src/org/chromium/base/SelectionActionMenuClientWrapper.java",
       "android/java/src/org/chromium/base/serial/SerialManager.java",
       "android/java/src/org/chromium/base/serial/SerialPort.java",
diff --git a/base/android/java/src/org/chromium/base/AconfigFlaggedApiDelegate.java b/base/android/java/src/org/chromium/base/AconfigFlaggedApiDelegate.java
index 9f2f1014..0515d6d2 100644
--- a/base/android/java/src/org/chromium/base/AconfigFlaggedApiDelegate.java
+++ b/base/android/java/src/org/chromium/base/AconfigFlaggedApiDelegate.java
@@ -366,10 +366,10 @@
     }
 
     /**
-     * Returns the {@link PasswordEchoSplitSettingDelegate} if the feature to split the Android
-     * setting 'Show passwords' is enabled. the feature is enabled. Returns null otherwise.
+     * Returns the {@link PasswordEchoSettingDelegate} if the feature to split the Android setting
+     * 'Show passwords' is enabled. the feature is enabled. Returns null otherwise.
      */
-    default @Nullable PasswordEchoSplitSettingDelegate getPasswordEchoSplitSettingDelegate() {
+    default @Nullable PasswordEchoSettingDelegate getPasswordEchoSettingDelegate() {
         return null;
     }
 
diff --git a/base/android/java/src/org/chromium/base/PasswordEchoSplitSettingDelegate.java b/base/android/java/src/org/chromium/base/PasswordEchoSettingDelegate.java
similarity index 84%
rename from base/android/java/src/org/chromium/base/PasswordEchoSplitSettingDelegate.java
rename to base/android/java/src/org/chromium/base/PasswordEchoSettingDelegate.java
index 1fd4bd155..b435afb 100644
--- a/base/android/java/src/org/chromium/base/PasswordEchoSplitSettingDelegate.java
+++ b/base/android/java/src/org/chromium/base/PasswordEchoSettingDelegate.java
@@ -6,9 +6,9 @@
 
 import org.chromium.build.annotations.NullMarked;
 
-/** Delegate interface for Password Echo Split settings. */
+/** Delegate interface for Password Echo settings. */
 @NullMarked
-public interface PasswordEchoSplitSettingDelegate {
+public interface PasswordEchoSettingDelegate {
     /** Registers a callback to be invoked when the password echo settings change. */
     void registerCallback(Runnable callback);
 
diff --git a/base/auto_reset.h b/base/auto_reset.h
index c17bc25..ce2b3b1 100644
--- a/base/auto_reset.h
+++ b/base/auto_reset.h
@@ -5,10 +5,11 @@
 #ifndef BASE_AUTO_RESET_H_
 #define BASE_AUTO_RESET_H_
 
-#include <utility>
 // Necessary per <utility>'s usage of `sizeof(std::intmax_t)` without IWYU.
 #include <stdint.h>
 
+#include <utility>
+
 #include "base/check_op.h"
 #include "base/memory/raw_ptr_exclusion.h"
 
diff --git a/base/base_switches.cc b/base/base_switches.cc
deleted file mode 100644
index 4cfa5372..0000000
--- a/base/base_switches.cc
+++ /dev/null
@@ -1,197 +0,0 @@
-// Copyright 2012 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "base/base_switches.h"
-
-#include "build/build_config.h"
-
-namespace switches {
-
-// Delays execution of TaskPriority::BEST_EFFORT tasks until shutdown.
-const char kDisableBestEffortTasks[] = "disable-best-effort-tasks";
-
-// Disables the crash reporting.
-const char kDisableBreakpad[] = "disable-breakpad";
-
-// Comma-separated list of feature names to disable. See also kEnableFeatures.
-const char kDisableFeatures[] = "disable-features";
-
-// Force disabling of low-end device mode when set.
-const char kDisableLowEndDeviceMode[] = "disable-low-end-device-mode";
-
-// Indicates that crash reporting should be enabled. On platforms where helper
-// processes cannot access to files needed to make this decision, this flag is
-// generated internally.
-const char kEnableCrashReporter[] = "enable-crash-reporter";
-
-// Comma-separated list of feature names to enable. See also kDisableFeatures.
-const char kEnableFeatures[] = "enable-features";
-
-// Force low-end device mode when set.
-const char kEnableLowEndDeviceMode[] = "enable-low-end-device-mode";
-
-// Configure the background threadpool field trial.
-const char kBackgroundThreadPoolFieldTrial[] =
-    "background-thread-pool-field-trial";
-
-// Handle to the shared memory segment containing field trial state that is to
-// be shared between processes. The argument to this switch is made of segments
-// separated by commas:
-// - The platform-specific handle id for the shared memory as a string.
-// - (Windows only) i=inherited by duplication or p=child must open parent.
-// - The high 64 bits of the shared memory block GUID.
-// - The low 64 bits of the shared memory block GUID.
-// - The size of the shared memory segment as a string.
-const char kFieldTrialHandle[] = "field-trial-handle";
-
-// This option can be used to force field trials when testing changes locally.
-// The argument is a list of name and value pairs, separated by slashes. If a
-// trial name is prefixed with an asterisk, that trial will start activated.
-// For example, the following argument defines two trials, with the second one
-// activated: "GoogleNow/Enable/*MaterialDesignNTP/Default/" This option can
-// also be used by the browser process to send the list of trials to a
-// non-browser process, using the same format. See
-// FieldTrialList::CreateTrialsFromString() in field_trial.h for details.
-const char kForceFieldTrials[] = "force-fieldtrials";
-
-// Generates full memory crash dump.
-const char kFullMemoryCrashReport[] = "full-memory-crash-report";
-
-// Logs information about all tasks posted with TaskPriority::BEST_EFFORT. Use
-// this to diagnose issues that are thought to be caused by
-// TaskPriority::BEST_EFFORT execution fences. Note: Tasks posted to a
-// non-BEST_EFFORT UpdateableSequencedTaskRunner whose priority is later lowered
-// to BEST_EFFORT are not logged.
-const char kLogBestEffortTasks[] = "log-best-effort-tasks";
-
-// Handle to the shared memory segment a child process should use to transmit
-// histograms back to the browser process.
-const char kMetricsSharedMemoryHandle[] = "metrics-shmem-handle";
-
-// Suppresses all error dialogs when present.
-const char kNoErrorDialogs[] = "noerrdialogs";
-
-// Starts the sampling based profiler for the browser process at startup. This
-// will only work if chrome has been built with the gn arg enable_profiling =
-// true. The output will go to the value of kProfilingFile.
-const char kProfilingAtStart[] = "profiling-at-start";
-
-// Specifies a location for profiling output. This will only work if chrome has
-// been built with the gyp variable profiling=1 or gn arg enable_profiling=true.
-//
-//   {pid} if present will be replaced by the pid of the process.
-//   {count} if present will be incremented each time a profile is generated
-//           for this process.
-// The default is chrome-profile-{pid} for the browser and test-profile-{pid}
-// for tests.
-const char kProfilingFile[] = "profiling-file";
-
-// Controls whether profile data is periodically flushed to a file. Normally
-// the data gets written on exit but cases exist where chromium doesn't exit
-// cleanly (especially when using single-process). A time in seconds can be
-// specified.
-const char kProfilingFlush[] = "profiling-flush";
-
-// When running certain tests that spawn child processes, this switch indicates
-// to the test framework that the current process is a child process.
-const char kTestChildProcess[] = "test-child-process";
-
-// Sends trace events from these categories to a file.
-// --trace-to-file on its own sends to default categories.
-const char kTraceToFile[] = "trace-to-file";
-
-// Specifies the file name for --trace-to-file. If unspecified, it will
-// go to a default file name.
-const char kTraceToFileName[] = "trace-to-file-name";
-
-// Gives the default maximal active V-logging level; 0 is the default.
-// Normally positive values are used for V-logging levels.
-const char kV[] = "v";
-
-// Gives the per-module maximal V-logging levels to override the value
-// given by --v.  E.g. "my_module=2,foo*=3" would change the logging
-// level for all code in source files "my_module.*" and "foo*.*"
-// ("-inl" suffixes are also disregarded for this matching).
-//
-// Any pattern containing a forward or backward slash will be tested
-// against the whole pathname and not just the module.  E.g.,
-// "*/foo/bar/*=2" would change the logging level for all code in
-// source files under a "foo/bar" directory.
-const char kVModule[] = "vmodule";
-
-// Will wait for 60 seconds for a debugger to come to attach to the process.
-const char kWaitForDebugger[] = "wait-for-debugger";
-
-#if BUILDFLAG(IS_WIN)
-// Disable high-resolution timer on Windows.
-const char kDisableHighResTimer[] = "disable-highres-timer";
-
-// Disables the USB keyboard detection for blocking the OSK on Windows.
-const char kDisableUsbKeyboardDetect[] = "disable-usb-keyboard-detect";
-
-// Forces the use of QPC for TimeTicks even if cpuid doesn't report the presence
-// of an invariant TSC.
-const char kForceHighResTimeTicks[] = "force-high-res-timeticks";
-#endif
-
-#if BUILDFLAG(IS_LINUX)
-// The /dev/shm partition is too small in certain VM environments, causing
-// Chrome to fail or crash (see http://crbug.com/715363). Use this flag to
-// work-around this issue (a temporary directory will always be used to create
-// anonymous shared memory files).
-const char kDisableDevShmUsage[] = "disable-dev-shm-usage";
-#endif
-
-#if BUILDFLAG(IS_POSIX)
-// Used for turning on Breakpad crash reporting in a debug environment where
-// crash reporting is typically compiled but disabled.
-const char kEnableCrashReporterForTesting[] =
-    "enable-crash-reporter-for-testing";
-#endif
-
-#if BUILDFLAG(IS_ANDROID)
-// For testing, do not initialize child service process but also do not exit
-// (until requested by browser).
-const char kAndroidSkipChildServiceInitForTesting[] =
-    "android-skip-child-service-init-for-testing";
-
-// Default country code to be used for search engine localization.
-const char kDefaultCountryCodeAtInstall[] = "default-country-code";
-
-// Adds additional thread idle time information into the trace event output.
-const char kEnableIdleTracing[] = "enable-idle-tracing";
-
-// Forces the DeviceInfo.isDesktop() check to return true. Can be used to enable
-// desktop-only features on other form factors.
-const char kForceDesktopAndroid[] = "force-desktop-android";
-
-// When we retrieve the package name within the SDK Runtime, we need to use
-// a bit of a hack to do this by taking advantage of the fact that the pid
-// is the same pid as the application's pid + 10000.
-// see:
-// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/Process.java;l=292;drc=47fffdd53115a9af1820e3f89d8108745be4b55d
-// When the render process is created however, it is just a regular isolated
-// process with no particular association so we can't perform the same hack.
-// When creating minidumps, the package name is retrieved from the process
-// meaning the render process minidumps would end up reporting a generic
-// process name not associated with the app.
-// We work around this by feeding through the host package information to the
-// render process when launching it.
-const char kHostPackageName[] = "host-package-name";
-const char kHostPackageLabel[] = "host-package-label";
-const char kHostVersionCode[] = "host-version-code";
-const char kPackageName[] = "package-name";
-const char kPackageVersionName[] = "package-version-name";
-#endif
-
-#if BUILDFLAG(IS_CHROMEOS)
-// Override the default scheduling boosting value for urgent tasks.
-// This can be adjusted if a specific chromeos device shows better perf/power
-// ratio (e.g. by running video conference tests).
-// Currently, this values directs to linux scheduler's utilization min clamp.
-// Range is 0(no biased load) ~ 100(mamximum load value).
-const char kSchedulerBoostUrgent[] = "scheduler-boost-urgent";
-#endif
-
-}  // namespace switches
diff --git a/base/base_switches.h b/base/base_switches.h
index ca266ebb..1c8c6d0 100644
--- a/base/base_switches.h
+++ b/base/base_switches.h
@@ -11,61 +11,195 @@
 
 namespace switches {
 
-extern const char kDisableBestEffortTasks[];
-extern const char kDisableBreakpad[];
-extern const char kDisableFeatures[];
-extern const char kDisableLowEndDeviceMode[];
-extern const char kEnableCrashReporter[];
-extern const char kEnableFeatures[];
-extern const char kEnableLowEndDeviceMode[];
-extern const char kBackgroundThreadPoolFieldTrial[];
-extern const char kFieldTrialHandle[];
-extern const char kForceFieldTrials[];
-extern const char kFullMemoryCrashReport[];
-extern const char kLogBestEffortTasks[];
-extern const char kMetricsSharedMemoryHandle[];
-extern const char kNoErrorDialogs[];
-extern const char kProfilingAtStart[];
-extern const char kProfilingFile[];
-extern const char kProfilingFlush[];
-extern const char kTestChildProcess[];
-extern const char kTraceToFile[];
-extern const char kTraceToFileName[];
-extern const char kV[];
-extern const char kVModule[];
-extern const char kWaitForDebugger[];
+// Delays execution of TaskPriority::BEST_EFFORT tasks until shutdown.
+inline constexpr char kDisableBestEffortTasks[] = "disable-best-effort-tasks";
+
+// Disables the crash reporting.
+inline constexpr char kDisableBreakpad[] = "disable-breakpad";
+
+// Comma-separated list of feature names to disable. See also kEnableFeatures.
+inline constexpr char kDisableFeatures[] = "disable-features";
+
+// Force disabling of low-end device mode when set.
+inline constexpr char kDisableLowEndDeviceMode[] =
+    "disable-low-end-device-mode";
+
+// Indicates that crash reporting should be enabled. On platforms where helper
+// processes cannot access to files needed to make this decision, this flag is
+// generated internally.
+inline constexpr char kEnableCrashReporter[] = "enable-crash-reporter";
+
+// Comma-separated list of feature names to enable. See also kDisableFeatures.
+inline constexpr char kEnableFeatures[] = "enable-features";
+
+// Force low-end device mode when set.
+inline constexpr char kEnableLowEndDeviceMode[] = "enable-low-end-device-mode";
+
+// Configure the background threadpool field trial.
+inline constexpr char kBackgroundThreadPoolFieldTrial[] =
+    "background-thread-pool-field-trial";
+
+// Handle to the shared memory segment containing field trial state that is to
+// be shared between processes. The argument to this switch is made of segments
+// separated by commas:
+// - The platform-specific handle id for the shared memory as a string.
+// - (Windows only) i=inherited by duplication or p=child must open parent.
+// - The high 64 bits of the shared memory block GUID.
+// - The low 64 bits of the shared memory block GUID.
+// - The size of the shared memory segment as a string.
+inline constexpr char kFieldTrialHandle[] = "field-trial-handle";
+
+// This option can be used to force field trials when testing changes locally.
+// The argument is a list of name and value pairs, separated by slashes. If a
+// trial name is prefixed with an asterisk, that trial will start activated.
+// For example, the following argument defines two trials, with the second one
+// activated: "GoogleNow/Enable/*MaterialDesignNTP/Default/" This option can
+// also be used by the browser process to send the list of trials to a
+// non-browser process, using the same format. See
+// FieldTrialList::CreateTrialsFromString() in field_trial.h for details.
+inline constexpr char kForceFieldTrials[] = "force-fieldtrials";
+
+// Generates full memory crash dump.
+inline constexpr char kFullMemoryCrashReport[] = "full-memory-crash-report";
+
+// Logs information about all tasks posted with TaskPriority::BEST_EFFORT. Use
+// this to diagnose issues that are thought to be caused by
+// TaskPriority::BEST_EFFORT execution fences. Note: Tasks posted to a
+// non-BEST_EFFORT UpdateableSequencedTaskRunner whose priority is later lowered
+// to BEST_EFFORT are not logged.
+inline constexpr char kLogBestEffortTasks[] = "log-best-effort-tasks";
+
+// Handle to the shared memory segment a child process should use to transmit
+// histograms back to the browser process.
+inline constexpr char kMetricsSharedMemoryHandle[] = "metrics-shmem-handle";
+
+// Suppresses all error dialogs when present.
+inline constexpr char kNoErrorDialogs[] = "noerrdialogs";
+
+// Starts the sampling based profiler for the browser process at startup. This
+// will only work if chrome has been built with the gn arg enable_profiling =
+// true. The output will go to the value of kProfilingFile.
+inline constexpr char kProfilingAtStart[] = "profiling-at-start";
+
+// Specifies a location for profiling output. This will only work if chrome has
+// been built with the gyp variable profiling=1 or gn arg enable_profiling=true.
+//
+//   {pid} if present will be replaced by the pid of the process.
+//   {count} if present will be incremented each time a profile is generated
+//           for this process.
+// The default is chrome-profile-{pid} for the browser and test-profile-{pid}
+// for tests.
+inline constexpr char kProfilingFile[] = "profiling-file";
+
+// Controls whether profile data is periodically flushed to a file. Normally
+// the data gets written on exit but cases exist where chromium doesn't exit
+// cleanly (especially when using single-process). A time in seconds can be
+// specified.
+inline constexpr char kProfilingFlush[] = "profiling-flush";
+
+// When running certain tests that spawn child processes, this switch indicates
+// to the test framework that the current process is a child process.
+inline constexpr char kTestChildProcess[] = "test-child-process";
+
+// Sends trace events from these categories to a file.
+// --trace-to-file on its own sends to default categories.
+inline constexpr char kTraceToFile[] = "trace-to-file";
+
+// Specifies the file name for --trace-to-file. If unspecified, it will
+// go to a default file name.
+inline constexpr char kTraceToFileName[] = "trace-to-file-name";
+
+// Gives the default maximal active V-logging level; 0 is the default.
+// Normally positive values are used for V-logging levels.
+inline constexpr char kV[] = "v";
+
+// Gives the per-module maximal V-logging levels to override the value
+// given by --v.  E.g. "my_module=2,foo*=3" would change the logging
+// level for all code in source files "my_module.*" and "foo*.*"
+// ("-inl" suffixes are also disregarded for this matching).
+//
+// Any pattern containing a forward or backward slash will be tested
+// against the whole pathname and not just the module.  E.g.,
+// "*/foo/bar/*=2" would change the logging level for all code in
+// source files under a "foo/bar" directory.
+inline constexpr char kVModule[] = "vmodule";
+
+// Will wait for 60 seconds for a debugger to come to attach to the process.
+inline constexpr char kWaitForDebugger[] = "wait-for-debugger";
 
 // See flag_descriptions.cc for more details.
 inline constexpr char kEnableBenchmarking[] = "enable-benchmarking";
 
 #if BUILDFLAG(IS_WIN)
-extern const char kDisableHighResTimer[];
-extern const char kDisableUsbKeyboardDetect[];
-extern const char kForceHighResTimeTicks[];
+// Disable high-resolution timer on Windows.
+inline constexpr char kDisableHighResTimer[] = "disable-highres-timer";
+
+// Disables the USB keyboard detection for blocking the OSK on Windows.
+inline constexpr char kDisableUsbKeyboardDetect[] =
+    "disable-usb-keyboard-detect";
+
+// Forces the use of QPC for TimeTicks even if cpuid doesn't report the presence
+// of an invariant TSC.
+inline constexpr char kForceHighResTimeTicks[] = "force-high-res-timeticks";
 #endif
 
 #if BUILDFLAG(IS_LINUX)
-extern const char kDisableDevShmUsage[];
+// The /dev/shm partition is too small in certain VM environments, causing
+// Chrome to fail or crash (see http://crbug.com/715363). Use this flag to
+// work-around this issue (a temporary directory will always be used to create
+// anonymous shared memory files).
+inline constexpr char kDisableDevShmUsage[] = "disable-dev-shm-usage";
 #endif
 
 #if BUILDFLAG(IS_POSIX)
-extern const char kEnableCrashReporterForTesting[];
+// Used for turning on Breakpad crash reporting in a debug environment where
+// crash reporting is typically compiled but disabled.
+inline constexpr char kEnableCrashReporterForTesting[] =
+    "enable-crash-reporter-for-testing";
 #endif
 
 #if BUILDFLAG(IS_ANDROID)
-extern const char kAndroidSkipChildServiceInitForTesting[];
-extern const char kDefaultCountryCodeAtInstall[];
-extern const char kEnableIdleTracing[];
-extern const char kForceDesktopAndroid[];
-extern const char kHostPackageName[];
-extern const char kHostPackageLabel[];
-extern const char kHostVersionCode[];
-extern const char kPackageName[];
-extern const char kPackageVersionName[];
+// For testing, do not initialize child service process but also do not exit
+// (until requested by browser).
+inline constexpr char kAndroidSkipChildServiceInitForTesting[] =
+    "android-skip-child-service-init-for-testing";
+
+// Default country code to be used for search engine localization.
+inline constexpr char kDefaultCountryCodeAtInstall[] = "default-country-code";
+
+// Adds additional thread idle time information into the trace event output.
+inline constexpr char kEnableIdleTracing[] = "enable-idle-tracing";
+
+// Forces the DeviceInfo.isDesktop() check to return true. Can be used to enable
+// desktop-only features on other form factors.
+inline constexpr char kForceDesktopAndroid[] = "force-desktop-android";
+
+// When we retrieve the package name within the SDK Runtime, we need to use
+// a bit of a hack to do this by taking advantage of the fact that the pid
+// is the same pid as the application's pid + 10000.
+// see:
+// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/Process.java;l=292;drc=47fffdd53115a9af1820e3f89d8108745be4b55d
+// When the render process is created however, it is just a regular isolated
+// process with no particular association so we can't perform the same hack.
+// When creating minidumps, the package name is retrieved from the process
+// meaning the render process minidumps would end up reporting a generic
+// process name not associated with the app.
+// We work around this by feeding through the host package information to the
+// render process when launching it.
+inline constexpr char kHostPackageName[] = "host-package-name";
+inline constexpr char kHostPackageLabel[] = "host-package-label";
+inline constexpr char kHostVersionCode[] = "host-version-code";
+inline constexpr char kPackageName[] = "package-name";
+inline constexpr char kPackageVersionName[] = "package-version-name";
 #endif
 
 #if BUILDFLAG(IS_CHROMEOS)
-extern const char kSchedulerBoostUrgent[];
+// Override the default scheduling boosting value for urgent tasks.
+// This can be adjusted if a specific chromeos device shows better perf/power
+// ratio (e.g. by running video conference tests).
+// Currently, this values directs to linux scheduler's utilization min clamp.
+// Range is 0(no biased load) ~ 100(mamximum load value).
+inline constexpr char kSchedulerBoostUrgent[] = "scheduler-boost-urgent";
 #endif
 
 }  // namespace switches
diff --git a/base/enterprise_util.h b/base/enterprise_util.h
index b20622c..b5d74f41 100644
--- a/base/enterprise_util.h
+++ b/base/enterprise_util.h
@@ -5,10 +5,13 @@
 #ifndef BASE_ENTERPRISE_UTIL_H_
 #define BASE_ENTERPRISE_UTIL_H_
 
-#include "base/auto_reset.h"
 #include "base/base_export.h"
 #include "build/build_config.h"
 
+#if BUILDFLAG(IS_WIN)
+#include "base/auto_reset.h"
+#endif
+
 namespace base {
 
 // Returns true if an outside entity manages the current machine. To be
diff --git a/base/enterprise_util_win.cc b/base/enterprise_util_win.cc
index 9309fe45..9ae154b 100644
--- a/base/enterprise_util_win.cc
+++ b/base/enterprise_util_win.cc
@@ -33,8 +33,7 @@
   return base::win::IsEnrolledToDomain() || base::win::IsJoinedToAzureAD();
 }
 
-[[nodiscard]] AutoReset<bool> SetIsEnterpriseDeviceForTesting(
-    bool is_enterprise) {
+AutoReset<bool> SetIsEnterpriseDeviceForTesting(bool is_enterprise) {
   return AutoReset<bool>(&g_is_enterprise_device_for_testing_, is_enterprise);
 }
 
diff --git a/base/metrics/histogram_functions.h b/base/metrics/histogram_functions.h
index eda83e4..86aa158 100644
--- a/base/metrics/histogram_functions.h
+++ b/base/metrics/histogram_functions.h
@@ -71,11 +71,8 @@
 //                                 NewTabPageAction::kClickTitle);
 //
 // `kMaxValue` should be 1000 or less.
-// Note that there is code that refers to implementation details of this
-// function. Keep it synchronized.
-// LINT.IfChange(UmaHistogramEnumeration)
-template <typename T>
-void UmaHistogramEnumeration(std::string_view name, T sample) {
+template <typename StringType, typename T>
+void UmaHistogramEnumeration(const StringType& name, T sample) {
   static_assert(std::is_enum_v<T>, "T is not an enum.");
   // kMaxValue is the max value in enum, so bucket count is one more than that.
   // This also ensures that an enumeration that doesn't define kMaxValue fails
@@ -111,8 +108,8 @@
 // Note: The value in |sample| must be strictly less than |enum_size|. This is
 // otherwise functionally equivalent to the above.
 // `enum_size` must be less than or equal to 1001.
-template <typename T>
-void UmaHistogramEnumeration(std::string_view name, T sample, T enum_size) {
+template <typename StringType, typename T>
+void UmaHistogramEnumeration(const StringType& name, T sample, T enum_size) {
   static_assert(std::is_enum_v<T>, "T is not an enum.");
   DCHECK_LE(static_cast<uintmax_t>(enum_size), static_cast<uintmax_t>(INT_MAX));
   DCHECK_LT(static_cast<uintmax_t>(sample), static_cast<uintmax_t>(enum_size));
@@ -121,7 +118,6 @@
   return UmaHistogramExactLinear(name, static_cast<int>(sample),
                                  static_cast<int>(enum_size));
 }
-// LINT.ThenChange(/base/metrics/histogram_functions_internal_overloads.h:UmaHistogramEnumeration)
 
 // For adding a boolean sample to histogram.
 // Sample usage:
diff --git a/base/metrics/histogram_functions_internal_overloads.h b/base/metrics/histogram_functions_internal_overloads.h
index c7cf079..9d91bcab8 100644
--- a/base/metrics/histogram_functions_internal_overloads.h
+++ b/base/metrics/histogram_functions_internal_overloads.h
@@ -45,68 +45,6 @@
                                          int exclusive_max);
 // LINT.ThenChange(/base/metrics/histogram_functions.h:UmaHistogramExactLinear)
 
-// LINT.IfChange(UmaHistogramEnumeration)
-template <typename T>
-void UmaHistogramEnumeration(const std::string& name, T sample) {
-  static_assert(std::is_enum_v<T>, "T is not an enum.");
-  // kMaxValue is the max value in enum, so bucket count is one more than that.
-  // This also ensures that an enumeration that doesn't define kMaxValue fails
-  // with a semi-useful error ("no member named 'kMaxValue' in ...").
-  constexpr auto kBucketCount = static_cast<int>(T::kMaxValue) + 1;
-  constexpr auto kBucketCountMax =
-      static_cast<int>(LinearHistogram::kBucketCount_MAX);
-  // Note: UmaHistogramExactLinear() adds 1 to the bucket count for the overflow
-  // bucket, so kBucketCount must be less than kBucketCount_MAX.
-  static_assert(kBucketCount < kBucketCountMax,
-                "Enumeration's kMaxValue is out of range of "
-                "LinearHistogram::kBucketCount_MAX. Use a sparse histogram "
-                "instead.");
-  DCHECK_LE(static_cast<uintmax_t>(sample),
-            static_cast<uintmax_t>(T::kMaxValue));
-  return UmaHistogramExactLinear(name, static_cast<int>(sample),
-                                 static_cast<int>(T::kMaxValue) + 1);
-}
-
-template <typename T>
-void UmaHistogramEnumeration(const char* name, T sample) {
-  static_assert(std::is_enum_v<T>, "T is not an enum.");
-  // kMaxValue is the max value in enum, so bucket count is one more than that.
-  // This also ensures that an enumeration that doesn't define kMaxValue fails
-  // with a semi-useful error ("no member named 'kMaxValue' in ...").
-  constexpr auto kBucketCount = static_cast<int>(T::kMaxValue) + 1;
-  constexpr auto kBucketCountMax =
-      static_cast<int>(LinearHistogram::kBucketCount_MAX);
-  // Note: UmaHistogramExactLinear() adds 1 to the bucket count for the overflow
-  // bucket, so kBucketCount must be less than kBucketCount_MAX.
-  static_assert(kBucketCount < kBucketCountMax,
-                "Enumeration's kMaxValue is out of range of "
-                "LinearHistogram::kBucketCount_MAX. Use a sparse histogram "
-                "instead.");
-  DCHECK_LE(static_cast<uintmax_t>(sample),
-            static_cast<uintmax_t>(T::kMaxValue));
-  return UmaHistogramExactLinear(name, static_cast<int>(sample),
-                                 static_cast<int>(T::kMaxValue) + 1);
-}
-
-template <typename T>
-void UmaHistogramEnumeration(const std::string& name, T sample, T enum_size) {
-  static_assert(std::is_enum_v<T>, "T is not an enum.");
-  DCHECK_LE(static_cast<uintmax_t>(enum_size), static_cast<uintmax_t>(INT_MAX));
-  DCHECK_LT(static_cast<uintmax_t>(sample), static_cast<uintmax_t>(enum_size));
-  return UmaHistogramExactLinear(name, static_cast<int>(sample),
-                                 static_cast<int>(enum_size));
-}
-
-template <typename T>
-void UmaHistogramEnumeration(const char* name, T sample, T enum_size) {
-  static_assert(std::is_enum_v<T>, "T is not an enum.");
-  DCHECK_LE(static_cast<uintmax_t>(enum_size), static_cast<uintmax_t>(INT_MAX));
-  DCHECK_LT(static_cast<uintmax_t>(sample), static_cast<uintmax_t>(enum_size));
-  return UmaHistogramExactLinear(name, static_cast<int>(sample),
-                                 static_cast<int>(enum_size));
-}
-// LINT.ThenChange(/base/metrics/histogram_functions.h:UmaHistogramEnumeration)
-
 // LINT.IfChange(UmaHistogramBoolean)
 BASE_EXPORT void UmaHistogramBoolean(const std::string& name, bool sample);
 BASE_EXPORT void UmaHistogramBoolean(const char* name, bool sample);
diff --git a/base/metrics/persistent_histogram_allocator.cc b/base/metrics/persistent_histogram_allocator.cc
index 72aae7a..3e7f941 100644
--- a/base/metrics/persistent_histogram_allocator.cc
+++ b/base/metrics/persistent_histogram_allocator.cc
@@ -78,7 +78,7 @@
   return bucket_count * kBytesPerBucket;
 }
 
-bool MergeSamplesToExistingHistogram(
+PersistentHistogramAllocator::MergeResult MergeSamplesToExistingHistogram(
     HistogramBase* existing,
     const HistogramBase* histogram,
     std::unique_ptr<HistogramSamples> samples) {
@@ -87,10 +87,11 @@
   if (existing_type == HistogramType::DUMMY_HISTOGRAM) {
     // Merging into a dummy histogram (e.g. histogram is expired) is a no-op and
     // not considered a failure case.
-    return true;
+    return PersistentHistogramAllocator::MergeResult::kSuccess;
   }
   if (histogram->GetHistogramType() != existing_type) {
-    return false;  // Merge failed due to different histogram types.
+    // Merge failed due to different histogram types.
+    return PersistentHistogramAllocator::MergeResult::kTypeMismatch;
   }
 
   if (existing_type == HistogramType::HISTOGRAM ||
@@ -108,7 +109,8 @@
     DCHECK(histogram_buckets->HasValidChecksum());
 
     if (existing_buckets->checksum() != histogram_buckets->checksum()) {
-      return false;  // Merge failed due to different buckets.
+      // Merge failed due to different buckets.
+      return PersistentHistogramAllocator::MergeResult::kRangesMismatch;
     }
   }
 
@@ -117,7 +119,11 @@
   // It's possible for the buckets to differ but their checksums to match due
   // to a collision, in which case AddSamples() will return false, which we
   // propagate to the caller (indicating histogram mismatch).
-  return existing->AddSamples(*samples);
+  if (existing->AddSamples(*samples)) {
+    return PersistentHistogramAllocator::MergeResult::kSuccess;
+  }
+
+  return PersistentHistogramAllocator::MergeResult::kAddFailed;
 }
 
 }  // namespace
@@ -481,7 +487,8 @@
   }
 }
 
-bool PersistentHistogramAllocator::MergeHistogramDeltaToStatisticsRecorder(
+PersistentHistogramAllocator::MergeResult
+PersistentHistogramAllocator::MergeHistogramDeltaToStatisticsRecorder(
     HistogramBase* histogram) {
   DCHECK(histogram);
 
@@ -490,21 +497,22 @@
   // the StatisticsRecorder, which requires acquiring a lock.
   std::unique_ptr<HistogramSamples> samples = histogram->SnapshotDelta();
   if (samples->IsDefinitelyEmpty()) {
-    return true;
+    return PersistentHistogramAllocator::MergeResult::kSuccess;
   }
 
   HistogramBase* existing = GetOrCreateStatisticsRecorderHistogram(histogram);
   if (!existing) {
     // The above should never fail but if it does, no real harm is done.
     // Some metric data will be lost but that is better than crashing.
-    return false;
+    return PersistentHistogramAllocator::MergeResult::kCouldNotCreate;
   }
 
   return MergeSamplesToExistingHistogram(existing, histogram,
                                          std::move(samples));
 }
 
-bool PersistentHistogramAllocator::MergeHistogramFinalDeltaToStatisticsRecorder(
+PersistentHistogramAllocator::MergeResult
+PersistentHistogramAllocator::MergeHistogramFinalDeltaToStatisticsRecorder(
     const HistogramBase* histogram) {
   DCHECK(histogram);
 
@@ -513,14 +521,14 @@
   // requires acquiring a lock.
   std::unique_ptr<HistogramSamples> samples = histogram->SnapshotFinalDelta();
   if (samples->IsDefinitelyEmpty()) {
-    return true;
+    return PersistentHistogramAllocator::MergeResult::kSuccess;
   }
 
   HistogramBase* existing = GetOrCreateStatisticsRecorderHistogram(histogram);
   if (!existing) {
     // The above should never fail but if it does, no real harm is done.
     // Some metric data will be lost but that is better than crashing.
-    return false;
+    return PersistentHistogramAllocator::MergeResult::kCouldNotCreate;
   }
 
   return MergeSamplesToExistingHistogram(existing, histogram,
diff --git a/base/metrics/persistent_histogram_allocator.h b/base/metrics/persistent_histogram_allocator.h
index 7a11e3a..7530f174 100644
--- a/base/metrics/persistent_histogram_allocator.h
+++ b/base/metrics/persistent_histogram_allocator.h
@@ -266,22 +266,26 @@
   // True, forgetting it otherwise.
   void FinalizeHistogram(Reference ref, bool registered);
 
+  // Results of the merging.
+  enum class MergeResult {
+    kSuccess,
+    kTypeMismatch,
+    kRangesMismatch,
+    kAddFailed,
+    kCouldNotCreate,
+  };
+
   // Merges the data in a persistent histogram with one held globally by the
   // StatisticsRecorder, updating the "logged" samples within the passed
   // object so that repeated merges are allowed. Don't call this on a "global"
   // allocator because histograms created there will already be in the SR.
-  // Returns whether the merge was successful; if false, the histogram did not
-  // have the same shape (different types or buckets), or we couldn't get a
-  // target histogram from the statistic recorder.
-  bool MergeHistogramDeltaToStatisticsRecorder(HistogramBase* histogram);
+  MergeResult MergeHistogramDeltaToStatisticsRecorder(HistogramBase* histogram);
 
   // As above but merge the "final" delta. No update of "logged" samples is
   // done which means it can operate on read-only objects. It's essential,
   // however, not to call this more than once or those final samples will
-  // get recorded again. Returns whether the merge was successful; if false, the
-  // histogram did not have the same shape (different types or buckets), or we
-  // couldn't get a target histogram from the statistic recorder.
-  bool MergeHistogramFinalDeltaToStatisticsRecorder(
+  // get recorded again.
+  MergeResult MergeHistogramFinalDeltaToStatisticsRecorder(
       const HistogramBase* histogram);
 
   // Returns an object that manages persistent-sample-map records for a given
diff --git a/build/OWNERS.setnoparent b/build/OWNERS.setnoparent
index 85cd41dc..e49b3c1 100644
--- a/build/OWNERS.setnoparent
+++ b/build/OWNERS.setnoparent
@@ -100,3 +100,7 @@
 
 # Cronet
 file://components/cronet/CRONET_OWNERS
+
+# Gemini in Chrome API owners are responsible for making sure new usage of
+# sensitive features (e.g. auto submit) are properly reviewed/approved.
+file://chrome/browser/glic/API_OWNERS
diff --git a/build/config/clang/BUILD.gn b/build/config/clang/BUILD.gn
index 5031ea2..e05032c 100644
--- a/build/config/clang/BUILD.gn
+++ b/build/config/clang/BUILD.gn
@@ -89,7 +89,10 @@
     # The plugin is built directly into clang, so there's no need to load it
     # dynamically.
     plugin = "find-bad-constructs"
-    plugin_arguments = [ "check-stack-allocated" ]
+    plugin_arguments = [
+      "check-stack-allocated",
+      "check-std-ranges-pipe-operator",
+    ]
 
     if (is_linux || is_chromeos || is_android || is_fuchsia) {
       plugin_arguments += [ "check-ipc" ]
diff --git a/build/vs_toolchain.py b/build/vs_toolchain.py
index 7b1c798c..b66824d 100755
--- a/build/vs_toolchain.py
+++ b/build/vs_toolchain.py
@@ -17,7 +17,7 @@
 
 from gn_helpers import ToGNString
 
-# VS 2022 17.13.4 with 10.0.26100.4654 SDK with ARM64 libraries and UWP support.
+# VS 2026 17.13.4 with 10.0.26100.7705 SDK with ARM64 libraries and UWP support.
 # See go/win-toolchain-reference for instructions about how to update the
 # toolchain.
 #
@@ -59,7 +59,7 @@
 # * docs/windows_build_instructions.md
 #   Make sure any version numbers in the documentation match the code.
 #
-TOOLCHAIN_HASH = 'e4305f407e'
+TOOLCHAIN_HASH = 'e66617bc68'
 SDK_VERSION = '10.0.26100.0'
 
 # Visual Studio versions are listed in descending order of priority.
@@ -67,7 +67,8 @@
 # which makes a difference for the arm64 runtime.
 # The second number is an alternate version number, only used in an error string
 MSVS_VERSIONS = collections.OrderedDict([
-    ('2022', '17.0'),  # The VS version in our packaged toolchain.
+    ('2026', '18.0'),  # The VS version in our packaged toolchain.
+    ('2022', '17.0'),
     ('2019', '16.0'),
     ('2017', '15.0'),
 ])
@@ -75,6 +76,7 @@
 # List of preferred VC toolset version based on MSVS
 # Order is not relevant for this dictionary.
 MSVC_TOOLSET_VERSION = {
+    '2026': 'VC145',
     '2022': 'VC143',
     '2019': 'VC142',
     '2017': 'VC141',
@@ -201,7 +203,8 @@
     # Checking vs%s_install environment variables.
     # For example, vs2019_install could have the value
     # "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community".
-    # Only vs2017_install, vs2019_install and vs2022_install are supported.
+    # Only vs2017_install, vs2019_install, vs2022_install, and vs2026_install
+    # are supported.
     path = os.environ.get('vs%s_install' % version)
     if path and os.path.exists(path):
       available_versions.append(version)
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index c3bb477..bba0d849 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -1133,6 +1133,7 @@
       "//chrome/browser/facilitated_payments/ui/android/internal:junit",
       "//chrome/browser/feed/android:junit",
       "//chrome/browser/flags:flags_junit_tests",
+      "//chrome/browser/glic/android:junit",
       "//chrome/browser/hub:junit",
       "//chrome/browser/hub/internal:junit",
       "//chrome/browser/incognito:incognito_junit_tests",
diff --git a/chrome/android/chrome_java_sources.gni b/chrome/android/chrome_java_sources.gni
index b724663..7a250a10 100644
--- a/chrome/android/chrome_java_sources.gni
+++ b/chrome/android/chrome_java_sources.gni
@@ -105,6 +105,7 @@
   "java/src/org/chromium/chrome/browser/app/tab_activity_glue/ReparentingTabsTask.java",
   "java/src/org/chromium/chrome/browser/app/tab_activity_glue/ReparentingTask.java",
   "java/src/org/chromium/chrome/browser/app/tab_activity_glue/TabReparentingController.java",
+  "java/src/org/chromium/chrome/browser/app/tabmodel/ActiveTabCache.java",
   "java/src/org/chromium/chrome/browser/app/tabmodel/AllTabObserver.java",
   "java/src/org/chromium/chrome/browser/app/tabmodel/ArchivedTabModelOrchestrator.java",
   "java/src/org/chromium/chrome/browser/app/tabmodel/AsyncTabParamsManagerSingleton.java",
diff --git a/chrome/android/expectations/trichrome_chrome_64_32_bundle__chrome.AndroidManifest.expected b/chrome/android/expectations/trichrome_chrome_64_32_bundle__chrome.AndroidManifest.expected
index d535307..b05559c 100644
--- a/chrome/android/expectations/trichrome_chrome_64_32_bundle__chrome.AndroidManifest.expected
+++ b/chrome/android/expectations/trichrome_chrome_64_32_bundle__chrome.AndroidManifest.expected
@@ -730,6 +730,15 @@
         android:name="com.google.android.gms.cast.framework.media.MediaIntentReceiver"
         android:exported="false">
     </receiver>  # DIFF-ANCHOR: 0a6f8fa5
+    <receiver  # DIFF-ANCHOR: 83f0ffb6
+        android:name="org.chromium.chrome.browser.actor.ActorBroadcastReceiver"
+        android:exported="false">
+      <intent-filter>  # DIFF-ANCHOR: 43c058ef
+        <action android:name="org.chromium.chrome.browser.actor.ACTION_CANCEL"/>
+        <action android:name="org.chromium.chrome.browser.actor.ACTION_PAUSE"/>
+        <action android:name="org.chromium.chrome.browser.actor.ACTION_RESUME"/>
+      </intent-filter>  # DIFF-ANCHOR: 43c058ef
+    </receiver>  # DIFF-ANCHOR: 83f0ffb6
     <receiver  # DIFF-ANCHOR: 68e22783
         android:name="org.chromium.chrome.browser.announcement.AnnouncementNotificationManager$Receiver"
         android:exported="false">
diff --git a/chrome/android/features/tab_ui/java/res/layout/tab_grid_card_item_layout.xml b/chrome/android/features/tab_ui/java/res/layout/tab_grid_card_item_layout.xml
index ce76f7a..eeb8aa6 100644
--- a/chrome/android/features/tab_ui/java/res/layout/tab_grid_card_item_layout.xml
+++ b/chrome/android/features/tab_ui/java/res/layout/tab_grid_card_item_layout.xml
@@ -119,6 +119,17 @@
           app:cornerRadiusBottomStart="@dimen/tab_grid_card_thumbnail_corner_radius_bottom"
           app:cornerRadiusBottomEnd="@dimen/tab_grid_card_thumbnail_corner_radius_bottom"/>
 
+        <ProgressBar
+          android:id="@+id/fetch_thumbnail_spinner"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:indeterminate="true"
+          android:visibility="gone"
+          app:layout_constraintTop_toTopOf="@id/tab_thumbnail"
+          app:layout_constraintBottom_toBottomOf="@id/tab_thumbnail"
+          app:layout_constraintStart_toStartOf="@id/tab_thumbnail"
+          app:layout_constraintEnd_toEndOf="@id/tab_thumbnail" />
+
         <!-- Legacy layout for price cards. To be removed in favor of tab_card_label. -->
         <org.chromium.chrome.browser.tasks.tab_management.PriceCardView
           android:id="@+id/price_info_box_outer"
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/ArchivedTabsDialogCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/ArchivedTabsDialogCoordinator.java
index 645f56d..8ba59a16 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/ArchivedTabsDialogCoordinator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/ArchivedTabsDialogCoordinator.java
@@ -814,7 +814,7 @@
                         mUndoBarController,
                         COMPONENT_NAME,
                         TabListEditorCoordinator.UNLIMITED_SELECTION,
-                        false);
+                        /* isSingleContextMode= */ false);
     }
 
     @VisibleForTesting
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java
index e6a8ec1..b45933e 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java
@@ -271,7 +271,7 @@
                             /* undoBarExplicitTrigger= */ null,
                             mSnackbarManager,
                             TabListEditorCoordinator.UNLIMITED_SELECTION,
-                            false);
+                            /* isSingleContextMode= */ false);
             mTabListCoordinator.setOnLongPressTabItemEventListener(mMediator);
             mTabListCoordinator.registerItemType(
                     UiType.COLLABORATION_ACTIVITY_MESSAGE,
@@ -386,7 +386,7 @@
                             /* undoBarExplicitTrigger= */ null,
                             /* componentName= */ null,
                             TabListEditorCoordinator.UNLIMITED_SELECTION,
-                            false);
+                            /* isSingleContextMode= */ false);
         }
 
         return mTabListEditorCoordinator.getController();
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridView.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridView.java
index 9ea3229b..3baeb19f 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridView.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridView.java
@@ -33,8 +33,10 @@
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.build.annotations.Nullable;
 import org.chromium.chrome.browser.quick_delete.QuickDeleteAnimationGradientDrawable;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.tab.TabUtils;
+import org.chromium.chrome.browser.tab_ui.TabThumbnailView;
+import org.chromium.chrome.browser.tab_ui.TabThumbnailView.ThumbnailViewState;
 import org.chromium.chrome.browser.tasks.tab_management.TabActionButtonData.TabActionButtonType;
 import org.chromium.chrome.browser.tasks.tab_management.TabListModel.AnimationStatus;
 import org.chromium.chrome.browser.tasks.tab_management.TabProperties.TabActionState;
@@ -137,6 +139,17 @@
         scaleAnimator.start();
     }
 
+    void setThumbnailSpinnerVisibility(boolean isVisible) {
+        View spinner = findViewById(R.id.fetch_thumbnail_spinner);
+        if (spinner == null) return;
+
+        spinner.setVisibility(isVisible ? View.VISIBLE : View.GONE);
+        TabThumbnailView thumbnail = findViewById(R.id.tab_thumbnail);
+        if (thumbnail != null && isVisible) {
+            thumbnail.setThumbnailViewState(ThumbnailViewState.LOADING);
+        }
+    }
+
     void hideTabGridCardViewForQuickDelete(
             @QuickDeleteAnimationStatus int status, boolean isIncognito) {
         assert mTabActionState != TabActionState.UNSET;
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinder.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinder.java
index 180c2cc..92f8e79 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinder.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinder.java
@@ -30,13 +30,14 @@
 import org.chromium.base.ResettersForTesting;
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.build.annotations.Nullable;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.tab.TabUtils;
 import org.chromium.chrome.browser.tab.state.ShoppingPersistedTabData.PriceDrop;
 import org.chromium.chrome.browser.tab_ui.TabCardThemeUtil;
 import org.chromium.chrome.browser.tab_ui.TabListFaviconProvider.TabFavicon;
 import org.chromium.chrome.browser.tab_ui.TabListFaviconProvider.TabFaviconFetcher;
 import org.chromium.chrome.browser.tab_ui.TabThumbnailView;
+import org.chromium.chrome.browser.tab_ui.TabThumbnailView.ThumbnailViewState;
 import org.chromium.chrome.browser.tasks.tab_management.TabActionButtonData.TabActionButtonType;
 import org.chromium.chrome.browser.tasks.tab_management.TabListMediator.ShoppingPersistedTabDataFetcher;
 import org.chromium.chrome.browser.tasks.tab_management.TabListModel.CardProperties;
@@ -233,6 +234,9 @@
             @TabActionButtonType
             int actionButtonType = data != null ? data.type : TabActionButtonType.OVERFLOW;
             ((TabGridView) view).setTabActionButtonDrawable(actionButtonType);
+        } else if (TabProperties.SHOW_THUMBNAIL_SPINNER == propertyKey) {
+            ((TabGridView) view)
+                    .setThumbnailSpinnerVisibility(model.get(TabProperties.SHOW_THUMBNAIL_SPINNER));
         } else if (TabProperties.TAB_CLICK_LISTENER == propertyKey) {
             setNullableClickListener(model.get(TabProperties.TAB_CLICK_LISTENER), view, model);
         } else if (TabProperties.TAB_LONG_CLICK_LISTENER == propertyKey) {
@@ -509,9 +513,12 @@
         // the callback matches the current thumbnail fetcher and grid card size.
         Callback<@Nullable Drawable> callback =
                 result -> {
+                    ((TabGridView) view).setThumbnailSpinnerVisibility(false);
                     if (result != null) {
+                        thumbnail.setThumbnailViewState(ThumbnailViewState.THUMBNAIL_LOADED);
                         TabUtils.setDrawableAndUpdateImageMatrix(thumbnail, result, thumbnailSize);
                     } else {
+                        thumbnail.setThumbnailViewState(ThumbnailViewState.PLACEHOLDER_LOADED);
                         thumbnail.setImageDrawable(null);
                     }
                 };
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java
index b6b56f4..c39c7a9 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java
@@ -1198,4 +1198,15 @@
     public TabListModel getTabListModel() {
         return mModelList;
     }
+
+    /**
+     * Sets the visibility of the thumbnail spinner for a specific tab.
+     *
+     * @param tab The tab to update.
+     * @param isVisible Whether the spinner should be visible.
+     */
+    void setThumbnailSpinnerVisibility(Tab tab, boolean isVisible) {
+        assert mMediator != null;
+        mMediator.setThumbnailSpinnerVisibility(tab, isVisible);
+    }
 }
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorCoordinator.java
index 7cd5fa8a..2e47ec6 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorCoordinator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorCoordinator.java
@@ -185,6 +185,21 @@
          *     tabId for tabs or syncId for groups.
          */
         void selectTabs(Set<TabListEditorItemSelectionId> itemIds);
+
+        /**
+         * Sets the visibility of the thumbnail spinner for a specific tab.
+         *
+         * @param tab The tab to update.
+         * @param isVisible Whether the spinner should be visible.
+         */
+        void setThumbnailSpinnerVisibility(Tab tab, boolean isVisible);
+
+        /**
+         * Requests a thumbnail update for a specific tab.
+         *
+         * @param tab The tab to update.
+         */
+        void updateThumbnail(Tab tab);
     }
 
     /** An interface for embedders to provide navigation. */
@@ -304,6 +319,16 @@
                 public void selectTabs(Set<TabListEditorItemSelectionId> itemIds) {
                     mTabListEditorMediator.selectTabs(itemIds);
                 }
+
+                @Override
+                public void updateThumbnail(Tab tab) {
+                    mTabContentManager.cacheTabThumbnail(tab);
+                }
+
+                @Override
+                public void setThumbnailSpinnerVisibility(Tab tab, boolean isVisible) {
+                    mTabListEditorMediator.setThumbnailSpinnerVisibility(tab, isVisible);
+                }
             };
 
     private final Activity mActivity;
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorMediator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorMediator.java
index ea07fd8..5914820 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorMediator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorMediator.java
@@ -528,6 +528,16 @@
                 /* quickMode= */ true);
     }
 
+    @Override
+    public void updateThumbnail(Tab tab) {
+        // No-op.
+    }
+
+    @Override
+    public void setThumbnailSpinnerVisibility(Tab tab, boolean isVisible) {
+        mTabListCoordinator.setThumbnailSpinnerVisibility(tab, isVisible);
+    }
+
     /** Destroy any members that needs clean up. */
     public void destroy() {
         runListDestroyables();
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java
index 30d29ee..63878fa2 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java
@@ -69,8 +69,8 @@
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.quick_delete.QuickDeleteAnimationGradientDrawable;
 import org.chromium.chrome.browser.tab.EmptyTabObserver;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.tab.Tab;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
 import org.chromium.chrome.browser.tab.TabCreationState;
 import org.chromium.chrome.browser.tab.TabId;
 import org.chromium.chrome.browser.tab.TabLaunchType;
@@ -2218,7 +2218,6 @@
                         .with(TabProperties.MEDIA_INDICATOR, getTabGridMediaIndicator(tab))
                         .with(TabProperties.IS_PINNED, tab.getIsPinned())
                         .build();
-
         if (!mActionsOnAllRelatedTabs || isInTabGroup) {
             tabInfo.set(
                     TabProperties.FAVICON_FETCHER,
@@ -3291,6 +3290,17 @@
         mModelList.update(index, mModelList.get(index));
     }
 
+    void setThumbnailSpinnerVisibility(Tab tab, boolean isVisible) {
+        assert !mActionsOnAllRelatedTabs && !isTabInTabGroup(tab);
+        int index = mModelList.indexFromTabId(tab.getId());
+        if (index == TabModel.INVALID_TAB_INDEX) return;
+
+        PropertyModel model = mModelList.get(index).model;
+        if (model == null) return;
+
+        model.set(TabProperties.SHOW_THUMBNAIL_SPINNER, isVisible);
+    }
+
     private void updateThumbnailFetcher(PropertyModel model, int tabId) {
         if (mThumbnailProvider == null) return;
 
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabProperties.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabProperties.java
index 5e63800..74bbc4d 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabProperties.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabProperties.java
@@ -137,6 +137,9 @@
     public static final WritableObjectPropertyKey<ThumbnailFetcher> THUMBNAIL_FETCHER =
             new WritableObjectPropertyKey<>(true);
 
+    public static final WritableBooleanPropertyKey SHOW_THUMBNAIL_SPINNER =
+            new WritableBooleanPropertyKey();
+
     public static final WritableObjectPropertyKey<Size> GRID_CARD_SIZE =
             new WritableObjectPropertyKey<>();
 
@@ -215,6 +218,7 @@
                 FAVICON_FETCHER,
                 GRID_CARD_SIZE,
                 THUMBNAIL_FETCHER,
+                SHOW_THUMBNAIL_SPINNER,
                 TITLE,
                 CARD_ALPHA,
                 CARD_ANIMATION_STATUS,
diff --git a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/ClosableTabListEditorTest.java b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/ClosableTabListEditorTest.java
index 7870af4..8b7fe2b 100644
--- a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/ClosableTabListEditorTest.java
+++ b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/ClosableTabListEditorTest.java
@@ -111,7 +111,7 @@
                                     /* undoBarExplicitTrigger= */ null,
                                     /* componentName= */ null,
                                     TabListEditorCoordinator.UNLIMITED_SELECTION,
-                                    false);
+                                    /* isSingleContextMode= */ false);
 
                     mTabListEditorController = mTabListEditorCoordinator.getController();
                     mTabListEditorLayout =
diff --git a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/SelectableTabListEditorTest.java b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/SelectableTabListEditorTest.java
index a8d6902..7325bce 100644
--- a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/SelectableTabListEditorTest.java
+++ b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/SelectableTabListEditorTest.java
@@ -221,7 +221,7 @@
                                     /* undoBarExplicitTrigger= */ null,
                                     /* componentName= */ null,
                                     TabListEditorCoordinator.UNLIMITED_SELECTION,
-                                    false);
+                                    /* isSingleContextMode= */ false);
 
                     mTabListEditorController = mTabListEditorCoordinator.getController();
                     mTabListEditorLayout =
diff --git a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinderUnitTest.java b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinderUnitTest.java
index 583cb019..12d10871 100644
--- a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinderUnitTest.java
+++ b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinderUnitTest.java
@@ -59,7 +59,7 @@
 import org.chromium.base.test.util.Features.DisableFeatures;
 import org.chromium.base.test.util.Features.EnableFeatures;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.tab.state.ShoppingPersistedTabData;
 import org.chromium.chrome.browser.tab.state.ShoppingPersistedTabData.PriceDrop;
 import org.chromium.chrome.browser.tab_ui.TabListFaviconProvider.TabFavicon;
@@ -98,6 +98,7 @@
     @Mock private ShoppingPersistedTabDataFetcher mShoppingPersistedTabDataFetcher;
     @Mock private ShoppingPersistedTabData mShoppingPersistedTabData;
     @Mock private TextView mTabTitleView;
+    @Mock private View mSpinner;
 
     @Captor private ArgumentCaptor<Callback<Drawable>> mCallbackCaptor;
 
@@ -131,6 +132,7 @@
         when(mViewGroup.fastFindViewById(R.id.price_info_box_outer)).thenReturn(mPriceCardView);
         when(mViewGroup.fastFindViewById(R.id.tab_card_label_stub)).thenReturn(mTabCardLabelStub);
         when(mViewGroup.fastFindViewById(R.id.action_button)).thenReturn(mActionButton);
+        when(mViewGroup.fastFindViewById(R.id.fetch_thumbnail_spinner)).thenReturn(mSpinner);
         doAnswer(
                         (ignored) -> {
                             when(mViewGroup.fastFindViewById(R.id.tab_card_label_stub))
@@ -209,6 +211,8 @@
         verify(mThumbnailView).setImageDrawable(mBitmapDrawable);
         ArgumentCaptor<Matrix> matrixCaptor = ArgumentCaptor.forClass(Matrix.class);
         verify(mThumbnailView).setImageMatrix(matrixCaptor.capture());
+        verify(mThumbnailView)
+                .setThumbnailViewState(TabThumbnailView.ThumbnailViewState.THUMBNAIL_LOADED);
         verifyNoMoreInteractions(mThumbnailView);
 
         // Verify metrics scale + translate.
@@ -285,6 +289,8 @@
         verify(mThumbnailView).setImageDrawable(mBitmapDrawable);
         ArgumentCaptor<Matrix> matrixCaptor = ArgumentCaptor.forClass(Matrix.class);
         verify(mThumbnailView).setImageMatrix(matrixCaptor.capture());
+        verify(mThumbnailView)
+                .setThumbnailViewState(TabThumbnailView.ThumbnailViewState.THUMBNAIL_LOADED);
         verifyNoMoreInteractions(mThumbnailView);
 
         // Verify metrics scale + translate.
@@ -320,6 +326,8 @@
         verify(mThumbnailView).setImageDrawable(mBitmapDrawable);
         ArgumentCaptor<Matrix> matrixCaptor = ArgumentCaptor.forClass(Matrix.class);
         verify(mThumbnailView).setImageMatrix(matrixCaptor.capture());
+        verify(mThumbnailView)
+                .setThumbnailViewState(TabThumbnailView.ThumbnailViewState.THUMBNAIL_LOADED);
         verifyNoMoreInteractions(mThumbnailView);
 
         // Verify metrics scale + translate.
@@ -354,6 +362,8 @@
         verify(mThumbnailView).setImageDrawable(mBitmapDrawable);
         ArgumentCaptor<Matrix> matrixCaptor = ArgumentCaptor.forClass(Matrix.class);
         verify(mThumbnailView).setImageMatrix(matrixCaptor.capture());
+        verify(mThumbnailView)
+                .setThumbnailViewState(TabThumbnailView.ThumbnailViewState.THUMBNAIL_LOADED);
         verifyNoMoreInteractions(mThumbnailView);
 
         // Verify metrics scale + translate.
diff --git a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediatorUnitTest.java b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediatorUnitTest.java
index 23fd377..8f9c07ad 100644
--- a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediatorUnitTest.java
+++ b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediatorUnitTest.java
@@ -123,9 +123,9 @@
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
 import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.tab.MockTab;
 import org.chromium.chrome.browser.tab.Tab;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
 import org.chromium.chrome.browser.tab.TabCreationState;
 import org.chromium.chrome.browser.tab.TabLaunchType;
 import org.chromium.chrome.browser.tab.TabObserver;
@@ -5975,4 +5975,26 @@
         when(tab.getMediaState()).thenReturn(mediaState);
         mTabObserverCaptor.getValue().onMediaStateChanged(tab, mediaState);
     }
+
+    @Test
+    public void testSetThumbnailSpinnerVisibility() {
+        setUpTabListMediator(TabListMediatorType.TAB_GRID_DIALOG, TabListMode.GRID);
+        initAndAssertAllProperties();
+
+        PropertyModel model = mModelList.get(0).model;
+        org.chromium.ui.modelutil.PropertyObservable.PropertyObserver<
+                        org.chromium.ui.modelutil.PropertyKey>
+                observer =
+                        mock(org.chromium.ui.modelutil.PropertyObservable.PropertyObserver.class);
+        model.addObserver(observer);
+
+        mMediator.setThumbnailSpinnerVisibility(mTab1, true);
+        verify(observer).onPropertyChanged(eq(model), eq(TabProperties.SHOW_THUMBNAIL_SPINNER));
+        assertTrue(model.get(TabProperties.SHOW_THUMBNAIL_SPINNER));
+
+        mMediator.setThumbnailSpinnerVisibility(mTab1, false);
+        verify(observer, times(2))
+                .onPropertyChanged(eq(model), eq(TabProperties.SHOW_THUMBNAIL_SPINNER));
+        assertFalse(model.get(TabProperties.SHOW_THUMBNAIL_SPINNER));
+    }
 }
diff --git a/chrome/android/java/AndroidManifest.xml b/chrome/android/java/AndroidManifest.xml
index 0272fbfb..f2e1ee7 100644
--- a/chrome/android/java/AndroidManifest.xml
+++ b/chrome/android/java/AndroidManifest.xml
@@ -528,6 +528,16 @@
             </intent-filter>
         </receiver>
 
+        <!-- Actor related -->
+        <receiver android:name="org.chromium.chrome.browser.actor.ActorBroadcastReceiver"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="org.chromium.chrome.browser.actor.ACTION_PAUSE" />
+                <action android:name="org.chromium.chrome.browser.actor.ACTION_RESUME" />
+                <action android:name="org.chromium.chrome.browser.actor.ACTION_CANCEL" />
+            </intent-filter>
+        </receiver>
+
         <!-- Custom Tabs -->
         <activity android:name="org.chromium.chrome.browser.customtabs.CustomTabActivity"
             android:theme="@style/Theme.Chromium.Activity"
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java
index bb578791..8a64466 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java
@@ -1371,8 +1371,11 @@
         }
     }
 
-    protected FullscreenVideoPictureInPictureController
+    protected @Nullable FullscreenVideoPictureInPictureController
             ensureFullscreenVideoPictureInPictureController() {
+        if (!ChromeFeatureList.isEnabled(ChromeFeatureList.FULLSCREEN_VIDEO_PICTURE_IN_PICTURE)) {
+            return null;
+        }
         if (mFullscreenVideoPictureInPictureController == null) {
             mFullscreenVideoPictureInPictureController =
                     new FullscreenVideoPictureInPictureController(
@@ -1408,8 +1411,11 @@
             return;
         }
 
-        ensureFullscreenVideoPictureInPictureController();
-        mFullscreenVideoPictureInPictureController.attemptPictureInPicture();
+        FullscreenVideoPictureInPictureController controller =
+                ensureFullscreenVideoPictureInPictureController();
+        if (controller != null) {
+            controller.attemptPictureInPicture();
+        }
         // The attempt might not be successful.  If it is, then `onPictureInPictureModeChanged` will
         // let us know later.  Note that the activity might report that it is in PictureInPicture
         // mode at any point after this, which might be before we finish setup after receiving
@@ -1421,7 +1427,11 @@
         super.onPictureInPictureUiStateChanged(pipState);
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return;
         if (isActivityFinishingOrDestroyed()) return;
-        ensureFullscreenVideoPictureInPictureController().onStashReported(pipState.isStashed());
+        FullscreenVideoPictureInPictureController controller =
+                ensureFullscreenVideoPictureInPictureController();
+        if (controller != null) {
+            controller.onStashReported(pipState.isStashed());
+        }
     }
 
     /**
@@ -1437,12 +1447,15 @@
                 " custom tabs: " + wasInPictureInPictureForMinimizedCustomTabs());
         if (wasInPictureInPictureForMinimizedCustomTabs()) return;
         if (inPicture) {
-            maybeCreateActorPipController();
-            if (mActorPipController == null || !mActorPipController.shouldEnterPip()) {
-                ensureFullscreenVideoPictureInPictureController();
-                mFullscreenVideoPictureInPictureController.onEnteredPictureInPictureMode();
-            }
             mLastPictureInPictureModeForTesting = true;
+            maybeCreateActorPipController();
+            if (mActorPipController != null && mActorPipController.shouldEnterPip()) return;
+
+            FullscreenVideoPictureInPictureController controller =
+                    ensureFullscreenVideoPictureInPictureController();
+            if (controller != null) {
+                controller.onEnteredPictureInPictureMode();
+            }
         } else {
             if (mActorPipController != null) {
                 mActorPipController.onFrameworkExitedPictureInPicture();
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/ActiveTabCache.java b/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/ActiveTabCache.java
new file mode 100644
index 0000000..69b5f71
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/ActiveTabCache.java
@@ -0,0 +1,194 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.app.tabmodel;
+
+import static org.chromium.base.ThreadUtils.assertOnUiThread;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import org.chromium.base.ContextUtils;
+import org.chromium.base.Log;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.chrome.browser.crypto.CipherFactory;
+import org.chromium.chrome.browser.tab.Tab;
+import org.chromium.chrome.browser.tab.TabState;
+import org.chromium.chrome.browser.tab.TabStateExtractor;
+import org.chromium.chrome.browser.tabpersistence.TabStateFileManager;
+
+import java.io.File;
+
+/**
+ * Responsible for caching the active tab's state and index. This allows for loading the active
+ * tab's state pre-native, which the database is unable to do.
+ */
+@NullMarked
+public class ActiveTabCache {
+    private static final String TAG = "active_tab_cache";
+
+    /** The name of the base directory where the state is saved. */
+    private static final String CACHE_DIR_NAME = "active_tabs";
+
+    private static final String REGULAR_SUFFIX = "_regular";
+    private static final String INCOGNITO_SUFFIX = "_incognito";
+    private static @Nullable File sActiveTabDirectory;
+
+    /** Data class containing information about a cached active tab. */
+    public static class CachedActiveTab {
+        public final int tabIndex;
+        public final TabState tabState;
+
+        public CachedActiveTab(int tabIndex, TabState tabState) {
+            this.tabIndex = tabIndex;
+            this.tabState = tabState;
+        }
+    }
+
+    private final String mRegularTabFileName;
+    private final String mIncognitoTabFileName;
+
+    /**
+     * @param windowTag The tag for the window being tracked.
+     */
+    public ActiveTabCache(String windowTag) {
+        mRegularTabFileName = getFileName(windowTag, /* incognito= */ false);
+        mIncognitoTabFileName = getFileName(windowTag, /* incognito= */ true);
+    }
+
+    /**
+     * Saves the active tab's state and index to the cache.
+     *
+     * <p>Note that there is one file per window/otr-status combination, so we atomically "swap" the
+     * active tab each time it is updated.
+     *
+     * @param tab The active tab.
+     * @param tabIndex The index of the active tab.
+     * @param cipherFactory The cipher factory for encrypting incognito tab state.
+     */
+    public void saveActiveTab(Tab tab, int tabIndex, CipherFactory cipherFactory) {
+        assertOnUiThread();
+
+        boolean isOffTheRecord = tab.isOffTheRecord();
+        String fileName = isOffTheRecord ? mIncognitoTabFileName : mRegularTabFileName;
+        File file = new File(getOrCreateCacheDirectory(), fileName);
+        TabState tabState = TabStateExtractor.from(tab);
+        if (tabState == null) return;
+
+        TabStateFileManager.saveStateInternal(file, tabState, isOffTheRecord, cipherFactory);
+
+        getSharedPreferences().edit().putInt(fileName, tabIndex).apply();
+    }
+
+    /**
+     * Restores the active tab from the cache. If it doesn't exist or failed to restore, return
+     * null.
+     *
+     * @param isOffTheRecord Whether to restore the incognito active tab.
+     * @param cipherFactory The cipher factory for decrypting incognito tab state.
+     */
+    public @Nullable CachedActiveTab restoreActiveTab(
+            boolean isOffTheRecord, CipherFactory cipherFactory) {
+        assertOnUiThread();
+
+        String fileName = isOffTheRecord ? mIncognitoTabFileName : mRegularTabFileName;
+        File file = new File(getOrCreateCacheDirectory(), fileName);
+        if (!file.exists()) return null;
+
+        TabState tabState =
+                TabStateFileManager.restoreTabStateInternal(file, isOffTheRecord, cipherFactory);
+        if (tabState == null) return null;
+
+        int tabIndex = getSharedPreferences().getInt(fileName, -1);
+        if (tabIndex == -1) return null;
+
+        return new CachedActiveTab(tabIndex, tabState);
+    }
+
+    /**
+     * Clears the active tab cache for the given incognito state.
+     *
+     * @param incognito Whether to clear the incognito or regular active tab.
+     */
+    public void clearActiveTab(boolean incognito) {
+        String fileName = incognito ? mIncognitoTabFileName : mRegularTabFileName;
+        deleteFileAndPref(fileName);
+    }
+
+    /** Clears all active tab cache for the current window. */
+    public void clearCurrentWindow() {
+        clearActiveTab(false);
+        clearActiveTab(true);
+    }
+
+    /**
+     * Cleans up the active tab cache for the given window tag.
+     *
+     * @param windowTag The window tag to clean up.
+     */
+    public static void cleanupWindow(String windowTag) {
+        String regularFileName = getFileName(windowTag, false);
+        String incognitoFileName = getFileName(windowTag, true);
+
+        deleteFileAndPref(regularFileName);
+        deleteFileAndPref(incognitoFileName);
+    }
+
+    /** Clears all active tab cache global state. */
+    public static void clearGlobalState() {
+        assertOnUiThread();
+
+        File directory = getCacheDirectory();
+        if (directory.exists()) {
+            File[] files = directory.listFiles();
+            if (files != null) {
+                for (File f : files) {
+                    if (!f.delete()) {
+                        Log.e(TAG, "Failed to delete file: " + f);
+                    }
+                }
+            }
+            if (!directory.delete()) {
+                Log.e(TAG, "Failed to delete directory: " + directory);
+            }
+        }
+        sActiveTabDirectory = null;
+        getSharedPreferences().edit().clear().apply();
+    }
+
+    private static File getOrCreateCacheDirectory() {
+        assertOnUiThread();
+        if (sActiveTabDirectory == null) {
+            sActiveTabDirectory = getCacheDirectory();
+            if (!sActiveTabDirectory.exists() && !sActiveTabDirectory.mkdirs()) {
+                Log.e(TAG, "Failed to create active tab cache directory: " + sActiveTabDirectory);
+            }
+        }
+        return sActiveTabDirectory;
+    }
+
+    private static SharedPreferences getSharedPreferences() {
+        return ContextUtils.getApplicationContext()
+                .getSharedPreferences(CACHE_DIR_NAME, Context.MODE_PRIVATE);
+    }
+
+    private static File getCacheDirectory() {
+        return ContextUtils.getApplicationContext().getDir(CACHE_DIR_NAME, Context.MODE_PRIVATE);
+    }
+
+    private static String getFileName(String windowTag, boolean incognito) {
+        String suffix = incognito ? INCOGNITO_SUFFIX : REGULAR_SUFFIX;
+        return windowTag + suffix;
+    }
+
+    private static void deleteFileAndPref(String fileName) {
+        assertOnUiThread();
+        File file = new File(getCacheDirectory(), fileName);
+        if (file.exists() && !file.delete()) {
+            Log.e(TAG, "Failed to delete cache file: " + file);
+        }
+        getSharedPreferences().edit().remove(fileName).apply();
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/chrome_item_picker/TabItemPickerCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/chrome_item_picker/TabItemPickerCoordinator.java
index fe76996..0aea17a 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/chrome_item_picker/TabItemPickerCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/chrome_item_picker/TabItemPickerCoordinator.java
@@ -34,8 +34,10 @@
 import org.chromium.chrome.browser.page_content_annotations.PageContentExtractionService;
 import org.chromium.chrome.browser.page_content_annotations.PageContentExtractionServiceFactory;
 import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.tab.EmptyTabObserver;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.TabLoadIfNeededCaller;
+import org.chromium.chrome.browser.tab.TabObserver;
 import org.chromium.chrome.browser.tab_ui.RecyclerViewPosition;
 import org.chromium.chrome.browser.tab_ui.TabContentManager;
 import org.chromium.chrome.browser.tabmodel.IncognitoTabModel;
@@ -58,6 +60,7 @@
 import org.chromium.components.browser_ui.modaldialog.AppModalPresenter;
 import org.chromium.ui.modaldialog.ModalDialogManager;
 import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogType;
+import org.chromium.url.GURL;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -362,6 +365,29 @@
         private final TabModelSelector mTabModelSelector;
         private final Set<Integer> mCachedTabIds;
         private final Set<TabListEditorItemSelectionId> mInitialSelectedTabIds;
+        private final Set<Tab> mTabsBeingLoaded = new HashSet<>();
+        private final TabObserver mLoadObserver =
+                new EmptyTabObserver() {
+                    @Override
+                    public void onPageLoadFinished(Tab tab, GURL url) {
+                        onTabLoadFinished(tab);
+                    }
+
+                    @Override
+                    public void onPageLoadFailed(Tab tab, int errorCode) {
+                        onTabLoadFinished(tab);
+                    }
+
+                    @Override
+                    public void onCrash(Tab tab) {
+                        onTabLoadFinished(tab);
+                    }
+
+                    @Override
+                    public void onDestroyed(Tab tab) {
+                        onTabLoadFinished(tab);
+                    }
+                };
 
         public ItemPickerNavigationProvider(
                 Activity activity,
@@ -381,36 +407,74 @@
             boolean hasSelectionChanged = !Objects.equals(mInitialSelectedTabIds, selectedItems);
             mEnableDoneButtonSupplier.set(hasSelectionChanged);
 
-            if (ChromeFeatureList.sOnDemandBackgroundTabContextCapture.isEnabled()) {
-                // The maximum number of tabs that can be selected is determined by
-                // mAllowedSelectionCount, which should always be sufficiently small that there is
-                // no point caching which tabs have already been loaded. It is also safer to update
-                // each time as the OS may kill background tabs at any time.
-                for (TabListEditorItemSelectionId item : selectedItems) {
-                    assert item.isTabId();
-                    int tabId = item.getTabId();
+            if (!ChromeFeatureList.sOnDemandBackgroundTabContextCapture.isEnabled()) return;
 
-                    if (mCachedTabIds.contains(tabId)) continue;
-
-                    Tab tab = mTabModelSelector.getTabById(tabId);
-                    if (tab == null
-                            || !FuseboxTabUtils.isTabEligibleForAttachment(tab)
-                            || FuseboxTabUtils.isTabActive(tab)) {
-                        continue;
-                    }
-
-                    // If everything is working as expected the current tab should always be active
-                    // and therefore not loaded on demand, but just in case we still allow it to be
-                    // loaded here.
-                    tab.loadIfNeeded(TabLoadIfNeededCaller.FUSEBOX_ATTACHMENT);
-
-                    // TODO(crbug.com/486943788): On load complete, capture a new thumbnail and try
-                    // to extract the context. The context extraction might be left to
-                    // FuseboxAttachment, this is still under investigation.
-                }
+            // The maximum number of tabs that can be selected is determined by
+            // mAllowedSelectionCount, which should always be sufficiently small that there is
+            // no point caching which tabs have already been loaded. It is also safer to update
+            // each time as the OS may kill background tabs at any time.
+            for (TabListEditorItemSelectionId item : selectedItems) {
+                assert item.isTabId();
+                maybeTriggerTabReloadAndThumbnailFetch(item.getTabId());
             }
         }
 
+        /**
+         * Triggers a tab reload and thumbnail fetch if the tab is not cached and is eligible.
+         *
+         * @param tabId The ID of the tab to potentially reload.
+         */
+        private void maybeTriggerTabReloadAndThumbnailFetch(int tabId) {
+            // If the tab is already cached, we don't need to do anything.
+            if (mCachedTabIds.contains(tabId)) return;
+
+            Tab tab = mTabModelSelector.getTabById(tabId);
+            if (tab == null
+                    || !FuseboxTabUtils.isTabEligibleForAttachment(tab)
+                    || FuseboxTabUtils.isTabActive(tab)) {
+                return;
+            }
+
+            // If everything is working as expected the current tab should always be active
+            // and therefore not loaded on demand, but just in case we still allow it to be
+            // loaded here.
+            if (!tab.loadIfNeeded(TabLoadIfNeededCaller.FUSEBOX_ATTACHMENT)) return;
+
+            // If the tab finished loading immediately (e.g. it was already in memory),
+            // exit immediately.
+            if (!tab.isLoading()) return;
+
+            // Avoid double-observing the same tab if it's already being loaded.
+            if (mTabsBeingLoaded.contains(tab)) return;
+
+            mTabsBeingLoaded.add(tab);
+            tab.addObserver(mLoadObserver);
+
+            // Show a spinner while the thumbnail is being fetched/generated.
+            var controller = mControllerSupplier.get();
+            if (controller != null) {
+                controller.setThumbnailSpinnerVisibility(tab, /* isVisible= */ true);
+            }
+        }
+
+        private void onTabLoadFinished(Tab tab) {
+            tab.removeObserver(mLoadObserver);
+            if (!mTabsBeingLoaded.remove(tab)) return;
+
+            var controller = mControllerSupplier.get();
+            if (controller != null) {
+                controller.updateThumbnail(tab);
+            }
+        }
+
+        /** Cleans up observers and state. */
+        public void destroy() {
+            for (Tab tab : mTabsBeingLoaded) {
+                if (tab != null) tab.removeObserver(mLoadObserver);
+            }
+            mTabsBeingLoaded.clear();
+        }
+
         @Override
         public NonNullObservableSupplier<Boolean> getEnableDoneButtonSupplier() {
             return mEnableDoneButtonSupplier;
@@ -478,16 +542,22 @@
     /** Creates a TabContentManager instance required by the TabListEditorCoordinator. */
     private TabContentManager createTabContentManager(
             TabModelSelector selector, BrowserControlsStateProvider browserControlsStateProvider) {
-        return new TabContentManager(
-                mActivity,
-                browserControlsStateProvider,
-                /* snapshotsEnabled= */ true,
-                selector::getTabById,
-                TabWindowManagerSingleton.getInstance());
+        TabContentManager tabContentManager =
+                new TabContentManager(
+                        mActivity,
+                        browserControlsStateProvider,
+                        /* snapshotsEnabled= */ true,
+                        selector::getTabById,
+                        TabWindowManagerSingleton.getInstance());
+        tabContentManager.initWithNative();
+        return tabContentManager;
     }
 
     /** Cleans up the TabListEditorCoordinator and releases resources. */
     public void destroy() {
+        if (mNavigationProvider != null) {
+            mNavigationProvider.destroy();
+        }
         if (mTabListEditorCoordinator != null) {
             mTabListEditorCoordinator
                     .getController()
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/chrome_item_picker/TabItemPickerCoordinatorNavigationUnitTest.java b/chrome/android/java/src/org/chromium/chrome/browser/chrome_item_picker/TabItemPickerCoordinatorNavigationUnitTest.java
index 3dbf41f..bcc3b1f 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/chrome_item_picker/TabItemPickerCoordinatorNavigationUnitTest.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/chrome_item_picker/TabItemPickerCoordinatorNavigationUnitTest.java
@@ -11,6 +11,7 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -22,6 +23,8 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.Mockito;
@@ -41,6 +44,7 @@
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.TabLoadIfNeededCaller;
+import org.chromium.chrome.browser.tab.TabObserver;
 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
 import org.chromium.chrome.browser.tabmodel.TabModelSelectorImpl;
 import org.chromium.chrome.browser.tasks.tab_management.TabListEditorCoordinator;
@@ -67,6 +71,7 @@
     @Mock private ChromeItemPickerActivity mActivity;
     @Mock private TabListEditorCoordinator mTabListEditorCoordinator;
     @Mock private TabListEditorController mTabListEditorController;
+    @Captor private ArgumentCaptor<TabObserver> mTabObserverCaptor;
 
     private final Set<TabListEditorItemSelectionId> mInitialSelectedTabIds = new HashSet<>();
 
@@ -229,6 +234,8 @@
         int tabId = 101;
         Tab tab = mockTabActiveState(tabId, false);
         when(tab.getUrl()).thenReturn(JUnitTestGURLs.URL_1);
+        when(tab.loadIfNeeded(anyInt())).thenReturn(true);
+        when(tab.isLoading()).thenReturn(true);
 
         captureAndSpyNavigationProvider();
 
@@ -239,6 +246,55 @@
         mNavigationProvider.onSelectionStateChange(selection);
 
         verify(tab).loadIfNeeded(TabLoadIfNeededCaller.FUSEBOX_ATTACHMENT);
+        verify(mTabListEditorController).setThumbnailSpinnerVisibility(tab, true);
+        verify(tab).addObserver(mTabObserverCaptor.capture());
+    }
+
+    @Test
+    @EnableFeatures(ChromeFeatureList.ON_DEMAND_BACKGROUND_TAB_CONTEXT_CAPTURE)
+    public void testTabLoadFinished() {
+        int tabId = 101;
+        Tab tab = mockTabActiveState(tabId, false);
+        when(tab.getUrl()).thenReturn(JUnitTestGURLs.URL_1);
+        when(tab.loadIfNeeded(anyInt())).thenReturn(true);
+        when(tab.isLoading()).thenReturn(true);
+
+        captureAndSpyNavigationProvider();
+
+        TabListEditorItemSelectionId id = TabListEditorItemSelectionId.createTabId(tabId);
+        Set<TabListEditorItemSelectionId> selection = new HashSet<>();
+        selection.add(id);
+
+        mNavigationProvider.onSelectionStateChange(selection);
+
+        verify(tab).addObserver(mTabObserverCaptor.capture());
+        TabObserver observer = mTabObserverCaptor.getValue();
+
+        observer.onPageLoadFinished(tab, JUnitTestGURLs.URL_1);
+
+        verify(tab).removeObserver(observer);
+        verify(mTabListEditorController).updateThumbnail(tab);
+    }
+
+    @Test
+    @EnableFeatures(ChromeFeatureList.ON_DEMAND_BACKGROUND_TAB_CONTEXT_CAPTURE)
+    public void testTabLoadFinished_AlreadyLoaded() {
+        int tabId = 101;
+        Tab tab = mockTabActiveState(tabId, false);
+        when(tab.getUrl()).thenReturn(JUnitTestGURLs.URL_1);
+        when(tab.loadIfNeeded(anyInt())).thenReturn(true);
+        when(tab.isLoading()).thenReturn(false);
+
+        captureAndSpyNavigationProvider();
+
+        TabListEditorItemSelectionId id = TabListEditorItemSelectionId.createTabId(tabId);
+        Set<TabListEditorItemSelectionId> selection = new HashSet<>();
+        selection.add(id);
+
+        mNavigationProvider.onSelectionStateChange(selection);
+
+        verify(tab, never()).addObserver(any());
+        verify(mTabListEditorController, never()).updateThumbnail(tab);
     }
 
     @Test
@@ -261,6 +317,94 @@
 
     @Test
     @EnableFeatures(ChromeFeatureList.ON_DEMAND_BACKGROUND_TAB_CONTEXT_CAPTURE)
+    public void testSelectionChangeLoadsBackgroundTabs_RedundantTrigger() {
+        int tabId = 101;
+        Tab tab = mockTabActiveState(tabId, false);
+        when(tab.getUrl()).thenReturn(JUnitTestGURLs.URL_1);
+        when(tab.loadIfNeeded(anyInt())).thenReturn(true);
+        when(tab.isLoading()).thenReturn(true);
+
+        captureAndSpyNavigationProvider();
+
+        TabListEditorItemSelectionId id = TabListEditorItemSelectionId.createTabId(tabId);
+        Set<TabListEditorItemSelectionId> selection = new HashSet<>();
+        selection.add(id);
+
+        mNavigationProvider.onSelectionStateChange(selection);
+        mNavigationProvider.onSelectionStateChange(selection);
+
+        verify(tab, times(2)).loadIfNeeded(anyInt());
+    }
+
+    @Test
+    @EnableFeatures(ChromeFeatureList.ON_DEMAND_BACKGROUND_TAB_CONTEXT_CAPTURE)
+    public void testSelectionChangeDoesNotAddObserverTwice() {
+        int tabId = 101;
+        Tab tab = mockTabActiveState(tabId, false);
+        when(tab.getUrl()).thenReturn(JUnitTestGURLs.URL_1);
+        when(tab.loadIfNeeded(anyInt())).thenReturn(true);
+        when(tab.isLoading()).thenReturn(true);
+
+        captureAndSpyNavigationProvider();
+
+        TabListEditorItemSelectionId id = TabListEditorItemSelectionId.createTabId(tabId);
+        Set<TabListEditorItemSelectionId> selection = new HashSet<>();
+        selection.add(id);
+
+        mNavigationProvider.onSelectionStateChange(selection);
+        mNavigationProvider.onSelectionStateChange(selection);
+
+        verify(tab, times(1)).addObserver(any());
+    }
+
+    @Test
+    @EnableFeatures(ChromeFeatureList.ON_DEMAND_BACKGROUND_TAB_CONTEXT_CAPTURE)
+    public void testSelectionChangeDoesNotShowSpinnerTwice() {
+        int tabId = 101;
+        Tab tab = mockTabActiveState(tabId, false);
+        when(tab.getUrl()).thenReturn(JUnitTestGURLs.URL_1);
+        when(tab.loadIfNeeded(anyInt())).thenReturn(true);
+        when(tab.isLoading()).thenReturn(true);
+
+        captureAndSpyNavigationProvider();
+
+        TabListEditorItemSelectionId id = TabListEditorItemSelectionId.createTabId(tabId);
+        Set<TabListEditorItemSelectionId> selection = new HashSet<>();
+        selection.add(id);
+
+        mNavigationProvider.onSelectionStateChange(selection);
+        mNavigationProvider.onSelectionStateChange(selection);
+
+        verify(mTabListEditorController, times(1)).setThumbnailSpinnerVisibility(tab, true);
+    }
+
+    @Test
+    @EnableFeatures(ChromeFeatureList.ON_DEMAND_BACKGROUND_TAB_CONTEXT_CAPTURE)
+    public void testSelectionChange_RedundantTrigger_Loading() {
+        int tabId = 101;
+        Tab tab = mockTabActiveState(tabId, false);
+        when(tab.getUrl()).thenReturn(JUnitTestGURLs.URL_1);
+        when(tab.loadIfNeeded(anyInt())).thenReturn(true);
+        when(tab.isLoading()).thenReturn(true);
+
+        captureAndSpyNavigationProvider();
+
+        TabListEditorItemSelectionId id = TabListEditorItemSelectionId.createTabId(tabId);
+        Set<TabListEditorItemSelectionId> selection = new HashSet<>();
+        selection.add(id);
+
+        mNavigationProvider.onSelectionStateChange(selection);
+        mNavigationProvider.onSelectionStateChange(selection);
+
+        verify(tab, times(2)).loadIfNeeded(anyInt());
+        // updateThumbnail is not called yet because the tab is still loading.
+        verify(mTabListEditorController, never()).updateThumbnail(tab);
+        // Spinner is shown once.
+        verify(mTabListEditorController, times(1)).setThumbnailSpinnerVisibility(tab, true);
+    }
+
+    @Test
+    @EnableFeatures(ChromeFeatureList.ON_DEMAND_BACKGROUND_TAB_CONTEXT_CAPTURE)
     public void testSelectionChangeDoesNotLoadCachedTabs() {
         int tabId = 101;
         mCachedTabIds.add(tabId);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelper.java b/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelper.java
index 431a0c5f..8b55593 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelper.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelper.java
@@ -106,8 +106,8 @@
 import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.share.ShareDelegate;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.tab.Tab;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
 import org.chromium.chrome.browser.tab.TabId;
 import org.chromium.chrome.browser.tab_group_sync.TabGroupSyncServiceFactory;
 import org.chromium.chrome.browser.tab_ui.ActionConfirmationManager;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelperManager.java b/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelperManager.java
index 1bd6e79..a4dac6e 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelperManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelperManager.java
@@ -84,9 +84,9 @@
 import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
 import org.chromium.chrome.browser.share.ShareDelegate;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.Tab.LoadUrlResult;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
 import org.chromium.chrome.browser.tab.TabClosingSource;
 import org.chromium.chrome.browser.tab.TabCreationState;
 import org.chromium.chrome.browser.tab.TabLaunchType;
@@ -201,6 +201,7 @@
     private static final float GLIC_ICON_WIDTH_DP = 24.f;
     private static final float GLIC_ICON_TEXT_PADDING_DP = 4.f;
     private static final float GLIC_BUTTON_END_PADDING_DP = 10.f;
+    private static final float GLIC_BUTTON_CORNER_RADIUS = 12.f;
     private static final float GLIC_BUTTON_HOVER_BACKGROUND_PRESSED_OPACITY = 0.24f;
     private static final float GLIC_BUTTON_HOVER_BACKGROUND_DEFAULT_OPACITY = 0.16f;
 
@@ -761,7 +762,6 @@
                         keyboardFocusHandler,
                         R.drawable.ic_spark_24dp,
                         BUTTON_CLICK_SLOP_DP);
-        mGlicButton.setBackgroundResourceId(R.drawable.bg_circle_tab_strip_button);
         mGlicButton.setDrawY(BUTTON_BACKGROUND_Y_OFFSET_DP);
         mGlicButton.setVisible(false);
 
@@ -1932,10 +1932,10 @@
      * @param defaultTint The default tint to use.
      */
     public @ColorInt int getMediaIndicatorTintColor(
-            @Tab.MediaState int mediaState, @ColorInt int defaultTint) {
-        if (mediaState == Tab.MediaState.RECORDING) {
+            @MediaState int mediaState, @ColorInt int defaultTint) {
+        if (mediaState == MediaState.RECORDING) {
             return mContext.getColor(R.color.tab_recording_media_color);
-        } else if (mediaState == Tab.MediaState.SHARING) {
+        } else if (mediaState == MediaState.SHARING) {
             return mContext.getColor(R.color.tab_sharing_media_color);
         }
         return defaultTint;
@@ -2033,6 +2033,10 @@
         return GLIC_ICON_TEXT_PADDING_DP;
     }
 
+    public float getGlicButtonCornerRadius() {
+        return GLIC_BUTTON_CORNER_RADIUS;
+    }
+
     private boolean shouldMsbBeVisible() {
         if (mModelSelectorButton == null) return false;
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutTab.java b/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutTab.java
index d2a24a0..61426a1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutTab.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutTab.java
@@ -35,8 +35,8 @@
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.layouts.animation.CompositorAnimator;
 import org.chromium.chrome.browser.layouts.components.VirtualView;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.tab.Tab;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
 import org.chromium.chrome.browser.tasks.tab_management.TabUiThemeUtil;
 import org.chromium.components.browser_ui.styles.ChromeColors;
 import org.chromium.components.browser_ui.styles.SemanticColorUtils;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/TabContextMenuCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/TabContextMenuCoordinator.java
index 1351a30..d1e297b 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/TabContextMenuCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/compositor/overlays/strip/TabContextMenuCoordinator.java
@@ -157,6 +157,7 @@
                         tabModelSupplier,
                         tabGroupModelFilter,
                         tabGroupListBottomSheetCoordinator,
+                        tabGroupCreationCallback,
                         multiInstanceManager,
                         shareDelegateSupplier),
                 tabModelSupplier,
@@ -225,6 +226,7 @@
             Supplier<TabModel> tabModelSupplier,
             TabGroupModelFilter tabGroupModelFilter,
             TabGroupListBottomSheetCoordinator tabGroupListBottomSheetCoordinator,
+            TabGroupCreationCallback tabGroupCreationCallback,
             MultiInstanceManager multiInstanceManager,
             MonotonicObservableSupplier<ShareDelegate> shareDelegateSupplier) {
         return (menuId, anchorInfo, collaborationId, listViewTouchTracker) -> {
@@ -237,6 +239,12 @@
 
             if (menuId == R.id.add_to_tab_group) {
                 tabGroupListBottomSheetCoordinator.showBottomSheet(tabs);
+            } else if (menuId == R.id.add_to_new_tab_group) {
+                createNewGroupForTabs(
+                        tabs,
+                        tabGroupModelFilter,
+                        /* tabMovedCallback= */ null,
+                        tabGroupCreationCallback);
             } else if (menuId == R.id.remove_from_tab_group) {
                 // Ungrouping in reverse to maintain the order of the tabs.
                 Collections.reverse(tabs);
@@ -585,6 +593,8 @@
     private static void recordMenuAction(int menuId, boolean isMultipleTabs) {
         if (menuId == R.id.add_to_tab_group) {
             recordUserAction("AddToTabGroup", isMultipleTabs);
+        } else if (menuId == R.id.add_to_new_tab_group) {
+            recordUserAction("AddToNewTabGroup", isMultipleTabs);
         } else if (menuId == R.id.remove_from_tab_group) {
             recordUserAction("RemoveTabFromTabGroup", isMultipleTabs);
         } else if (menuId == R.id.move_to_other_window_menu_id) {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/compositor/scene_layer/TabStripSceneLayer.java b/chrome/android/java/src/org/chromium/chrome/browser/compositor/scene_layer/TabStripSceneLayer.java
index 65b44d0..d4fda39f 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/compositor/scene_layer/TabStripSceneLayer.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/compositor/scene_layer/TabStripSceneLayer.java
@@ -28,7 +28,7 @@
 import org.chromium.chrome.browser.compositor.overlays.strip.StripLayoutUtils;
 import org.chromium.chrome.browser.layouts.scene_layer.SceneLayer;
 import org.chromium.chrome.browser.layouts.scene_layer.SceneOverlayLayer;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.tab.TabId;
 import org.chromium.chrome.browser.tab.TabUtils;
 import org.chromium.chrome.browser.tasks.tab_management.TabUiThemeUtil;
@@ -199,10 +199,10 @@
                     .updateGlicButton(
                             mNativePtr,
                             glicButton.getResourceId(),
-                            glicButton.getBackgroundResourceId(),
                             Math.round(glicButton.getDrawX() * mDpToPx),
                             Math.round(glicButton.getDrawY() * mDpToPx),
                             Math.round(glicButton.getWidth() * mDpToPx),
+                            Math.round(glicButton.getHeight() * mDpToPx),
                             glicButtonVisible,
                             glicButton.getShouldApplyHoverBackground(),
                             glicButton.getTint(),
@@ -213,7 +213,8 @@
                             glicButton.getKeyboardFocusRingColor(),
                             glicButton.getTextResourceId(),
                             layoutHelper.getGlicButtonStartPadding(),
-                            layoutHelper.getGlicIconTextPadding());
+                            layoutHelper.getGlicIconTextPadding(),
+                            Math.round(layoutHelper.getGlicButtonCornerRadius() * mDpToPx));
         }
 
         CompositorButton modelSelectorButton = layoutHelper.getModelSelectorButton();
@@ -440,10 +441,10 @@
         void updateGlicButton(
                 long nativeTabStripSceneLayer,
                 @DrawableRes int resourceId,
-                @DrawableRes int backgroundResourceId,
                 float x,
                 float y,
                 float buttonWidth,
+                float buttonHeight,
                 boolean visible,
                 boolean isHovered,
                 @ColorInt int tint,
@@ -454,7 +455,8 @@
                 @ColorInt int keyboardFocusRingColor,
                 int textTextureId,
                 float buttonStartPadding,
-                float buttonTextPadding);
+                float buttonTextPadding,
+                float cornerRadius);
 
         void updateModelSelectorButton(
                 long nativeTabStripSceneLayer,
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
index 408e7ae8..fe66b37e 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
@@ -16,7 +16,6 @@
 import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 
 import androidx.annotation.CallSuper;
@@ -68,8 +67,6 @@
 import org.chromium.content_public.common.ResourceRequestBody;
 import org.chromium.ui.base.ActivityWindowAndroid;
 import org.chromium.ui.base.ViewAndroidDelegate;
-import org.chromium.ui.display.DisplayAndroid;
-import org.chromium.ui.display.DisplayUtil;
 import org.chromium.ui.modaldialog.ModalDialogManager;
 import org.chromium.url.GURL;
 
@@ -322,63 +319,6 @@
                                                 this, mInitiatorTab, this::finish));
             }
         }
-
-        if (mWindowOptions != null && mWindowOptions.windowBounds != null) {
-            contentLayout
-                    .getViewTreeObserver()
-                    .addOnGlobalLayoutListener(
-                            new ViewTreeObserver.OnGlobalLayoutListener() {
-                                @Override
-                                public void onGlobalLayout() {
-                                    resizeContents(
-                                            assumeNonNull(mWindowOptions.windowBounds).width(),
-                                            assumeNonNull(mWindowOptions.windowBounds).height());
-
-                                    contentLayout
-                                            .getViewTreeObserver()
-                                            .removeOnGlobalLayoutListener(this);
-                                }
-                            });
-        }
-    }
-
-    /**
-     * Resizes the contents of the activity to the given DP dimensions.
-     *
-     * <p>This method resizes the contents of the activity to the given DP dimensions by resizing
-     * the window. Note that Android has a minimum size (220dp) & a maximum size (70% of display
-     * size in width and height) for pinned windows, so the requested size may not be respected.
-     */
-    private void resizeContents(int widthDp, int heightDp) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
-            // This method is not supported on API versions below 30.
-            return;
-        }
-
-        FrameLayout contentLayout = findViewById(R.id.document_picture_in_picture_content);
-        DisplayAndroid display = assumeNonNull(getWindowAndroid()).getDisplay();
-        int curContentsWidth = DisplayUtil.pxToDp(display, contentLayout.getWidth());
-        int curContentsHeight = DisplayUtil.pxToDp(display, contentLayout.getHeight());
-
-        if (curContentsWidth == widthDp && curContentsHeight == heightDp) {
-            return;
-        }
-
-        int widthDiff = widthDp - curContentsWidth;
-        int heightDiff = heightDp - curContentsHeight;
-
-        Rect currentWindowBounds =
-                DisplayUtil.convertLocalPxToGlobalDipCoordinates(
-                        display,
-                        new Rect(getWindowManager().getCurrentWindowMetrics().getBounds()));
-
-        MultiWindowUtils.moveActivityToBounds(
-                this,
-                new Rect(
-                        currentWindowBounds.left - widthDiff,
-                        currentWindowBounds.top - heightDiff,
-                        currentWindowBounds.right,
-                        currentWindowBounds.bottom));
     }
 
     @Override
@@ -557,7 +497,7 @@
 
         @Override
         public void setContentsBounds(WebContents source, Rect bounds) {
-            resizeContents(bounds.width(), bounds.height());
+            MultiWindowUtils.moveActivityToBounds(DocumentPictureInPictureActivity.this, bounds);
         }
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureController.java b/chrome/android/java/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureController.java
index 3efd0fa..83470d3 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureController.java
@@ -333,6 +333,10 @@
         webContents.setHasPersistentVideo(true);
 
         final Tab activityTab = mActivityTabProvider.get();
+        if (activityTab == null) {
+            Log.i(TAG, "Activity tab is null, not entering Picture-in-picture");
+            return;
+        }
 
         // We don't want InfoBars displaying while in PiP, they cover too much content.
         assumeNonNull(getInfoBarContainerForTab(activityTab)).setHidden(true);
@@ -340,8 +344,15 @@
         mOnLeavePipCallbacks.add(
                 () -> {
                     Log.i(TAG, "Running Picture-in-picture exit callbacks");
-                    webContents.setHasPersistentVideo(false);
-                    assumeNonNull(getInfoBarContainerForTab(activityTab)).setHidden(false);
+                    if (!webContents.isDestroyed()) {
+                        webContents.setHasPersistentVideo(false);
+                    }
+                    if (!activityTab.isDestroyed()) {
+                        InfoBarContainer container = getInfoBarContainerForTab(activityTab);
+                        if (container != null) {
+                            container.setHidden(false);
+                        }
+                    }
                 });
 
         // Setup observers to dismiss the Activity on events that should end PiP.  In auto-enter
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageLayout.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageLayout.java
index 99579d5..25f364f7 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageLayout.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageLayout.java
@@ -591,27 +591,37 @@
 
     private void initializeLayoutChangeListener() {
         TraceEvent.begin(TAG + ".initializeLayoutChangeListener()");
-        mOnLayoutChangeListener =
-                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
-                    int oldHeight = oldBottom - oldTop;
-                    int newHeight = bottom - top;
-
-                    if (oldHeight == newHeight && !mTileCountChanged) return;
-                    mTileCountChanged = false;
-
-                    // Re-apply the url focus change amount after a rotation to ensure the views are
-                    // correctly placed with their new layout configurations.
-                    onUrlFocusAnimationChanged();
-                    updateSearchBoxOnScroll();
-
-                    // The positioning of elements may have been changed (since the elements expand
-                    // to fill the available vertical space), so adjust the scroll.
-                    if (mScrollDelegate.isScrollViewInitialized()) mScrollDelegate.snapScroll();
-                };
+        mOnLayoutChangeListener = this::onLayoutChanged;
         addOnLayoutChangeListener(mOnLayoutChangeListener);
         TraceEvent.end(TAG + ".initializeLayoutChangeListener()");
     }
 
+    private void onLayoutChanged(
+            View view,
+            int left,
+            int top,
+            int right,
+            int bottom,
+            int oldLeft,
+            int oldTop,
+            int oldRight,
+            int oldBottom) {
+        int oldHeight = oldBottom - oldTop;
+        int newHeight = bottom - top;
+
+        if (oldHeight == newHeight && !mTileCountChanged) return;
+        mTileCountChanged = false;
+
+        // Re-apply the url focus change amount after a rotation to ensure the views are
+        // correctly placed with their new layout configurations.
+        onUrlFocusAnimationChanged();
+        updateSearchBoxOnScroll();
+
+        // The positioning of elements may have been changed (since the elements expand
+        // to fill the available vertical space), so adjust the scroll.
+        if (mScrollDelegate.isScrollViewInitialized()) mScrollDelegate.snapScroll();
+    }
+
     private void initializeLogoCoordinator() {
         Callback<LoadUrlParams> logoClickedCallback =
                 mCallbackController.makeCancelable(
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
index efcda53..8725540 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
@@ -2879,7 +2879,7 @@
         if (mMediaState == mediaState) return;
         mMediaState = mediaState;
         RecordHistogram.recordEnumeratedHistogram(
-                "Tab.Android.MediaState", mediaState, MediaState.COUNT);
+                "Tab.Android.MediaState", mediaState, MediaState.MAX_VALUE + 1);
         if (ChromeFeatureList.sMediaIndicatorsAndroid.isEnabled()) {
             for (TabObserver observer : mObservers) {
                 observer.onMediaStateChanged(this, mediaState);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabUtils.java b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabUtils.java
index 133b189c5..1879976b 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabUtils.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabUtils.java
@@ -32,7 +32,6 @@
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
 import org.chromium.chrome.browser.media.MediaCaptureDevicesDispatcherAndroid;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
 import org.chromium.chrome.browser.tasks.tab_management.TabUiThemeProvider;
 import org.chromium.components.browser_ui.util.AutomotiveUtils;
 import org.chromium.components.browser_ui.util.DimensionCompat;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedAdaptiveToolbarBehavior.java b/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedAdaptiveToolbarBehavior.java
index 698941a..b65827f6 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedAdaptiveToolbarBehavior.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedAdaptiveToolbarBehavior.java
@@ -54,6 +54,7 @@
     private final Supplier<ModalDialogManager> mModalDialogManagerSupplier;
     private final MonotonicObservableSupplier<@StripVisibilityState Integer>
             mTabStripVisibilitySupplier;
+    private final Runnable mToggleGlicCallback;
 
     public TabbedAdaptiveToolbarBehavior(
             Context context,
@@ -66,7 +67,8 @@
             Supplier<GroupSuggestionsButtonController> groupSuggestionsButtonController,
             Supplier<TabModelSelector> tabModelSelectorSupplier,
             Supplier<ModalDialogManager> modalDialogManagerSupplier,
-            MonotonicObservableSupplier<@StripVisibilityState Integer> tabStripVisibilitySupplier) {
+            MonotonicObservableSupplier<@StripVisibilityState Integer> tabStripVisibilitySupplier,
+            Runnable toggleGlicCallback) {
         mContext = context;
         mActivityLifecycleDispatcher = activityLifecycleDispatcher;
         mTabCreatorManagerSupplier = tabCreatorManagerSupplier;
@@ -78,6 +80,7 @@
         mTabModelSelectorSupplier = tabModelSelectorSupplier;
         mModalDialogManagerSupplier = modalDialogManagerSupplier;
         mTabStripVisibilitySupplier = tabStripVisibilitySupplier;
+        mToggleGlicCallback = toggleGlicCallback;
     }
 
     @Override
@@ -111,10 +114,6 @@
                         AiAssistantService.getInstance(),
                         trackerSupplier);
         controller.addButtonVariant(AdaptiveToolbarButtonVariant.PAGE_SUMMARY, pageSummary);
-        if (AdaptiveToolbarFeatures.isGlicActionEnabled()) {
-            var glicButton = new GlicToolbarButtonController(mContext, mActivityTabProvider);
-            controller.addButtonVariant(AdaptiveToolbarButtonVariant.GLIC, glicButton);
-        }
         if (AdaptiveToolbarFeatures.isTabGroupingPageActionEnabled()) {
             var tabGrouping =
                     new GroupSuggestionsButtonDataProvider(
@@ -126,6 +125,13 @@
             controller.addButtonVariant(AdaptiveToolbarButtonVariant.TAB_GROUPING, tabGrouping);
         }
 
+        if (AdaptiveToolbarFeatures.isGlicActionEnabled()) {
+            controller.addButtonVariant(
+                    AdaptiveToolbarButtonVariant.GLIC,
+                    new GlicToolbarButtonController(
+                            mContext, mActivityTabProvider, mToggleGlicCallback));
+        }
+
         mRegisterVoiceSearchRunnable.run();
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedRootUiCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedRootUiCoordinator.java
index 0d57e49..063dd59 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedRootUiCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedRootUiCoordinator.java
@@ -46,7 +46,6 @@
 import org.chromium.base.supplier.SupplierUtils;
 import org.chromium.base.version_info.VersionInfo;
 import org.chromium.build.BuildConfig;
-import org.chromium.build.annotations.NonNull;
 import org.chromium.build.annotations.Nullable;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.ActivityTabProvider;
@@ -315,10 +314,9 @@
     private @Nullable BookmarkOpener mBookmarkOpener;
     private @Nullable TabBottomSheetManager mTabBottomSheetManager;
     private @Nullable CoBrowseViewFactory mCoBrowseViewFactory;
-    private final @NonNull MonotonicObservableSupplier<BookmarkManagerOpener>
-            mBookmarkManagerOpenerSupplier;
-    private @NonNull AdvancedProtectionCoordinator mAdvancedProtectionCoordinator;
-    private final @NonNull KeyboardFocusRowManager mKeyboardFocusRowManager;
+    private final MonotonicObservableSupplier<BookmarkManagerOpener> mBookmarkManagerOpenerSupplier;
+    private AdvancedProtectionCoordinator mAdvancedProtectionCoordinator;
+    private final KeyboardFocusRowManager mKeyboardFocusRowManager;
     private CharSequence mApplicationLabel;
     private TipsOptInCoordinator mTipsOptInCoordinator;
     private final OneshotSupplier<ChromeInactivityTracker> mInactivityTrackerSupplier;
@@ -437,65 +435,60 @@
      *     space mode, false otherwise.
      */
     public TabbedRootUiCoordinator(
-            @NonNull AppCompatActivity activity,
+            AppCompatActivity activity,
             @Nullable Callback<Boolean> onOmniboxFocusChangedListener,
-            @NonNull MonotonicObservableSupplier<ShareDelegate> shareDelegateSupplier,
-            @NonNull ActivityTabProvider tabProvider,
-            @NonNull MonotonicObservableSupplier<Profile> profileSupplier,
-            @NonNull NullableObservableSupplier<BookmarkModel> bookmarkModelSupplier,
-            @NonNull MonotonicObservableSupplier<TabBookmarker> tabBookmarkerSupplier,
-            @NonNull MonotonicObservableSupplier<TabModelSelector> tabModelSelectorSupplier,
-            @NonNull OneshotSupplier<TabSwitcher> tabSwitcherSupplier,
-            @NonNull OneshotSupplier<TabSwitcher> incognitoTabSwitcherSupplier,
-            @NonNull OneshotSupplier<HubManager> hubManagerSupplier,
-            @NonNull OneshotSupplier<ToolbarIntentMetadata> intentMetadataOneshotSupplier,
-            @NonNull OneshotSupplier<LayoutStateProvider> layoutStateProviderOneshotSupplier,
-            @NonNull BrowserControlsManager browserControlsManager,
-            @NonNull ActivityWindowAndroid windowAndroid,
-            @NonNull ActivityResultTracker activityResultTracker,
-            @NonNull OneshotSupplier chromeAndroidTaskSupplier,
-            @NonNull ActivityLifecycleDispatcher activityLifecycleDispatcher,
-            @NonNull MonotonicObservableSupplier<LayoutManagerImpl> layoutManagerSupplier,
-            @NonNull MenuOrKeyboardActionController menuOrKeyboardActionController,
-            @NonNull Supplier<Integer> activityThemeColorSupplier,
-            @NonNull NonNullObservableSupplier<ModalDialogManager> modalDialogManagerSupplier,
-            @NonNull AppMenuBlocker appMenuBlocker,
-            @NonNull BooleanSupplier supportsAppMenuSupplier,
-            @NonNull BooleanSupplier supportsFindInPage,
-            @NonNull Supplier<TabCreatorManager> tabCreatorManagerSupplier,
-            @NonNull FullscreenManager fullscreenManager,
-            @NonNull Supplier<CompositorViewHolder> compositorViewHolderSupplier,
-            @NonNull Supplier<TabContentManager> tabContentManagerSupplier,
-            @NonNull MonotonicObservableSupplier<SnackbarManager> snackbarManagerSupplier,
-            @NonNull SettableMonotonicObservableSupplier<EdgeToEdgeController> edgeToEdgeSupplier,
-            @NonNull TopInsetProvider topInsetProvider,
-            @NonNull OneshotSupplierImpl<SystemBarColorHelper> systemBarColorHelperSupplier,
+            MonotonicObservableSupplier<ShareDelegate> shareDelegateSupplier,
+            ActivityTabProvider tabProvider,
+            MonotonicObservableSupplier<Profile> profileSupplier,
+            NullableObservableSupplier<BookmarkModel> bookmarkModelSupplier,
+            MonotonicObservableSupplier<TabBookmarker> tabBookmarkerSupplier,
+            MonotonicObservableSupplier<TabModelSelector> tabModelSelectorSupplier,
+            OneshotSupplier<TabSwitcher> tabSwitcherSupplier,
+            OneshotSupplier<TabSwitcher> incognitoTabSwitcherSupplier,
+            OneshotSupplier<HubManager> hubManagerSupplier,
+            OneshotSupplier<ToolbarIntentMetadata> intentMetadataOneshotSupplier,
+            OneshotSupplier<LayoutStateProvider> layoutStateProviderOneshotSupplier,
+            BrowserControlsManager browserControlsManager,
+            ActivityWindowAndroid windowAndroid,
+            ActivityResultTracker activityResultTracker,
+            OneshotSupplier chromeAndroidTaskSupplier,
+            ActivityLifecycleDispatcher activityLifecycleDispatcher,
+            MonotonicObservableSupplier<LayoutManagerImpl> layoutManagerSupplier,
+            MenuOrKeyboardActionController menuOrKeyboardActionController,
+            Supplier<Integer> activityThemeColorSupplier,
+            MonotonicObservableSupplier<ModalDialogManager> modalDialogManagerSupplier,
+            AppMenuBlocker appMenuBlocker,
+            BooleanSupplier supportsAppMenuSupplier,
+            BooleanSupplier supportsFindInPage,
+            Supplier<TabCreatorManager> tabCreatorManagerSupplier,
+            FullscreenManager fullscreenManager,
+            Supplier<CompositorViewHolder> compositorViewHolderSupplier,
+            Supplier<TabContentManager> tabContentManagerSupplier,
+            MonotonicObservableSupplier<SnackbarManager> snackbarManagerSupplier,
+            SettableMonotonicObservableSupplier<EdgeToEdgeController> edgeToEdgeSupplier,
+            TopInsetProvider topInsetProvider,
+            OneshotSupplierImpl<SystemBarColorHelper> systemBarColorHelperSupplier,
             @ActivityType int activityType,
-            @NonNull Supplier<Boolean> isInOverviewModeSupplier,
-            @NonNull AppMenuDelegate appMenuDelegate,
-            @NonNull StatusBarColorProvider statusBarColorProvider,
-            @NonNull
-                    SettableMonotonicObservableSupplier<EphemeralTabCoordinator>
-                            ephemeralTabCoordinatorSupplier,
-            @NonNull IntentRequestTracker intentRequestTracker,
-            @NonNull InsetObserver insetObserver,
-            @NonNull Function<Tab, Boolean> backButtonShouldCloseTabFn,
-            @NonNull Callback<Tab> sendToBackground,
+            Supplier<Boolean> isInOverviewModeSupplier,
+            AppMenuDelegate appMenuDelegate,
+            StatusBarColorProvider statusBarColorProvider,
+            SettableMonotonicObservableSupplier<EphemeralTabCoordinator>
+                    ephemeralTabCoordinatorSupplier,
+            IntentRequestTracker intentRequestTracker,
+            InsetObserver insetObserver,
+            Function<Tab, Boolean> backButtonShouldCloseTabFn,
+            Callback<Tab> sendToBackground,
             boolean initializeUiWithIncognitoColors,
-            @NonNull BackPressManager backPressManager,
+            BackPressManager backPressManager,
             @Nullable Bundle savedInstanceState,
             @Nullable PersistableBundle persistentState,
             @Nullable MultiInstanceManager multiInstanceManager,
-            @NonNull NonNullObservableSupplier<Integer> overviewColorSupplier,
-            @NonNull
-                    MonotonicObservableSupplier<ManualFillingComponent>
-                            manualFillingComponentSupplier,
-            @NonNull EdgeToEdgeManager edgeToEdgeManager,
-            @NonNull
-                    MonotonicObservableSupplier<BookmarkManagerOpener>
-                            bookmarkManagerOpenerSupplier,
+            NonNullObservableSupplier<Integer> overviewColorSupplier,
+            MonotonicObservableSupplier<ManualFillingComponent> manualFillingComponentSupplier,
+            EdgeToEdgeManager edgeToEdgeManager,
+            MonotonicObservableSupplier<BookmarkManagerOpener> bookmarkManagerOpenerSupplier,
             NonNullObservableSupplier<Boolean> xrSpaceModeObservableSupplier,
-            @NonNull OneshotSupplier<ChromeInactivityTracker> inactivityTrackerSupplier) {
+            OneshotSupplier<ChromeInactivityTracker> inactivityTrackerSupplier) {
         super(
                 activity,
                 onOmniboxFocusChangedListener,
@@ -517,7 +510,7 @@
                 layoutManagerSupplier,
                 menuOrKeyboardActionController,
                 activityThemeColorSupplier,
-                modalDialogManagerSupplier,
+                modalDialogManagerSupplier.asNonNull(),
                 appMenuBlocker,
                 supportsAppMenuSupplier,
                 supportsFindInPage,
@@ -1148,7 +1141,8 @@
                 mTabModelSelectorSupplier,
                 mModalDialogManagerSupplier,
                 // TODO(agrieve): See if this can be changed to a NonNullObservableSupplier.
-                (MonotonicObservableSupplier<Integer>) mTabStripVisibilitySupplier);
+                (MonotonicObservableSupplier<Integer>) mTabStripVisibilitySupplier,
+                () -> toggleGlic());
     }
 
     @Override
@@ -2031,8 +2025,12 @@
         // any promo that you want to trigger at every startup (temporarily for debugging and/or
         // development).
         if (FullscreenSigninPromoLauncher.launchPromoIfForced(
-                mActivity, profile, SigninAndHistorySyncActivityLauncherImpl.get())) return true;
-        if (PwaRestorePromoUtils.maybeForceShowPromo(profile, mWindowAndroid)) return true;
+                mActivity, profile, SigninAndHistorySyncActivityLauncherImpl.get())) {
+            return true;
+        }
+        if (PwaRestorePromoUtils.maybeForceShowPromo(profile, mWindowAndroid)) {
+            return true;
+        }
 
         return false;
     }
diff --git a/chrome/android/javatests/BUILD.gn b/chrome/android/javatests/BUILD.gn
index fd4f0b3..b892d99 100644
--- a/chrome/android/javatests/BUILD.gn
+++ b/chrome/android/javatests/BUILD.gn
@@ -543,6 +543,7 @@
     "src/org/chromium/chrome/browser/app/flags/ChromeCachedFlagsTest.java",
     "src/org/chromium/chrome/browser/app/metrics/TabbedActivityLaunchCauseMetricsTest.java",
     "src/org/chromium/chrome/browser/app/tab_activity_glue/TabletPhoneLayoutChangeTest.java",
+    "src/org/chromium/chrome/browser/app/tabmodel/ActiveTabCacheTest.java",
     "src/org/chromium/chrome/browser/app/tabmodel/AllTabObserverTest.java",
     "src/org/chromium/chrome/browser/app/tabmodel/ArchivedTabModelOrchestratorTest.java",
     "src/org/chromium/chrome/browser/app/tabmodel/ArchivedTabsTest.java",
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/app/tabmodel/ActiveTabCacheTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/app/tabmodel/ActiveTabCacheTest.java
new file mode 100644
index 0000000..55fe996a
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/app/tabmodel/ActiveTabCacheTest.java
@@ -0,0 +1,173 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.app.tabmodel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.content.Context;
+
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.ContextUtils;
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.test.util.Batch;
+import org.chromium.base.test.util.CriteriaHelper;
+import org.chromium.chrome.browser.app.tabmodel.ActiveTabCache.CachedActiveTab;
+import org.chromium.chrome.browser.crypto.CipherFactory;
+import org.chromium.chrome.browser.tab.Tab;
+import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
+import org.chromium.chrome.test.transit.ChromeTransitTestRules;
+import org.chromium.chrome.test.transit.FreshCtaTransitTestRule;
+import org.chromium.chrome.test.transit.page.WebPageStation;
+
+import java.io.File;
+import java.util.concurrent.ExecutionException;
+
+/** Integration tests for {@link ActiveTabCache}. */
+@RunWith(ChromeJUnit4ClassRunner.class)
+@Batch(Batch.PER_CLASS)
+public class ActiveTabCacheTest {
+    @Rule
+    public FreshCtaTransitTestRule mActivityTestRule =
+            ChromeTransitTestRules.freshChromeTabbedActivityRule();
+
+    private final CipherFactory mCipherFactory = new CipherFactory();
+    private ActiveTabCache mActiveTabCache;
+
+    @Before
+    public void setUp() {
+        mActiveTabCache = new ActiveTabCache("0");
+    }
+
+    private File getActiveTabFile(boolean incognito) {
+        String fileName = incognito ? "0_incognito" : "0_regular";
+        return new File(
+                ContextUtils.getApplicationContext().getDir("active_tabs", Context.MODE_PRIVATE),
+                fileName);
+    }
+
+    @Test
+    @MediumTest
+    public void testSaveAndRestore() throws ExecutionException {
+        WebPageStation page = mActivityTestRule.startOnBlankPage();
+        Tab tab = page.getTab();
+
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    mActiveTabCache.saveActiveTab(tab, 0, null);
+                });
+
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    CachedActiveTab cachedTab = mActiveTabCache.restoreActiveTab(false, null);
+                    assertNotNull("Cached tab should not be null", cachedTab);
+                    assertEquals("Tab index should match", 0, cachedTab.tabIndex);
+                    assertNotNull("Tab state should not be null", cachedTab.tabState);
+                    assertEquals(
+                            "URL should match",
+                            tab.getUrl().getSpec(),
+                            cachedTab.tabState.url.getSpec());
+                });
+    }
+
+    @Test
+    @MediumTest
+    public void testSaveAndRestoreIncognito() throws ExecutionException {
+        WebPageStation page = mActivityTestRule.startOnIncognitoBlankPage();
+        Tab tab = page.getTab();
+
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    mActiveTabCache.saveActiveTab(tab, 0, mCipherFactory);
+                });
+
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    CachedActiveTab cachedTab =
+                            mActiveTabCache.restoreActiveTab(true, mCipherFactory);
+                    assertNotNull("Cached incognito tab should not be null", cachedTab);
+                    assertEquals("Tab index should match", 0, cachedTab.tabIndex);
+                    assertNotNull("Tab state should not be null", cachedTab.tabState);
+                    assertEquals(
+                            "URL should match",
+                            tab.getUrl().getSpec(),
+                            cachedTab.tabState.url.getSpec());
+                });
+    }
+
+    @Test
+    @MediumTest
+    public void testReplaceActiveTab() throws ExecutionException {
+        WebPageStation page = mActivityTestRule.startOnBlankPage();
+        Tab tab = page.getTab();
+
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    mActiveTabCache.saveActiveTab(tab, 0, null);
+                });
+
+        CriteriaHelper.pollInstrumentationThread(
+                () -> getActiveTabFile(false).exists(), "Active tab file should exist");
+
+        // Verify first save
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    CachedActiveTab cachedTab = mActiveTabCache.restoreActiveTab(false, null);
+                    assertNotNull("Cached tab should not be null", cachedTab);
+                    assertEquals("Tab index should match", 0, cachedTab.tabIndex);
+                    assertEquals(
+                            "URL should match", "about:blank", cachedTab.tabState.url.getSpec());
+                });
+
+        // Navigate to a new URL
+        String newUrl = "chrome://version/";
+        page = page.loadWebPageProgrammatically(newUrl);
+        Tab newTab = page.getTab();
+
+        // Save again with new state and different index
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    mActiveTabCache.saveActiveTab(newTab, 1, null);
+                });
+
+        // Verify second save replaced the first
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    CachedActiveTab cachedTab = mActiveTabCache.restoreActiveTab(false, null);
+                    assertNotNull("Cached tab should not be null", cachedTab);
+                    assertEquals("Tab index should be updated", 1, cachedTab.tabIndex);
+                    assertEquals("URL should be updated", newUrl, cachedTab.tabState.url.getSpec());
+                });
+    }
+
+    @Test
+    @MediumTest
+    public void testClearActiveTab() throws ExecutionException {
+        WebPageStation page = mActivityTestRule.startOnBlankPage();
+        Tab tab = page.getTab();
+
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    mActiveTabCache.saveActiveTab(tab, 0, null);
+                });
+
+        CriteriaHelper.pollInstrumentationThread(
+                () -> getActiveTabFile(false).exists(), "Active tab file should exist");
+
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    mActiveTabCache.clearActiveTab(false);
+                    CachedActiveTab cachedTab = mActiveTabCache.restoreActiveTab(false, null);
+                    assertNull("Cached tab should be null after clear", cachedTab);
+                });
+    }
+}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/display_cutout/DisplayCutoutTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/display_cutout/DisplayCutoutTest.java
index 0274d9f..1e8df1f 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/display_cutout/DisplayCutoutTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/display_cutout/DisplayCutoutTest.java
@@ -39,7 +39,6 @@
 // test suite. TODO(crbug.com/377778493): To fix this test to work properly w/ EdgeToEdge.
 @Features.DisableFeatures({
     ChromeFeatureList.DRAW_CUTOUT_EDGE_TO_EDGE,
-    ChromeFeatureList.EDGE_TO_EDGE_BOTTOM_CHIN
 })
 @DisableIf.Device(DeviceFormFactor.DESKTOP) // https://crbug.com/376095153
 public class DisplayCutoutTest {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
index bf291116c..9e06730a 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
@@ -5,7 +5,6 @@
 package org.chromium.chrome.browser.media;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -13,7 +12,6 @@
 
 import android.content.Intent;
 import android.content.res.Configuration;
-import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
 import android.view.View;
@@ -23,12 +21,10 @@
 import androidx.test.runner.lifecycle.Stage;
 
 import org.junit.After;
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -43,7 +39,6 @@
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.content.WebContentsFactory;
 import org.chromium.chrome.browser.tab.Tab;
-import org.chromium.chrome.browser.util.PictureInPictureWindowOptions;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.chrome.test.transit.ChromeTransitTestRules;
 import org.chromium.chrome.test.transit.FreshCtaTransitTestRule;
@@ -214,46 +209,14 @@
         CriteriaHelper.pollUiThread(() -> newActivity.getWebContentsForTesting() == mWebContents);
     }
 
-    @Test
-    @MediumTest
-    public void testResizeContents() throws Exception {
-        Rect bounds = new Rect(0, 0, 50, 50);
-        PictureInPictureWindowOptions options = new PictureInPictureWindowOptions(bounds, false);
-        DocumentPictureInPictureActivity activity = launchActivity(options.toBundle());
-        CriteriaHelper.pollUiThread(() -> !activity.isFinishing());
-
-        ArgumentCaptor<Rect> captor = ArgumentCaptor.forClass(Rect.class);
-        verify(mAconfigMock).moveTaskTo(any(), anyInt(), captor.capture());
-        Rect capturedBounds = captor.getValue();
-        float density = activity.getResources().getDisplayMetrics().density;
-
-        // Calculate expected bounds change based on content resize
-        View content = activity.findViewById(R.id.document_picture_in_picture_content);
-        int currentContentWidth = content.getWidth();
-        int currentContentHeight = content.getHeight();
-        int expectedWidthChange = (int) (50 * density) - currentContentWidth;
-        int expectedHeightChange = (int) (50 * density) - currentContentHeight;
-
-        Rect initialWindowBounds =
-                activity.getWindowManager().getCurrentWindowMetrics().getBounds();
-
-        int expectedWidth = initialWindowBounds.width() + expectedWidthChange;
-        int expectedHeight = initialWindowBounds.height() + expectedHeightChange;
-
-        Assert.assertTrue(Math.abs(capturedBounds.width() - expectedWidth) <= Math.ceil(density));
-        Assert.assertTrue(Math.abs(capturedBounds.height() - expectedHeight) <= Math.ceil(density));
-    }
-
     private DocumentPictureInPictureActivity launchActivity() throws Exception {
-        return launchActivity(new Bundle());
-    }
-
-    private DocumentPictureInPictureActivity launchActivity(Bundle optionsBundle) throws Exception {
         Intent intent =
                 new Intent(
                         InstrumentationRegistry.getInstrumentation().getTargetContext(),
                         DocumentPictureInPictureActivity.class);
         // We set WebContents via static setter in setUp, so we don't need to put it in intent.
+        // But we do need window options.
+        Bundle optionsBundle = new Bundle();
         intent.putExtra(DocumentPictureInPictureActivity.WINDOW_OPTIONS_KEY, optionsBundle);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/ui/FullscreenVideoPictureInPictureControllerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/ui/FullscreenVideoPictureInPictureControllerTest.java
index 77b59f8..f381d09 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/media/ui/FullscreenVideoPictureInPictureControllerTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/ui/FullscreenVideoPictureInPictureControllerTest.java
@@ -9,6 +9,7 @@
 import androidx.test.filters.MediumTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import org.hamcrest.Matchers;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
@@ -18,11 +19,15 @@
 import org.chromium.base.ThreadUtils;
 import org.chromium.base.test.util.Batch;
 import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.base.test.util.Criteria;
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.DisableIf;
 import org.chromium.base.test.util.DisabledTest;
+import org.chromium.base.test.util.Features.DisableFeatures;
+import org.chromium.base.test.util.Features.EnableFeatures;
 import org.chromium.base.test.util.Restriction;
 import org.chromium.chrome.browser.ChromeTabbedActivity;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.init.AsyncInitializationActivity;
 import org.chromium.chrome.browser.tab.EmptyTabObserver;
@@ -51,10 +56,12 @@
     DeviceRestriction.RESTRICTION_TYPE_NON_AUTO // PiP not supported on AAOS.
 })
 @DisableIf.Device(DeviceFormFactor.DESKTOP) // https://crbug.com/481444525
+@EnableFeatures(ChromeFeatureList.FULLSCREEN_VIDEO_PICTURE_IN_PICTURE)
 public class FullscreenVideoPictureInPictureControllerTest {
     // TODO(peconn): Add a test for exit on Tab Reparenting.
     private static final String TEST_PATH = "/chrome/test/data/media/bigbuck-player.html";
     private static final String VIDEO_ID = "video";
+    private static final long PIP_TIMEOUT_MS = 10000L;
 
     @Rule
     public FreshCtaTransitTestRule mActivityTestRule =
@@ -102,6 +109,25 @@
                 AsyncInitializationActivity::wasMoveTaskToBackInterceptedForTesting);
     }
 
+    /** Tests that PiP is not entered when the feature is disabled. */
+    @Test
+    @MediumTest
+    @DisableFeatures(ChromeFeatureList.FULLSCREEN_VIDEO_PICTURE_IN_PICTURE)
+    public void testNoPipWhenDisabled() throws Throwable {
+        enterFullscreen();
+
+        ThreadUtils.runOnUiThreadBlocking(
+                () ->
+                        InstrumentationRegistry.getInstrumentation()
+                                .callActivityOnUserLeaving(mActivity));
+
+        // Wait a bit to ensure it doesn't enter PiP.
+        // If it was going to enter PiP, it would have happened already or shortly after
+        // callActivityOnUserLeaving.
+        Thread.sleep(1000);
+        Assert.assertFalse(ThreadUtils.runOnUiThreadBlocking(mActivity::isInPictureInPictureMode));
+    }
+
     /** Tests that PiP is left when we navigate the main page. */
     @Test
     @MediumTest
@@ -143,14 +169,14 @@
     /** Tests that PiP is left when a new Tab is created in the foreground. */
     @Test
     @MediumTest
-    @DisabledTest(message = "https://crbug.com/1429112")
     public void testExitOnNewForegroundTab() throws Throwable {
         testExitOn(
                 new Runnable() {
                     @Override
                     public void run() {
                         try {
-                            mActivityTestRule.loadUrlInNewTab("https://www.example.com/");
+                            mActivityTestRule.loadUrlInNewTab(
+                                    mActivityTestRule.getTestServer().getURL(TEST_PATH));
                         } catch (Exception e) {
                             throw new RuntimeException();
                         }
@@ -192,18 +218,66 @@
     /** Tests that we can resume PiP after it has been cancelled. */
     @Test
     @MediumTest
-    @DisabledTest(message = "https://crbug.com/1429112")
     public void testReenterPip() throws Throwable {
         enterFullscreen();
         triggerAutoPiPAndWait();
+        exitPipAndFullscreenAndWait();
 
         mActivityTestRule.resumeMainActivityFromLauncher();
-        CriteriaHelper.pollUiThread(() -> !mActivity.getLastPictureInPictureModeForTesting());
 
-        enterFullscreen(false);
+        // Open a new tab and wait for it to load.
+        mActivityTestRule.loadUrlInNewTab(mActivityTestRule.getTestServer().getURL(TEST_PATH));
+
+        // Wait for the new tab to be active and ready.
+        CriteriaHelper.pollUiThread(
+                () -> {
+                    Tab tab = mActivityTestRule.getActivityTab();
+                    return tab != null && tab.getWebContents() != null && !tab.isClosing();
+                },
+                "New tab should be active and ready",
+                PIP_TIMEOUT_MS,
+                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
+
+        enterFullscreen(true);
         triggerAutoPiPAndWait();
     }
 
+    private void exitPipAndFullscreenAndWait() throws Throwable {
+        AsyncInitializationActivity.interceptMoveTaskToBackForTesting();
+        JavaScriptUtils.executeJavaScript(getWebContents(), "document.exitFullscreen()");
+
+        CriteriaHelper.pollUiThread(
+                () -> !getWebContents().hasActiveEffectivelyFullscreenVideo(),
+                "Engine should not have fullscreen video",
+                PIP_TIMEOUT_MS,
+                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
+
+        final Tab tab = mActivityTestRule.getActivityTab();
+        CriteriaHelper.pollInstrumentationThread(
+                () -> {
+                    Criteria.checkThat(
+                            tab.getWebContents().getFullscreenVideoSize(), Matchers.nullValue());
+                },
+                PIP_TIMEOUT_MS,
+                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
+
+        CriteriaHelper.pollUiThread(
+                AsyncInitializationActivity::wasMoveTaskToBackInterceptedForTesting,
+                "Chrome should have attempted dismissal after exitFullscreen",
+                PIP_TIMEOUT_MS,
+                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
+
+        // Since we intercept moveTaskToBack, the framework won't automatically send the signal.
+        mActivity.onPictureInPictureModeChanged(false, mActivity.getResources().getConfiguration());
+
+        // Wait for Chrome to acknowledge PiP exit.
+        CriteriaHelper.pollUiThread(
+                () -> !mActivity.getLastPictureInPictureModeForTesting(),
+                "Chrome should have acknowledged PiP exit",
+                PIP_TIMEOUT_MS,
+                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
+    }
+
     private WebContents getWebContents() {
         return mActivityTestRule.getWebContents();
     }
@@ -213,7 +287,13 @@
                 () ->
                         InstrumentationRegistry.getInstrumentation()
                                 .callActivityOnUserLeaving(mActivity));
-        CriteriaHelper.pollUiThread(mActivity::getLastPictureInPictureModeForTesting);
+
+        // Wait for Chrome to process the callback.
+        CriteriaHelper.pollUiThread(
+                mActivity::getLastPictureInPictureModeForTesting,
+                "Chrome should have acknowledged PiP mode",
+                PIP_TIMEOUT_MS,
+                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
     }
 
     private void enterFullscreen() throws Throwable {
@@ -235,7 +315,20 @@
                         /* shouldScrollIntoView= */ false));
 
         // We use the web contents fullscreen heuristic.
-        CriteriaHelper.pollUiThread(getWebContents()::hasActiveEffectivelyFullscreenVideo);
+        CriteriaHelper.pollUiThread(
+                getWebContents()::hasActiveEffectivelyFullscreenVideo,
+                PIP_TIMEOUT_MS,
+                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
+
+        // It can take a while for the fullscreen video to register.
+        final Tab tab = mActivityTestRule.getActivityTab();
+        CriteriaHelper.pollInstrumentationThread(
+                () -> {
+                    Criteria.checkThat(
+                            tab.getWebContents().getFullscreenVideoSize(), Matchers.notNullValue());
+                },
+                PIP_TIMEOUT_MS,
+                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
     }
 
     private void testExitOn(Runnable runnable) throws Throwable {
@@ -247,7 +340,10 @@
         runnable.run();
 
         CriteriaHelper.pollUiThread(
-                AsyncInitializationActivity::wasMoveTaskToBackInterceptedForTesting);
+                AsyncInitializationActivity::wasMoveTaskToBackInterceptedForTesting,
+                "Failed to move task to the background.",
+                PIP_TIMEOUT_MS,
+                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
         // This logic would run if we hadn't intercepted moveTaskToBack (which is how PiP gets
         // exited), so run it now just in case.
         mActivity.onPictureInPictureModeChanged(false, mActivity.getResources().getConfiguration());
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tab/TabMediaIndicatorTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tab/TabMediaIndicatorTest.java
index 0f8c21a..e811dad 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/tab/TabMediaIndicatorTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tab/TabMediaIndicatorTest.java
@@ -67,7 +67,7 @@
 import java.util.List;
 import java.util.concurrent.TimeoutException;
 
-/** Tests for {@link Tab.MediaState}. */
+/** Tests for {@link MediaState}. */
 @RunWith(ChromeJUnit4ClassRunner.class)
 @CommandLineFlags.Add({
     ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
@@ -141,7 +141,7 @@
 
         new TabLoadObserver(mTab).fullyLoadUrl(mActivityTestRule.getTestServer().getURL(TEST_PATH));
         DOMUtils.waitForNonZeroNodeBounds(mTab.getWebContents(), VIDEO_ID);
-        assertEquals(Tab.MediaState.NONE, mTab.getMediaState());
+        assertEquals(MediaState.NONE, mTab.getMediaState());
 
         ForegroundServiceUtils.setInstanceForTesting(Mockito.mock(ForegroundServiceUtils.class));
 
@@ -162,7 +162,7 @@
     public void testMediaStateAudible() throws TimeoutException {
         DOMUtils.playMedia(mTab.getWebContents(), VIDEO_ID);
         DOMUtils.waitForMediaPlay(mTab.getWebContents(), VIDEO_ID);
-        waitForMediaState(mTab, Tab.MediaState.AUDIBLE);
+        waitForMediaState(mTab, MediaState.AUDIBLE);
     }
 
     @Test
@@ -171,7 +171,7 @@
         setMuteState(true);
         DOMUtils.playMedia(mTab.getWebContents(), VIDEO_ID);
         DOMUtils.waitForMediaPlay(mTab.getWebContents(), VIDEO_ID);
-        waitForMediaState(mTab, Tab.MediaState.MUTED);
+        waitForMediaState(mTab, MediaState.MUTED);
     }
 
     @Test
@@ -180,9 +180,9 @@
         setMuteState(true);
         DOMUtils.playMedia(mTab.getWebContents(), VIDEO_ID);
         DOMUtils.waitForMediaPlay(mTab.getWebContents(), VIDEO_ID);
-        waitForMediaState(mTab, Tab.MediaState.MUTED);
+        waitForMediaState(mTab, MediaState.MUTED);
         setMuteState(false);
-        waitForMediaState(mTab, Tab.MediaState.AUDIBLE);
+        waitForMediaState(mTab, MediaState.AUDIBLE);
     }
 
     @Test
@@ -190,9 +190,9 @@
     public void testMediaStateAudibleThenMute() throws TimeoutException {
         DOMUtils.playMedia(mTab.getWebContents(), VIDEO_ID);
         DOMUtils.waitForMediaPlay(mTab.getWebContents(), VIDEO_ID);
-        waitForMediaState(mTab, Tab.MediaState.AUDIBLE);
+        waitForMediaState(mTab, MediaState.AUDIBLE);
         setMuteState(true);
-        waitForMediaState(mTab, Tab.MediaState.MUTED);
+        waitForMediaState(mTab, MediaState.MUTED);
     }
 
     @Test
@@ -200,23 +200,23 @@
     public void testMediaStateAudibleMuteWithPause() throws Exception {
         DOMUtils.playMedia(mTab.getWebContents(), VIDEO_ID);
         DOMUtils.waitForMediaPlay(mTab.getWebContents(), VIDEO_ID);
-        waitForMediaState(mTab, Tab.MediaState.AUDIBLE);
+        waitForMediaState(mTab, MediaState.AUDIBLE);
 
         // Pause video.
         DOMUtils.pauseMedia(mTab.getWebContents(), VIDEO_ID);
         DOMUtils.waitForMediaPauseBeforeEnd(mTab.getWebContents(), VIDEO_ID);
 
         // Wait for the recently audible state to clear.
-        waitForMediaState(mTab, Tab.MediaState.NONE);
+        waitForMediaState(mTab, MediaState.NONE);
 
         // Mute video.
         setMuteState(true);
-        assertEquals(Tab.MediaState.NONE, mTab.getMediaState());
+        assertEquals(MediaState.NONE, mTab.getMediaState());
 
         // Play the video again.
         DOMUtils.playMedia(mTab.getWebContents(), VIDEO_ID);
         DOMUtils.waitForMediaPlay(mTab.getWebContents(), VIDEO_ID);
-        waitForMediaState(mTab, Tab.MediaState.MUTED);
+        waitForMediaState(mTab, MediaState.MUTED);
     }
 
     @Test
@@ -224,73 +224,72 @@
     public void testMediaStateWithVideoMutedAndUnmuted() throws Exception {
         DOMUtils.playMedia(mTab.getWebContents(), VIDEO_ID);
         DOMUtils.waitForMediaPlay(mTab.getWebContents(), VIDEO_ID);
-        waitForMediaState(mTab, Tab.MediaState.AUDIBLE);
+        waitForMediaState(mTab, MediaState.AUDIBLE);
 
         // Mute video element.
         DOMUtils.clickNodeWithJavaScript(mTab.getWebContents(), MUTE_VIDEO_ID);
 
         // Wait for the recently audible state to clear.
         assertFalse(DOMUtils.isMediaPaused(mTab.getWebContents(), VIDEO_ID));
-        waitForMediaState(mTab, Tab.MediaState.NONE);
+        waitForMediaState(mTab, MediaState.NONE);
 
         // Unmute video element.
         DOMUtils.clickNodeWithJavaScript(mTab.getWebContents(), UNMUTE_VIDEO_ID);
-        waitForMediaState(mTab, Tab.MediaState.AUDIBLE);
+        waitForMediaState(mTab, MediaState.AUDIBLE);
     }
 
     @Test
     @SmallTest
     public void testMediaStateRecordingMic() throws InterruptedException {
         requestRecording(REQUEST_MIC_ID);
-        waitForMediaState(mTab, Tab.MediaState.RECORDING);
+        waitForMediaState(mTab, MediaState.RECORDING);
     }
 
     @Test
     @SmallTest
     public void testMediaStateRecordingCam() throws InterruptedException {
         requestRecording(REQUEST_CAM_ID);
-        waitForMediaState(mTab, Tab.MediaState.RECORDING);
+        waitForMediaState(mTab, MediaState.RECORDING);
     }
 
     @Test
     @SmallTest
     public void testMediaStatePriority() throws Exception {
-        assertEquals(Tab.MediaState.NONE, mTab.getMediaState());
+        assertEquals(MediaState.NONE, mTab.getMediaState());
 
         // MUTED
         setMuteState(true);
         DOMUtils.playMedia(mTab.getWebContents(), VIDEO_ID);
         DOMUtils.waitForMediaPlay(mTab.getWebContents(), VIDEO_ID);
-        waitForMediaState(mTab, Tab.MediaState.MUTED);
+        waitForMediaState(mTab, MediaState.MUTED);
 
         // AUDIBLE
         setMuteState(false);
-        waitForMediaState(mTab, Tab.MediaState.AUDIBLE);
+        waitForMediaState(mTab, MediaState.AUDIBLE);
 
         // RECORDING
         requestRecording(REQUEST_MIC_ID);
-        waitForMediaState(mTab, Tab.MediaState.RECORDING);
+        waitForMediaState(mTab, MediaState.RECORDING);
     }
 
     @Test
     @SmallTest
     @Restriction(DeviceFormFactor.DESKTOP)
     public void testMediaStateSharing() throws InterruptedException {
-        assertEquals(Tab.MediaState.NONE, mTab.getMediaState());
+        assertEquals(MediaState.NONE, mTab.getMediaState());
 
         // Expect SHARING
         HistogramWatcher watcher =
                 HistogramWatcher.newSingleRecordWatcher(
-                        "Tab.Android.MediaState", Tab.MediaState.SHARING);
+                        "Tab.Android.MediaState", MediaState.SHARING);
         startTabCapture(mTab, mTab);
         watcher.assertExpected();
 
         // Expect NONE
         watcher =
-                HistogramWatcher.newSingleRecordWatcher(
-                        "Tab.Android.MediaState", Tab.MediaState.NONE);
+                HistogramWatcher.newSingleRecordWatcher("Tab.Android.MediaState", MediaState.NONE);
         stopTabCapture(mTab);
-        waitForMediaState(mTab, Tab.MediaState.NONE);
+        waitForMediaState(mTab, MediaState.NONE);
         watcher.assertExpected();
     }
 
@@ -299,11 +298,11 @@
     @Restriction(DeviceFormFactor.DESKTOP)
     public void testMediaStateSharingOverridesRecording() throws Exception {
         requestRecording(REQUEST_MIC_ID);
-        waitForMediaState(mTab, Tab.MediaState.RECORDING);
+        waitForMediaState(mTab, MediaState.RECORDING);
 
         startTabCapture(mTab, mTab);
         stopTabCapture(mTab);
-        waitForMediaState(mTab, Tab.MediaState.RECORDING);
+        waitForMediaState(mTab, MediaState.RECORDING);
     }
 
     @Test
@@ -322,7 +321,7 @@
         selectTab(mTab);
         startTabCapture(mTab, newTab);
         stopTabCapture(mTab);
-        waitForMediaState(newTab, Tab.MediaState.NONE);
+        waitForMediaState(newTab, MediaState.NONE);
     }
 
     @Test
@@ -342,7 +341,7 @@
         startTabCapture(mTab, newTab);
 
         closeTab(mTab);
-        waitForMediaState(newTab, Tab.MediaState.NONE);
+        waitForMediaState(newTab, MediaState.NONE);
     }
 
     @Test
@@ -380,12 +379,12 @@
         selectTab(capturer1Tab);
         stopTabCapture(capturer1Tab);
         // The media state should persist as the second capturer is still active.
-        assertEquals(Tab.MediaState.SHARING, captureeTab.getMediaState());
+        assertEquals(MediaState.SHARING, captureeTab.getMediaState());
 
         // Stop capture from the second tab and verify the indicator is gone.
         selectTab(capturer2Tab);
         stopTabCapture(capturer2Tab);
-        waitForMediaState(captureeTab, Tab.MediaState.NONE);
+        waitForMediaState(captureeTab, MediaState.NONE);
     }
 
     @Test
@@ -415,49 +414,47 @@
 
         // After capturee is closed, the sharing should stop.
         waitForTitle(mTab, "ended");
-        waitForMediaState(captureeTab, Tab.MediaState.NONE);
+        waitForMediaState(captureeTab, MediaState.NONE);
     }
 
     @Test
     @SmallTest
     public void testMediaStateHistogram() throws Exception {
-        assertEquals(Tab.MediaState.NONE, mTab.getMediaState());
+        assertEquals(MediaState.NONE, mTab.getMediaState());
 
         // Expect AUDIBLE
         HistogramWatcher watcher =
                 HistogramWatcher.newSingleRecordWatcher(
-                        "Tab.Android.MediaState", Tab.MediaState.AUDIBLE);
+                        "Tab.Android.MediaState", MediaState.AUDIBLE);
         DOMUtils.playMedia(mTab.getWebContents(), VIDEO_ID);
         DOMUtils.waitForMediaPlay(mTab.getWebContents(), VIDEO_ID);
-        waitForMediaState(mTab, Tab.MediaState.AUDIBLE);
+        waitForMediaState(mTab, MediaState.AUDIBLE);
         watcher.assertExpected();
 
         // Expect MUTED
         watcher =
-                HistogramWatcher.newSingleRecordWatcher(
-                        "Tab.Android.MediaState", Tab.MediaState.MUTED);
+                HistogramWatcher.newSingleRecordWatcher("Tab.Android.MediaState", MediaState.MUTED);
         setMuteState(true);
-        waitForMediaState(mTab, Tab.MediaState.MUTED);
+        waitForMediaState(mTab, MediaState.MUTED);
         watcher.assertExpected();
 
         // Expect NONE
         watcher =
-                HistogramWatcher.newSingleRecordWatcher(
-                        "Tab.Android.MediaState", Tab.MediaState.NONE);
+                HistogramWatcher.newSingleRecordWatcher("Tab.Android.MediaState", MediaState.NONE);
         // Pause video.
         DOMUtils.pauseMedia(mTab.getWebContents(), VIDEO_ID);
         DOMUtils.waitForMediaPauseBeforeEnd(mTab.getWebContents(), VIDEO_ID);
 
         // Wait for the recently audible state to clear.
-        waitForMediaState(mTab, Tab.MediaState.NONE);
+        waitForMediaState(mTab, MediaState.NONE);
         watcher.assertExpected();
 
         // Expect RECORDING
         watcher =
                 HistogramWatcher.newSingleRecordWatcher(
-                        "Tab.Android.MediaState", Tab.MediaState.RECORDING);
+                        "Tab.Android.MediaState", MediaState.RECORDING);
         requestRecording(REQUEST_MIC_ID);
-        waitForMediaState(mTab, Tab.MediaState.RECORDING);
+        waitForMediaState(mTab, MediaState.RECORDING);
         watcher.assertExpected();
     }
 
@@ -510,7 +507,7 @@
                             Matchers.is(true));
                 });
         waitForTitle(capturer, "stream_ready");
-        waitForMediaState(capturee, Tab.MediaState.SHARING);
+        waitForMediaState(capturee, MediaState.SHARING);
     }
 
     private void stopTabCapture(Tab capturer) {
@@ -518,7 +515,7 @@
         waitForTitle(capturer, "stopped_successfully");
     }
 
-    private void waitForMediaState(Tab tab, @Tab.MediaState int expectedState) {
+    private void waitForMediaState(Tab tab, @MediaState int expectedState) {
         CriteriaHelper.pollUiThread(
                 () -> {
                     Criteria.checkThat(
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tabbed_mode/TabbedNavigationBarColorControllerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tabbed_mode/TabbedNavigationBarColorControllerTest.java
index 6874a56e..aac57d7 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/tabbed_mode/TabbedNavigationBarColorControllerTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tabbed_mode/TabbedNavigationBarColorControllerTest.java
@@ -46,6 +46,7 @@
 import org.chromium.base.test.util.Features;
 import org.chromium.base.test.util.Features.DisableFeatures;
 import org.chromium.base.test.util.Features.EnableFeatures;
+import org.chromium.base.test.util.MaxAndroidSdkLevel;
 import org.chromium.base.test.util.MinAndroidSdkLevel;
 import org.chromium.base.test.util.Restriction;
 import org.chromium.base.test.util.TestAnimations.EnableAnimations;
@@ -125,7 +126,6 @@
     @Test
     @SmallTest
     @DisabledTest(message = "crbug.com/419391905")
-    @DisableFeatures(ChromeFeatureList.EDGE_TO_EDGE_BOTTOM_CHIN)
     public void testToggleOverview() {
         assertEquals(
                 "Navigation bar should match the tab background before entering overview mode.",
@@ -151,39 +151,38 @@
 
     @Test
     @SmallTest
-    @DisableFeatures(ChromeFeatureList.EDGE_TO_EDGE_BOTTOM_CHIN)
     // TODO(crbug.com/428056054): Do not read color from system window bars on B+.
     @DisableIf.Build(
             sdk_is_greater_than = Build.VERSION_CODES.VANILLA_ICE_CREAM,
             message = "crbug.com/428056054")
-    public void testToggleIncognito() {
-        assertEquals(
-                "Navigation bar should match the tab background on normal tabs.",
-                mActivityTestRule.getActivityTab().getBackgroundColor(),
-                mWindow.getNavigationBarColor());
-
+    public void testToggleIncognitoLegacy() {
         IncognitoNewTabPageStation incognitoNtp = mPage.openNewIncognitoTabOrWindowFast();
 
-        assertEquals(
-                "Navigation bar should be dark_elev_3 on incognito tabs.",
-                mDarkNavigationColor,
-                incognitoNtp.getActivity().getWindow().getNavigationBarColor());
+        assertWindowNavBarIsTransparentOrMatchesColor(
+                incognitoNtp.getActivity().getWindow(), mDarkNavigationColor);
 
         if (!incognitoNtp.getActivity().isIncognitoWindow()) {
             RegularNewTabPageStation regularNtp = incognitoNtp.openNewTabOrWindowFast();
-
-            assertEquals(
-                    "Navigation bar should match the tab background after switching back to normal"
-                            + " tab.",
-                    mActivityTestRule.getActivityTab().getBackgroundColor(),
-                    regularNtp.getActivity().getWindow().getNavigationBarColor());
+            assertWindowNavBarIsTransparentOrMatchesColor(
+                    regularNtp.getActivity().getWindow(),
+                    mActivityTestRule.getActivityTab().getBackgroundColor());
         }
     }
 
+    // By default, the window navbar will match the tab's background color. However, when drawing
+    // edge-to-edge, the window navbar will be transparent, and allow tab contents to display
+    // directly. Either is acceptable, and depends on device navbar settings (3-button vs gesture)
+    // that are not explicitly exposed by Android.
+    private static void assertWindowNavBarIsTransparentOrMatchesColor(
+            Window window, @ColorInt int color) {
+        boolean hasTransparentNavBar = window.getNavigationBarColor() == Color.TRANSPARENT;
+        boolean navBarColorMatchesColor = window.getNavigationBarColor() == color;
+        assertTrue(hasTransparentNavBar || navBarColorMatchesColor);
+    }
+
     @Test
     @MediumTest
     @DisabledTest(message = "crbug.com/1381509")
-    @DisableFeatures(ChromeFeatureList.EDGE_TO_EDGE_BOTTOM_CHIN)
     public void testToggleFullscreen() throws TimeoutException {
         assertEquals(
                 "Navigation bar should be colorSurface before entering fullscreen mode.",
@@ -217,12 +216,8 @@
 
     @Test
     @MediumTest
-    @DisableFeatures(ChromeFeatureList.EDGE_TO_EDGE_BOTTOM_CHIN)
-    // TODO(crbug.com/428056054): Do not read color from system window bars on B+.
-    @DisableIf.Build(
-            sdk_is_greater_than = Build.VERSION_CODES.VANILLA_ICE_CREAM,
-            message = "crbug.com/428056054")
-    public void testSetNavigationBarScrimFraction() {
+    @MaxAndroidSdkLevel(29)
+    public void testSetNavigationBarScrimFractionPreEdgeToEdge() {
         assertEquals(
                 "Navigation bar should match the tab background on normal tabs.",
                 mActivityTestRule.getActivityTab().getBackgroundColor(),
@@ -264,7 +259,6 @@
     @MediumTest
     @EnableFeatures({
         ChromeFeatureList.NAV_BAR_COLOR_ANIMATION,
-        ChromeFeatureList.EDGE_TO_EDGE_BOTTOM_CHIN
     })
     @DisableFeatures(ChromeFeatureList.EDGE_TO_EDGE_EVERYWHERE)
     @EnableAnimations
@@ -283,7 +277,6 @@
         ChromeFeatureList.NAV_BAR_COLOR_ANIMATION,
         ChromeFeatureList.EDGE_TO_EDGE_EVERYWHERE
     })
-    @DisableFeatures(ChromeFeatureList.EDGE_TO_EDGE_BOTTOM_CHIN)
     @EnableAnimations
     @Restriction({DeviceFormFactor.PHONE, DeviceRestriction.RESTRICTION_TYPE_NON_AUTO})
     @MinAndroidSdkLevel(Build.VERSION_CODES.R)
@@ -294,7 +287,6 @@
     // Disable the dedicated feature flag.
     @Test
     @SmallTest
-    @EnableFeatures({ChromeFeatureList.EDGE_TO_EDGE_BOTTOM_CHIN})
     @DisableFeatures(ChromeFeatureList.NAV_BAR_COLOR_ANIMATION)
     public void testNavBarColorAnimationsFeatureFlagDisabled() {
         Assume.assumeTrue(
diff --git a/chrome/android/junit/BUILD.gn b/chrome/android/junit/BUILD.gn
index 69813ef..6dc39ae0 100644
--- a/chrome/android/junit/BUILD.gn
+++ b/chrome/android/junit/BUILD.gn
@@ -1315,6 +1315,7 @@
       "//base:holder_java",
       "//chrome/android:chrome_java",
       "//chrome/browser/settings:search_java",
+      "//chrome/browser/tab:java",
       "//chrome/browser/tab_group_suggestion:java",
       "//chrome/test/android:chrome_java_transit",
       "//components/sensitive_content:sensitive_content_features_java",
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/ChromeActivityUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/ChromeActivityUnitTest.java
index ce4f5a7..fc27240 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/ChromeActivityUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/ChromeActivityUnitTest.java
@@ -24,6 +24,8 @@
 import android.view.ViewGroup;
 import android.window.OnBackInvokedDispatcher;
 
+import androidx.annotation.Nullable;
+
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.junit.Assert;
@@ -146,8 +148,12 @@
         }
 
         @Override
-        protected FullscreenVideoPictureInPictureController
+        protected @Nullable FullscreenVideoPictureInPictureController
                 ensureFullscreenVideoPictureInPictureController() {
+            if (!ChromeFeatureList.isEnabled(
+                    ChromeFeatureList.FULLSCREEN_VIDEO_PICTURE_IN_PICTURE)) {
+                return null;
+            }
             return mFullscreenVideoPictureInPictureController;
         }
 
@@ -207,6 +213,7 @@
 
     @Test
     @Config(sdk = 31)
+    @EnableFeatures(ChromeFeatureList.FULLSCREEN_VIDEO_PICTURE_IN_PICTURE)
     public void testPictureInPictureStashing() {
         // Verify that ChromeActivity reports `isStashed` correctly to the controller.
         TestChromeActivity chromeActivity = Mockito.spy(new TestChromeActivity());
@@ -223,6 +230,19 @@
     }
 
     @Test
+    @Config(sdk = 31)
+    @DisableFeatures(ChromeFeatureList.FULLSCREEN_VIDEO_PICTURE_IN_PICTURE)
+    public void testPictureInPictureStashing_Disabled() {
+        // Verify that ChromeActivity does not report `isStashed` when the feature is disabled.
+        TestChromeActivity chromeActivity = Mockito.spy(new TestChromeActivity());
+
+        when(mPictureInPictureUiState.isStashed()).thenReturn(true);
+        chromeActivity.onPictureInPictureUiStateChanged(mPictureInPictureUiState);
+        Mockito.verify(mFullscreenVideoPictureInPictureController, Mockito.never())
+                .onStashReported(Mockito.anyBoolean());
+    }
+
+    @Test
     @EnableFeatures({ChromeFeatureList.PAGE_CONTENT_PROVIDER})
     public void testPageContentStructuredData() throws JSONException {
         TestChromeActivity chromeActivity = Mockito.spy(new TestChromeActivity());
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelperTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelperTest.java
index ae20d3d4..a47427f6 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelperTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelperTest.java
@@ -117,9 +117,9 @@
 import org.chromium.chrome.browser.multiwindow.MultiWindowTestUtils;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.share.ShareDelegate;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.tab.MockTab;
 import org.chromium.chrome.browser.tab.Tab;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
 import org.chromium.chrome.browser.tab.TabCreationState;
 import org.chromium.chrome.browser.tab.TabLaunchType;
 import org.chromium.chrome.browser.tab.TabSelectionType;
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutTabTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutTabTest.java
index c81090b..6b42e70 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutTabTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutTabTest.java
@@ -27,7 +27,7 @@
 import org.chromium.base.test.BaseRobolectricTestRunner;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.compositor.overlays.strip.StripLayoutTabDelegate.VisualState;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.ui.theme.ChromeSemanticColorUtils;
 import org.chromium.components.browser_ui.styles.ChromeColors;
 import org.chromium.components.browser_ui.styles.SemanticColorUtils;
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/TabContextMenuCoordinatorUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/TabContextMenuCoordinatorUnitTest.java
index c53be8a..ae06339 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/TabContextMenuCoordinatorUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/TabContextMenuCoordinatorUnitTest.java
@@ -11,6 +11,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -78,6 +79,7 @@
 import org.chromium.chrome.browser.tabmodel.TabClosureParams;
 import org.chromium.chrome.browser.tabmodel.TabCreator;
 import org.chromium.chrome.browser.tabmodel.TabGroupModelFilter;
+import org.chromium.chrome.browser.tabmodel.TabGroupModelFilter.MergeNotificationType;
 import org.chromium.chrome.browser.tabmodel.TabGroupUtils.TabGroupCreationCallback;
 import org.chromium.chrome.browser.tabmodel.TabList;
 import org.chromium.chrome.browser.tabmodel.TabModel;
@@ -308,6 +310,7 @@
                         () -> mTabModel,
                         mTabGroupModelFilter,
                         mBottomSheetCoordinator,
+                        mTabGroupCreationCallback,
                         mMultiInstanceManager,
                         ObservableSuppliers.createMonotonic(mShareDelegate));
         mTabContextMenuCoordinator =
@@ -326,7 +329,36 @@
     @Test
     @Feature("Tab Strip Context Menu")
     @EnableFeatures(ChromeFeatureList.SUBMENUS_TAB_CONTEXT_MENU_LFF_TAB_STRIP)
-    @SuppressWarnings("DirectInvocationOnMock")
+    public void testAddToNewTabGroup() {
+        mOnItemClickedCallback.onClick(
+                R.id.add_to_new_tab_group,
+                new AnchorInfo(TAB_ID, Collections.singletonList(TAB_ID)),
+                COLLABORATION_ID,
+                /* listViewTouchTracker= */ null);
+        verify(mTabGroupModelFilter, times(1)).createSingleTabGroup(mTab1);
+        verify(mTabGroupCreationCallback, times(1)).onTabGroupCreated(TAB_GROUP_ID);
+    }
+
+    @Test
+    @Feature("Tab Strip Context Menu")
+    @EnableFeatures(ChromeFeatureList.SUBMENUS_TAB_CONTEXT_MENU_LFF_TAB_STRIP)
+    public void testAddToNewTabGroup_multipleTabs() {
+        mOnItemClickedCallback.onClick(
+                R.id.add_to_new_tab_group,
+                new AnchorInfo(TAB_ID, List.of(TAB_ID, TAB_ID_2)),
+                COLLABORATION_ID,
+                /* listViewTouchTracker= */ null);
+        verify(mTabGroupModelFilter, times(1))
+                .mergeListOfTabsToGroup(
+                        eq(List.of(mTab1, mTab2)),
+                        eq(mTab1),
+                        eq(MergeNotificationType.NOTIFY_IF_NOT_NEW_GROUP));
+        verify(mTabGroupCreationCallback, times(1)).onTabGroupCreated(TAB_GROUP_ID);
+    }
+
+    @Test
+    @Feature("Tab Strip Context Menu")
+    @EnableFeatures(ChromeFeatureList.SUBMENUS_TAB_CONTEXT_MENU_LFF_TAB_STRIP)
     public void testListMenuItems_tabInGroup() {
         var modelList = new ModelList();
         mTabContextMenuCoordinator.configureMenuItemsForTesting(
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/TabStripIphControllerUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/TabStripIphControllerUnitTest.java
index f82dffc..96333b1 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/TabStripIphControllerUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/TabStripIphControllerUnitTest.java
@@ -35,7 +35,7 @@
 import org.chromium.chrome.browser.compositor.overlays.strip.TabLoadTracker.TabLoadTrackerCallback;
 import org.chromium.chrome.browser.compositor.overlays.strip.TabStripIphController.IphType;
 import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.user_education.IphCommand;
 import org.chromium.chrome.browser.user_education.UserEducationHelper;
 import org.chromium.components.feature_engagement.FeatureConstants;
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/reorder/ReorderStrategyTestBase.java b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/reorder/ReorderStrategyTestBase.java
index 1e2bafb..859c96a 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/reorder/ReorderStrategyTestBase.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/overlays/strip/reorder/ReorderStrategyTestBase.java
@@ -35,8 +35,8 @@
 import org.chromium.chrome.browser.compositor.overlays.strip.reorder.ReorderDelegate.StripUpdateDelegate;
 import org.chromium.chrome.browser.layouts.animation.CompositorAnimationHandler;
 import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.chrome.browser.tab.Tab;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
 import org.chromium.chrome.browser.tab.TabSelectionType;
 import org.chromium.chrome.browser.tab_ui.ActionConfirmationManager;
 import org.chromium.chrome.browser.tabmodel.TabGroupModelFilter;
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/scene_layer/TabStripSceneLayerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/scene_layer/TabStripSceneLayerTest.java
index 6433a11..747b4f9 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/scene_layer/TabStripSceneLayerTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/scene_layer/TabStripSceneLayerTest.java
@@ -50,7 +50,7 @@
 import org.chromium.chrome.browser.compositor.overlays.strip.StripLayoutView.StripLayoutViewOnKeyboardFocusHandler;
 import org.chromium.chrome.browser.compositor.overlays.strip.TabLoadTracker.TabLoadTrackerCallback;
 import org.chromium.chrome.browser.layouts.scene_layer.SceneLayer;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
+import org.chromium.chrome.browser.tab.MediaState;
 import org.chromium.ui.resources.ResourceManager;
 
 /** Tests for {@link TabStripSceneLayer}. */
@@ -604,7 +604,7 @@
                 .updateGlicButton(
                         eq(1L),
                         anyInt(),
-                        anyInt(),
+                        anyFloat(),
                         anyFloat(),
                         anyFloat(),
                         anyFloat(),
@@ -620,6 +620,7 @@
                                         mContext, R.attr.colorPrimary, /* defaultValue= */ 0)),
                         anyInt(),
                         anyFloat(),
+                        anyFloat(),
                         anyFloat());
     }
 
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedAdaptiveToolbarBehaviorTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedAdaptiveToolbarBehaviorTest.java
index d7bce9b..e77296a0 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedAdaptiveToolbarBehaviorTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedAdaptiveToolbarBehaviorTest.java
@@ -29,7 +29,7 @@
         Activity activity = Robolectric.setupActivity(Activity.class);
         mBehavior =
                 new TabbedAdaptiveToolbarBehavior(
-                        activity, null, null, null, null, null, null, null, null, null, null);
+                        activity, null, null, null, null, null, null, null, null, null, null, null);
     }
 
     @Test
diff --git a/chrome/android/profiles/newest.txt b/chrome/android/profiles/newest.txt
index ba052ac..5fba1e6 100644
--- a/chrome/android/profiles/newest.txt
+++ b/chrome/android/profiles/newest.txt
@@ -1 +1 @@
-chromeos-chrome-amd64-147.0.7710.0_pre1591974_rc-r1-merged.afdo.bz2
+chromeos-chrome-amd64-147.0.7715.0_pre1592720_rc-r1-merged.afdo.bz2
diff --git a/chrome/app/chrome_main_delegate.cc b/chrome/app/chrome_main_delegate.cc
index 61fd6fb..1fd05b2 100644
--- a/chrome/app/chrome_main_delegate.cc
+++ b/chrome/app/chrome_main_delegate.cc
@@ -1152,7 +1152,11 @@
       chrome::IsIsolationEnabled(command_line)) {
     const auto isolated_process = chrome::IsolatedBrowser::Launch(command_line);
     if (isolated_process.has_value()) {
-      return isolated_process.value()->WaitForExit();
+      const auto exit_code = isolated_process->WaitForExit();
+      if (exit_code) {
+        return *exit_code;
+      }
+      return CHROME_RESULT_CODE_INVALID_ISOLATED_BROWSER_PROCESS;
     }
   }
 
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 3aa5430b..feb36a5 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -8807,6 +8807,22 @@
       <message name="IDS_COMPOSE_CREATE_IMAGE_PLACEHOLDER" desc="Placeholder text for the composebox when in create image mode.">
         Describe your image
       </message>
+      <message name="IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THESE"
+        desc="Placeholder text for the composebox when there are multiple files in the carousel.">
+        Ask about these
+      </message>
+      <message name="IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_IMAGE"
+        desc="Placeholder text for the composebox when there is exactly one image in the carousel.">
+        Ask about this image
+      </message>
+      <message name="IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_TAB"
+        desc="Placeholder text for the composebox when there is exactly one tab in the carousel.">
+        Ask about this tab
+      </message>
+      <message name="IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_DOC"
+        desc="Placeholder text for the composebox when there is exactly one document (PDF) in the carousel.">
+        Ask about this doc
+      </message>
       <message name="IDS_NTP_ACTION_CHIP_TAB_HEADING_1" translateable="true" desc="Heading of the tab action chip on the NTP." >
         Ask about previous tab
       </message>
@@ -10234,54 +10250,6 @@
       </message>
 
       <!-- Strings for tab declutter -->
-      <message name="IDS_DECLUTTER_SELECTOR_HEADING_NO_DEDUPE" desc="Heading for the declutter button in the tab organization selector when dedupe is disabled.">
-        {NUM_TABS, plural,
-          =0 {No inactive tabs}
-          =1 {Review 1 inactive tab}
-          other {Review # inactive tabs}}
-      </message>
-      <message name="IDS_DECLUTTER_SELECTOR_HEADING" desc="Heading for the declutter button in the tab organization selector.">
-        {NUM_TABS, plural,
-          =0 {No unused tabs}
-          =1 {Review 1 unused tab}
-          other {Review # unused tabs}}
-      </message>
-      <message name="IDS_DECLUTTER_SELECTOR_SUBHEADING" desc="Subheading for the declutter button in the tab organization selector.">
-        Keep your tabs clutter-free
-      </message>
-      <message name="IDS_DECLUTTER_TITLE" desc="The header text for the declutter UI">
-        Clean up unused tabs
-      </message>
-      <message name="IDS_DECLUTTER_INACTIVE_TITLE_NO_DEDUPE" desc="The header text for the declutter inactive tab UI when dedupe is disabled">
-        Review inactive tabs
-      </message>
-      <message name="IDS_DECLUTTER_INACTIVE_TITLE" desc="The header text for the declutter inactive tab UI">
-        Inactive tabs
-      </message>
-      <message name="IDS_DECLUTTER_INACTIVE_BODY" desc="The body text for the declutter inactive tab UI">
-        Tabs you haven't used in <ph name="COUNT">$1<ex>7</ex></ph> or more days
-      </message>
-      <message name="IDS_DECLUTTER_DUPLICATE_TITLE" desc="The header text for the declutter duplicate tab UI">
-        Duplicate tabs
-      </message>
-      <message name="IDS_DECLUTTER_DUPLICATE_BODY" desc="The body text for the declutter duplicate tab UI">
-        Oldest copy kept when you close duplicates
-      </message>
-      <message name="IDS_DECLUTTER_EMPTY_TITLE" desc="The header text for the declutter empty state UI">
-        Things look neat!
-      </message>
-      <message name="IDS_DECLUTTER_EMPTY_BODY_NO_DEDUPE" desc="The body text for the declutter empty state UI when dedupe is disabled">
-        No inactive tabs right now
-      </message>
-      <message name="IDS_DECLUTTER_EMPTY_BODY" desc="The body text for the declutter empty state UI">
-        No unused tabs right now
-      </message>
-      <message name="IDS_DUPLICATE_ITEM_TITLE_SINGLE" desc="The title for an item in the duplicate tabs list with 1 duplicate">
-        <ph name="URL">$1<ex>google.com</ex></ph> • 1 duplicate
-      </message>
-      <message name="IDS_DUPLICATE_ITEM_TITLE_MULTI" desc="The title for an item in the duplicate tabs list with multiple duplicates">
-        <ph name="URL">$1<ex>google.com</ex></ph> • <ph name="DUPLICATE_COUNT">$2<ex>2</ex></ph> duplicates
-      </message>
       <message name="IDS_TOOLTIP_TAB_DECLUTTER_NO_DEDUPE" desc="The tooltip for the Tab Declutter button when dedupe is disabled.">
         Review inactive tabs?
       </message>
@@ -10300,12 +10268,6 @@
       <message name="IDS_ACCNAME_TAB_DECLUTTER" desc="The accessible name for the Tab Declutter button." is_accessibility_with_no_ui="true">
         Clean up unused tabs
       </message>
-      <message name="IDS_DECLUTTER_TIMESTAMP" desc="The description of when a tab was last accessed, for use in a declutter row.">
-        {NUM_DAYS, plural,
-          =0 {Visited 0 days ago}
-          =1 {Visited 1 day ago}
-          other {Visited # days ago}}
-      </message>
       <if expr="use_titlecase">
         <then>
           <message name="IDS_DECLUTTER_MENU_NO_DEDUPE" desc="In Title Case: The text label for the declutter app menu item when dedupe is disabled.">
@@ -10324,18 +10286,6 @@
           </message>
         </else>
       </if>
-      <message name="IDS_DECLUTTER_CLOSE_TABS" desc="The text label of the action button in the Tab Declutter UI.">
-        Close all
-      </message>
-      <message name="IDS_DECLUTTER_CLOSE_TAB_ARIA_LABEL" desc="The accessible label declutter for the button which excludes a tab from the list" is_accessibility_with_no_ui="true">
-        Remove <ph name="TAB_TITLE">$1<ex>New Tab</ex></ph>, <ph name="LAST_ACTIVE">$2<ex>Visited 9 days ago</ex></ph> from list
-      </message>
-      <message name="IDS_DECLUTTER_CLOSE_TAB_TOOLTIP" desc="The declutter tooltip for the button which excludes a tab from the list">
-        Remove tab from list
-      </message>
-      <message name="IDS_DECLUTTER_A11Y_TAB_EXCLUDED" desc="A11y message after user clicks on the close tab button in the declutter list item." is_accessibility_with_no_ui="true">
-        Tab excluded from list
-      </message>
       <!-- Strings for tab organization -->
       <message name="IDS_TOOLTIP_TAB_ORGANIZE" desc="The tooltip for the Tab Organization button.">
         Organize tabs?
@@ -10365,9 +10315,6 @@
       <message name="IDS_TAB_ORGANIZATION_SUCCESS_IPH_SCREENREADER" desc="The screen reader text of the IPH describing how to interact with a tab group resulting from tab organization." is_accessibility_with_no_ui="true">
         Select the tab group and activate the context menu to edit
       </message>
-      <message name="IDS_TAB_ORGANIZATION_SELECTOR_ARIA_LABEL" desc="The screen reader text describing the tab organization selector button" is_accessibility_with_no_ui="true">
-        <ph name="HEADING">$1<ex>Group tabs with AI</ex></ph>. <ph name="SUBHEADING">$2<ex>Get your tabs sorted</ex></ph>
-      </message>
 
       <!-- Strings for split view -->
       <message name="IDS_SPLIT_VIEW_NTP_EMPTY_TITLE" desc="The title of the split view new tab page in its empty state.">
@@ -12557,15 +12504,6 @@
       <message name="IDS_TAB_ORGANIZATION_THUMBS_UP" desc="Accessibility label for the thumbs up icon that a user can click to provide positive feedback about a tab organization." is_accessibility_with_no_ui="true">
         Thumbs up submits feedback that you like this tab group suggestion
       </message>
-      <message name="IDS_AUTO_TAB_GROUPS_SELECTOR_HEADING" desc="Heading for the auto tab groups button in the tab organization selector.">
-        Group tabs with AI
-      </message>
-      <message name="IDS_AUTO_TAB_GROUPS_SELECTOR_SUBHEADING" desc="Subheading for the auto tab groups button in the tab organization selector.">
-        Get your tabs sorted
-      </message>
-      <message name="IDS_TAB_ORGANIZATION_BACK_BUTTON_ARIA_LABEL" desc="Accessibility label for the back button within a tab organization subpage." is_accessibility_with_no_ui="true">
-        Click to navigate away from <ph name="TITLE">$1<ex>Auto Tab Groups</ex></ph>
-      </message>
       <message name="IDS_TAB_ORGANIZATION_A11Y_TAB_EXCLUDED" desc="A11y message after user clicks on the close tab button in the tab organization list item." is_accessibility_with_no_ui="true">
         Tab excluded from suggested group
       </message>
diff --git a/chrome/app/generated_resources_grd/IDS_AUTO_TAB_GROUPS_SELECTOR_HEADING.png.sha1 b/chrome/app/generated_resources_grd/IDS_AUTO_TAB_GROUPS_SELECTOR_HEADING.png.sha1
deleted file mode 100644
index 48a9f8e..0000000
--- a/chrome/app/generated_resources_grd/IDS_AUTO_TAB_GROUPS_SELECTOR_HEADING.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-851c79e5271579595b2a315f1efa1229c39a43aa
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_AUTO_TAB_GROUPS_SELECTOR_SUBHEADING.png.sha1 b/chrome/app/generated_resources_grd/IDS_AUTO_TAB_GROUPS_SELECTOR_SUBHEADING.png.sha1
deleted file mode 100644
index 48a9f8e..0000000
--- a/chrome/app/generated_resources_grd/IDS_AUTO_TAB_GROUPS_SELECTOR_SUBHEADING.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-851c79e5271579595b2a315f1efa1229c39a43aa
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THESE.png.sha1 b/chrome/app/generated_resources_grd/IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THESE.png.sha1
new file mode 100644
index 0000000..eb94497
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THESE.png.sha1
@@ -0,0 +1 @@
+044de2419c237318c4997f22eda9352030674414
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_DOC.png.sha1 b/chrome/app/generated_resources_grd/IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_DOC.png.sha1
new file mode 100644
index 0000000..b611d59c
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_DOC.png.sha1
@@ -0,0 +1 @@
+15b833d39653001e5f4b06ed5284d2fd33b8a6a0
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_IMAGE.png.sha1 b/chrome/app/generated_resources_grd/IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_IMAGE.png.sha1
new file mode 100644
index 0000000..c02934e
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_IMAGE.png.sha1
@@ -0,0 +1 @@
+131e008f1ab5088b55d50361d99cd62425dcaabd
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_TAB.png.sha1 b/chrome/app/generated_resources_grd/IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_TAB.png.sha1
new file mode 100644
index 0000000..b1bc901
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_TAB.png.sha1
@@ -0,0 +1 @@
+22651e9c89bebb67dbb52dafb090c382364ebf5f
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_CLOSE_TABS.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_CLOSE_TABS.png.sha1
deleted file mode 100644
index 8706c633a..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_CLOSE_TABS.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-525c54012932cde8b289821d7de75b7c16ff3714
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_CLOSE_TAB_TOOLTIP.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_CLOSE_TAB_TOOLTIP.png.sha1
deleted file mode 100644
index f064140..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_CLOSE_TAB_TOOLTIP.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-53a2ec23f86aee0df9494124bf4561cfd8134c10
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_DUPLICATE_BODY.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_DUPLICATE_BODY.png.sha1
deleted file mode 100644
index 8a9c971..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_DUPLICATE_BODY.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-acc62ee10c8dea96dc45def09fa94fc51b7ce22b
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_DUPLICATE_TITLE.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_DUPLICATE_TITLE.png.sha1
deleted file mode 100644
index 8a9c971..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_DUPLICATE_TITLE.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-acc62ee10c8dea96dc45def09fa94fc51b7ce22b
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_EMPTY_BODY.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_EMPTY_BODY.png.sha1
deleted file mode 100644
index d8242d5..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_EMPTY_BODY.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-c10d76d7607f0d28e2e318730319335e8e3a6116
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_EMPTY_BODY_NO_DEDUPE.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_EMPTY_BODY_NO_DEDUPE.png.sha1
deleted file mode 100644
index 87247b7..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_EMPTY_BODY_NO_DEDUPE.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-24264a450e0312c09103d3ecb939fe72c43c6a1e
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_EMPTY_TITLE.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_EMPTY_TITLE.png.sha1
deleted file mode 100644
index 32f1a4d6..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_EMPTY_TITLE.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-d80558c62b04ffece2c15e0a61e39d609006a69f
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_INACTIVE_BODY.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_INACTIVE_BODY.png.sha1
deleted file mode 100644
index 90ea925..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_INACTIVE_BODY.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-6953db9903ea7833e274a1a266e92fa3e93a0cf1
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_INACTIVE_TITLE.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_INACTIVE_TITLE.png.sha1
deleted file mode 100644
index fd38894..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_INACTIVE_TITLE.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-6953db9903ea7833e274a1a266e92fa3e93a0cf1
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_INACTIVE_TITLE_NO_DEDUPE.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_INACTIVE_TITLE_NO_DEDUPE.png.sha1
deleted file mode 100644
index 821dd520..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_INACTIVE_TITLE_NO_DEDUPE.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-f6a6a4112c0abd424b94a86898ecefd0f34e2e7f
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_SELECTOR_HEADING.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_SELECTOR_HEADING.png.sha1
deleted file mode 100644
index 7794c10b..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_SELECTOR_HEADING.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-b3c66483da6ef07bd752594610c33426aa8d3235
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_SELECTOR_HEADING_NO_DEDUPE.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_SELECTOR_HEADING_NO_DEDUPE.png.sha1
deleted file mode 100644
index 813cf28..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_SELECTOR_HEADING_NO_DEDUPE.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-02c666c4950901f0240698210449d0ea4664b5b7
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_SELECTOR_SUBHEADING.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_SELECTOR_SUBHEADING.png.sha1
deleted file mode 100644
index 48a9f8e..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_SELECTOR_SUBHEADING.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-851c79e5271579595b2a315f1efa1229c39a43aa
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_TIMESTAMP.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_TIMESTAMP.png.sha1
deleted file mode 100644
index 71de47d..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_TIMESTAMP.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-419c4497a551f476ee0b2653d515a071028383f6
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DECLUTTER_TITLE.png.sha1 b/chrome/app/generated_resources_grd/IDS_DECLUTTER_TITLE.png.sha1
deleted file mode 100644
index fd38894..0000000
--- a/chrome/app/generated_resources_grd/IDS_DECLUTTER_TITLE.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-6953db9903ea7833e274a1a266e92fa3e93a0cf1
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DUPLICATE_ITEM_TITLE_MULTI.png.sha1 b/chrome/app/generated_resources_grd/IDS_DUPLICATE_ITEM_TITLE_MULTI.png.sha1
deleted file mode 100644
index 37a2d3f0..0000000
--- a/chrome/app/generated_resources_grd/IDS_DUPLICATE_ITEM_TITLE_MULTI.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-453f2a8b36f3edf1d33b58e10a78babb471bf13b
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DUPLICATE_ITEM_TITLE_SINGLE.png.sha1 b/chrome/app/generated_resources_grd/IDS_DUPLICATE_ITEM_TITLE_SINGLE.png.sha1
deleted file mode 100644
index b43377972..0000000
--- a/chrome/app/generated_resources_grd/IDS_DUPLICATE_ITEM_TITLE_SINGLE.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-6f904b3e2a8226d9d309a55a39fe04094ea33f2b
\ No newline at end of file
diff --git a/chrome/app/glic_strings.grdp b/chrome/app/glic_strings.grdp
index 89e6a85..cb2623d8 100644
--- a/chrome/app/glic_strings.grdp
+++ b/chrome/app/glic_strings.grdp
@@ -270,6 +270,12 @@
   <message name="IDS_SETTINGS_GLIC_INSTRUCTIONS_BUTTON_SUBLABEL" desc="Description for the Gemini instructions linkout.">
     Customize how Gemini responds to you
   </message>
+  <message name="IDS_SETTINGS_GLIC_PERMISSIONS_CHROME_WEB_ACTUATION_TOGGLE" desc="Title for the Gemini feature introduction screen">
+    Let Chrome browse for you
+  </message>
+  <message name="IDS_SETTINGS_GLIC_PERMISSIONS_CHROME_WEB_ACTUATION_TOGGLE_SUBLABEL" desc="Subheader explaining what the Gemini feature does">
+    Gemini in Chrome uses auto browse to work in your tabs and complete tasks you give it. <ph name="LINK_BEGIN">&lt;a href="#"&gt;</ph>Learn more<ph name="LINK_END">&lt;/a&gt;</ph>
+  </message>
   <message name="IDS_SETTINGS_GLIC_PERMISSIONS_WEB_ACTUATION_TOGGLE_WHEN_ON_1" desc="First point in 'when on' column of the 'let Gemini browse for you' expands">
     Gemini can help you book appointments, create shopping carts, or do other tasks you give it.
   </message>
diff --git a/chrome/app/google_chrome_strings_grd/IDS_SETTINGS_GLIC_PERMISSIONS_WEB_ACTUATION_TOGGLE.png.sha1 b/chrome/app/glic_strings_grdp/IDS_SETTINGS_GLIC_PERMISSIONS_CHROME_WEB_ACTUATION_TOGGLE.png.sha1
similarity index 100%
rename from chrome/app/google_chrome_strings_grd/IDS_SETTINGS_GLIC_PERMISSIONS_WEB_ACTUATION_TOGGLE.png.sha1
rename to chrome/app/glic_strings_grdp/IDS_SETTINGS_GLIC_PERMISSIONS_CHROME_WEB_ACTUATION_TOGGLE.png.sha1
diff --git a/chrome/app/google_chrome_strings_grd/IDS_SETTINGS_GLIC_PERMISSIONS_WEB_ACTUATION_TOGGLE_SUBLABEL.png.sha1 b/chrome/app/glic_strings_grdp/IDS_SETTINGS_GLIC_PERMISSIONS_CHROME_WEB_ACTUATION_TOGGLE_SUBLABEL.png.sha1
similarity index 100%
rename from chrome/app/google_chrome_strings_grd/IDS_SETTINGS_GLIC_PERMISSIONS_WEB_ACTUATION_TOGGLE_SUBLABEL.png.sha1
rename to chrome/app/glic_strings_grdp/IDS_SETTINGS_GLIC_PERMISSIONS_CHROME_WEB_ACTUATION_TOGGLE_SUBLABEL.png.sha1
diff --git a/chrome/app/google_chrome_strings.grd b/chrome/app/google_chrome_strings.grd
index fa71b8c..46e1ec92 100644
--- a/chrome/app/google_chrome_strings.grd
+++ b/chrome/app/google_chrome_strings.grd
@@ -3044,22 +3044,16 @@
         To use Gemini in Chrome, sign in with a different Google Account
       </message>
 
-      <!-- Experimental Glic settings -->
-      <message name="IDS_SETTINGS_GLIC_PAGE_TITLE" desc="Title of the Gemini settings subpage. Also shown in tab strip.">
-        Gemini in Chrome
-      </message>
-      <message name="IDS_SETTINGS_GLIC_SECTION_TITLE" desc="Text of the Gemini section header on the AI settings page.">
-        Gemini in Chrome
-      </message>
-      <message name="IDS_SETTINGS_GLIC_ROW_LABEL" desc="Label of the Gemini row button on the AI settings page leading to the Gemini subpage.">
-        Gemini in Chrome
-      </message>
-      <message name="IDS_SETTINGS_GLIC_PERMISSIONS_WEB_ACTUATION_TOGGLE" desc="Title for the Gemini feature introduction screen">
-        Let Chrome browse for you
-      </message>
-      <message name="IDS_SETTINGS_GLIC_PERMISSIONS_WEB_ACTUATION_TOGGLE_SUBLABEL" desc="Subheader explaining what the Gemini feature does">
-        Gemini in Chrome uses auto browse to work in your tabs and complete tasks you give it. <ph name="LINK_BEGIN">&lt;a href="#"&gt;</ph>Learn more<ph name="LINK_END">&lt;/a&gt;</ph>
-      </message>
+        <!-- Experimental Glic settings -->
+        <message name="IDS_SETTINGS_GLIC_PAGE_TITLE" desc="Title of the Gemini settings subpage. Also shown in tab strip.">
+          Gemini in Chrome
+        </message>
+        <message name="IDS_SETTINGS_GLIC_SECTION_TITLE" desc="Text of the Gemini section header on the AI settings page.">
+          Gemini in Chrome
+        </message>
+        <message name="IDS_SETTINGS_GLIC_ROW_LABEL" desc="Label of the Gemini row button on the AI settings page leading to the Gemini subpage.">
+          Gemini in Chrome
+        </message>
 
       <!-- Profile Picker Glic version-->
       <message name="IDS_PROFILE_PICKER_MAIN_VIEW_TITLE_GLIC" desc="Heading describing that the section below choosing a Chrome profile to use with Gemini. 'Gemini' has a special styling.">
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 04e895ec..a0e7bb2 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -334,11 +334,6 @@
     "data_sharing/personal_collaboration_data/personal_collaboration_data_service_factory.h",
     "defaults.cc",
     "defaults.h",
-    "digital_credentials/digital_credentials_keyed_service.cc",
-    "digital_credentials/digital_credentials_keyed_service.h",
-    "digital_credentials/digital_identity_interstitial_closed_reason.h",
-    "digital_credentials/digital_identity_low_risk_origins.cc",
-    "digital_credentials/digital_identity_low_risk_origins.h",
     "download/background_download_service_factory.cc",
     "download/background_download_service_factory.h",
     "download/chrome_download_manager_delegate.cc",
@@ -521,10 +516,6 @@
     "icon_manager.h",
     "idle/idle_detection_permission_context.cc",
     "idle/idle_detection_permission_context.h",
-    "interstitials/chrome_settings_page_helper.cc",
-    "interstitials/chrome_settings_page_helper.h",
-    "interstitials/enterprise_util.cc",
-    "interstitials/enterprise_util.h",
     "invalidation/profile_invalidation_provider_factory.cc",
     "invalidation/profile_invalidation_provider_factory.h",
     "language/accept_languages_service_factory.cc",
@@ -1436,6 +1427,11 @@
     # TODO(crbug.com/486309771): Remove this circular dependency when
     # chrome_metrics_service_accessor.h gets extracted into its own target.
     "//chrome/browser/gpu:impl",
+
+    # TODO(crbug.com/489125036): Remove this circular dependency when
+    # enterprise/connectors/reporting/reporting_event_router_factory.h gets
+    # extracted into its own target.
+    "//chrome/browser/interstitials:impl",
     "//chrome/browser/media/webrtc",
     "//chrome/browser/navigation_predictor:impl",
 
@@ -1562,7 +1558,7 @@
     # - c/b/password_manager/chrome_password_manager_client.h
     "//chrome/browser/ui/passwords:impl",
 
-    # TODO(crbug.com/): Remove this circular dependency when
+    # TODO(crbug.com/353332589): Remove this circular dependency when
     # c/b/net/system_network_context_manager.h gets componentized.
     "//chrome/browser/assist_ranker:impl",
 
@@ -1634,6 +1630,8 @@
     ":chrome_content_browser_client_parts",
     "//chrome/browser/google",
     "//chrome/browser/headless",
+    "//chrome/browser/interstitials",
+    "//chrome/browser/obsolete_system",
     "//chrome/browser/profiles",
     "//chrome/browser/ui/tabs:tab_enums",
   ]
@@ -1732,6 +1730,8 @@
     "//chrome/browser/devtools",
     "//chrome/browser/diagnostics",
     "//chrome/browser/diagnostics:impl",
+    "//chrome/browser/digital_credentials",
+    "//chrome/browser/digital_credentials:impl",
     "//chrome/browser/dom_distiller",
     "//chrome/browser/dom_distiller:impl",
     "//chrome/browser/domain_reliability",
@@ -1762,6 +1762,7 @@
     "//chrome/browser/history",
     "//chrome/browser/image_decoder",
     "//chrome/browser/image_fetcher",
+    "//chrome/browser/interstitials:impl",
     "//chrome/browser/k_anonymity_service",
     "//chrome/browser/k_anonymity_service:impl",
     "//chrome/browser/lifetime:termination_notification",
@@ -1787,6 +1788,7 @@
     "//chrome/browser/notifications:system_notification_helper_impl",
     "//chrome/browser/notifications/scheduler:factory",
     "//chrome/browser/notifications/scheduler/public",
+    "//chrome/browser/obsolete_system:impl",
     "//chrome/browser/offline_items_collection",
     "//chrome/browser/omnibox",
     "//chrome/browser/optimization_guide",
@@ -2891,8 +2893,6 @@
       "device_reauth/android/device_authenticator_bridge_impl.h",
       "device_reauth/android/reauthenticator_bridge.cc",
       "device_reauth/android/reauthenticator_bridge.h",
-      "digital_credentials/digital_identity_provider_android.cc",
-      "digital_credentials/digital_identity_provider_android.h",
       "download/android/dangerous_download_dialog_bridge.cc",
       "download/android/dangerous_download_dialog_bridge.h",
       "download/android/download_callback_validator.cc",
@@ -3635,8 +3635,6 @@
       "data_sharing/desktop/data_sharing_sdk_delegate_desktop.h",
       "data_sharing/desktop/data_sharing_ui_delegate_desktop.cc",
       "data_sharing/desktop/data_sharing_ui_delegate_desktop.h",
-      "digital_credentials/digital_identity_provider_desktop.cc",
-      "digital_credentials/digital_identity_provider_desktop.h",
       "download/default_download_dir_policy_handler.cc",
       "download/default_download_dir_policy_handler.h",
       "download/download_auto_open_policy_handler.cc",
@@ -3868,7 +3866,6 @@
       "notifications/profile_notification.h",
       "notifications/screen_capture_notification_blocker.cc",
       "notifications/screen_capture_notification_blocker.h",
-      "obsolete_system/obsolete_system.h",
       "page_info/web_view_side_panel_throttle.cc",
       "page_info/web_view_side_panel_throttle.h",
       "page_load_metrics/observers/initial_webui_page_load_metrics_observer.cc",
@@ -4522,7 +4519,6 @@
       # TODO(crbug.com/441020158): Remove this circular dependency when the following headers get
       # componentized:
       # - c/b/enterprise/browser_management/management_service_factory.h
-      # - c/b/obsolete_system/obsolete_system.h
       # - c/b/lifetime/application_lifetime.h
       "//chrome/browser/upgrade_detector:impl",
 
@@ -4541,6 +4537,10 @@
       "//chrome/browser/device_api:impl",
 
       "//chrome/browser/glic/media",
+
+      # TODO(crbug.com/353332589): Remove this circular dependency when
+      # c/b/net/system_network_context_manager.h gets componentized.
+      "//chrome/browser/digital_credentials:impl",
     ]
 
     if (is_win || is_chromeos) {
@@ -5003,7 +5003,6 @@
       "notifications/passphrase_textfield.h",
       "notifications/web_page_notifier_controller.cc",
       "notifications/web_page_notifier_controller.h",
-      "obsolete_system/obsolete_system_stub.cc",
       "page_load_metrics/observers/ash_session_restore_page_load_metrics_observer.cc",
       "page_load_metrics/observers/ash_session_restore_page_load_metrics_observer.h",
       "performance_manager/mechanisms/working_set_trimmer_chromeos.cc",
@@ -6236,7 +6235,6 @@
       "notifications/win/notification_template_builder.h",
       "notifications/win/notification_util.cc",
       "notifications/win/notification_util.h",
-      "obsolete_system/obsolete_system_win.cc",
       "password_manager/password_manager_util_win.cc",
       "password_manager/password_manager_util_win.h",
       "performance_manager/policies/dll_pre_read_policy_win.cc",
@@ -6480,7 +6478,6 @@
       "notifications/mac/notification_platform_bridge_mac.h",
       "notifications/mac/notification_utils.cc",
       "notifications/mac/notification_utils.h",
-      "obsolete_system/obsolete_system_mac.cc",
       "password_manager/password_manager_util_mac.h",
       "password_manager/password_manager_util_mac.mm",
       "platform_util_mac.mm",
@@ -6947,7 +6944,6 @@
       "metrics/pressure/pressure_metrics.h",
       "metrics/pressure/pressure_metrics_reporter.cc",
       "metrics/pressure/pressure_metrics_reporter.h",
-      "obsolete_system/obsolete_system_linux.cc",
       "shell_integration_linux.cc",
       "shell_integration_linux.h",
     ]
@@ -8891,8 +8887,6 @@
     "download/download_test_file_activity_observer.h",
     "history/history_test_utils.cc",
     "history/history_test_utils.h",
-    "interstitials/security_interstitial_page_test_utils.cc",
-    "interstitials/security_interstitial_page_test_utils.h",
     "media/mock_media_engagement_service.cc",
     "media/mock_media_engagement_service.h",
     "media/webrtc/fake_desktop_media_list.cc",
@@ -8932,6 +8926,7 @@
   public_deps = [
     ":browser",
     "//chrome/browser/autofill:test_support",
+    "//chrome/browser/interstitials:test_support",
     "//chrome/browser/media/webrtc",
     "//chrome/browser/notifications",
     "//chrome/browser/predictors:test_support",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index eb9f9de3..0526190 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -10392,6 +10392,13 @@
      FEATURE_VALUE_TYPE(blink::features::kAIProofreadingAPI),
      flag_descriptions::kAIAPIsForGeminiNanoLinks},
 
+    {"summarizer-api-performance-preference",
+     flag_descriptions::kSummarizerAPIWithPerformancePreferenceName,
+     flag_descriptions::kSummarizerAPIWithPerformancePreferenceDescription,
+     kOsDesktop,
+     FEATURE_VALUE_TYPE(blink::features::kAISummarizationPerformancePreference),
+     flag_descriptions::kSummarizerAPIWithPerformancePreferenceLink},
+
     {"on-device-model-litert-lm-backend",
      flag_descriptions::kOnDeviceModelLitertLmBackendName,
      flag_descriptions::kOnDeviceModelLitertLmBackendDescription, kOsDesktop,
@@ -13051,6 +13058,11 @@
     {"ntp-mvc-refactor", flag_descriptions::kNtpMvcRefactorName,
      flag_descriptions::kNtpMvcRefactorDescription, kOsAndroid,
      FEATURE_VALUE_TYPE(chrome::android::kNtpMvcRefactor)},
+
+    {"fullscreen-video-picture-in-picture",
+     flag_descriptions::kFullscreenVideoPictureInPictureName,
+     flag_descriptions::kFullscreenVideoPictureInPictureDescription, kOsAndroid,
+     FEATURE_VALUE_TYPE(media::kFullscreenVideoPictureInPicture)},
 #endif
 
 #if BUILDFLAG(IS_ANDROID)
diff --git a/chrome/browser/actor/actor_features.cc b/chrome/browser/actor/actor_features.cc
index 7d8841a..31c4499 100644
--- a/chrome/browser/actor/actor_features.cc
+++ b/chrome/browser/actor/actor_features.cc
@@ -75,6 +75,11 @@
                    &kGlicCrossOriginNavigationGating,
                    "include_hardcoded_block_list_entries",
                    true);
+BASE_FEATURE_PARAM(bool,
+                   kGlicAllowImplicitToolOriginGrants,
+                   &kGlicCrossOriginNavigationGating,
+                   "allow_implicit_tool_origin_grants",
+                   true);
 
 BASE_FEATURE(kGlicRecordNavigationConfirmationRequestMetrics,
              base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/chrome/browser/actor/actor_features.h b/chrome/browser/actor/actor_features.h
index 8039614..ba09a95 100644
--- a/chrome/browser/actor/actor_features.h
+++ b/chrome/browser/actor/actor_features.h
@@ -49,6 +49,8 @@
 // Controls whether a hardcoded block list is enabled for the static block list.
 // TODO(crbug.com/453660392): Remove flag once Component Updater rollout starts.
 BASE_DECLARE_FEATURE_PARAM(bool, kGlicIncludeHardcodedBlockListEntries);
+// Controls whether tool requests can implicitly allow new origins.
+BASE_DECLARE_FEATURE_PARAM(bool, kGlicAllowImplicitToolOriginGrants);
 
 // Controls whether chrome records UMA metrics for navigations by sending the
 // `NavigationConfirmationRequest` and recording the response.
diff --git a/chrome/browser/actor/android/BUILD.gn b/chrome/browser/actor/android/BUILD.gn
index dad5e4ee..9a2e7db 100644
--- a/chrome/browser/actor/android/BUILD.gn
+++ b/chrome/browser/actor/android/BUILD.gn
@@ -7,6 +7,7 @@
 
 android_library("java") {
   sources = [
+    "java/src/org/chromium/chrome/browser/actor/ActorBroadcastReceiver.java",
     "java/src/org/chromium/chrome/browser/actor/ActorKeyedService.java",
     "java/src/org/chromium/chrome/browser/actor/ActorKeyedServiceFactory.java",
     "java/src/org/chromium/chrome/browser/actor/ActorPictureInPictureController.java",
diff --git a/chrome/browser/actor/android/java/src/org/chromium/chrome/browser/actor/ActorBroadcastReceiver.java b/chrome/browser/actor/android/java/src/org/chromium/chrome/browser/actor/ActorBroadcastReceiver.java
new file mode 100644
index 0000000..33e3b7eb
--- /dev/null
+++ b/chrome/browser/actor/android/java/src/org/chromium/chrome/browser/actor/ActorBroadcastReceiver.java
@@ -0,0 +1,18 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.actor;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import org.chromium.build.annotations.NullMarked;
+
+/** Handles broadcast intents for Actor tasks from notifications. */
+@NullMarked
+public class ActorBroadcastReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {}
+}
diff --git a/chrome/browser/actor/execution_engine.cc b/chrome/browser/actor/execution_engine.cc
index f71bb87..e728c04 100644
--- a/chrome/browser/actor/execution_engine.cc
+++ b/chrome/browser/actor/execution_engine.cc
@@ -692,7 +692,8 @@
     if (action->GetTabHandle() != tabs::TabHandle::Null()) {
       acting_tab_handles.insert(action->GetTabHandle().raw_value());
     }
-    if (IsNavigationGatingEnabled()) {
+    if (IsNavigationGatingEnabled() &&
+        kGlicAllowImplicitToolOriginGrants.Get()) {
       if (std::optional<url::Origin> maybe_origin =
               action->AssociatedOriginGrant();
           maybe_origin) {
diff --git a/chrome/browser/actor/execution_engine_origin_gating_browsertest.cc b/chrome/browser/actor/execution_engine_origin_gating_browsertest.cc
index 3e5528c..4379d24ad 100644
--- a/chrome/browser/actor/execution_engine_origin_gating_browsertest.cc
+++ b/chrome/browser/actor/execution_engine_origin_gating_browsertest.cc
@@ -367,6 +367,51 @@
       "Actor.NavigationGating.PermissionGranted", false, 1);
 }
 
+class ExecutionEngineOriginGatingExplicitGrantBrowserTest
+    : public ExecutionEngineOriginGatingBrowserTest {
+ public:
+  ExecutionEngineOriginGatingExplicitGrantBrowserTest() {
+    scoped_feature_list_.InitAndEnableFeatureWithParameters(
+        kGlicCrossOriginNavigationGating,
+        {{"allow_implicit_tool_origin_grants", "false"},
+         {"confirm_navigation_to_new_origins", "true"}});
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_P(ExecutionEngineOriginGatingExplicitGrantBrowserTest,
+                       ImplicitGrantDisabled) {
+  const GURL start_url =
+      embedded_https_test_server().GetURL("example.com", "/actor/blank.html");
+  const GURL destination_url =
+      embedded_https_test_server().GetURL("foo.com", "/actor/blank.html");
+
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), start_url));
+  OpenGlicAndCreateTask();
+
+  RunTestSequence(CreateMockWebClientRequest(
+      content::JsReplace(kHandleNavigationConfirmationTempl, false)));
+
+  std::unique_ptr<ToolRequest> navigate =
+      MakeNavigateRequest(*active_tab(), destination_url.spec());
+  ActResultFuture result;
+  actor_task().Act(ToRequestList(navigate), result.GetCallback());
+
+  // Since implicit grant is disabled, it should try to prompt and get denied.
+  ExpectErrorResult(result,
+                    mojom::ActionResultCode::kTriggeredNavigationBlocked);
+}
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         ExecutionEngineOriginGatingExplicitGrantBrowserTest,
+                         testing::Bool(),
+                         [](auto& info) {
+                           return info.param ? "MultiInstance"
+                                             : "SingleInstance";
+                         });
+
 IN_PROC_BROWSER_TEST_P(ExecutionEngineOriginGatingBrowserTest,
                        ConfirmBlockedOriginWithUser_Granted) {
   base::HistogramTester histogram_tester;
diff --git a/chrome/browser/ai/ai_manager.cc b/chrome/browser/ai/ai_manager.cc
index 1c3415c..f9f9201 100644
--- a/chrome/browser/ai/ai_manager.cc
+++ b/chrome/browser/ai/ai_manager.cc
@@ -508,6 +508,16 @@
                                 kUnavailableUnsupportedLanguage);
     return;
   }
+
+  // TODO(crbug.com/488092645): Support capability and speed preference for
+  // summarizer. This is currently a No-Op.
+  if (options &&
+      options->preference == blink::mojom::PerformancePreference::kSpeed) {
+    std::move(callback).Run(blink::mojom::ModelAvailabilityCheckResult::
+                                kUnavailableUnsupportedPerformancePreference);
+    return;
+  }
+
   CanCreateSession(optimization_guide::mojom::OnDeviceFeature::kSummarize,
                    on_device_model::Capabilities(), std::move(callback));
 }
@@ -525,6 +535,23 @@
     return;
   }
 
+  // TODO(crbug.com/488092645): Support capability and speed preference for
+  // summarizer. This is currently a No-Op.
+
+  // CanCreateSummarizer should have been called which has already verified
+  // that the preference is supported, but if the renderer is compromised, the
+  // CreateSummarizer mojo function could be called directly with invalid
+  // values.
+  if (options &&
+      options->preference == blink::mojom::PerformancePreference::kSpeed) {
+    mojo::Remote<blink::mojom::AIManagerCreateSummarizerClient> client_remote(
+        std::move(client));
+    on_device_ai::SendClientRemoteError(
+        client_remote, blink::mojom::AIManagerCreateClientError::
+                           kUnsupportedPerformancePreference);
+    return;
+  }
+
   if (!model_broker_client_) {
     mojo::Remote<blink::mojom::AIManagerCreateSummarizerClient> client_remote(
         std::move(client));
diff --git a/chrome/browser/ai/ai_summarizer_unittest.cc b/chrome/browser/ai/ai_summarizer_unittest.cc
index c647d98..9461ddf6 100644
--- a/chrome/browser/ai/ai_summarizer_unittest.cc
+++ b/chrome/browser/ai/ai_summarizer_unittest.cc
@@ -94,6 +94,7 @@
       kSharedContextString, blink::mojom::AISummarizerType::kTLDR,
       blink::mojom::AISummarizerFormat::kPlainText,
       blink::mojom::AISummarizerLength::kMedium,
+      blink::mojom::PerformancePreference::kAuto,
       /*expected_input_languages=*/std::vector<AILanguageCodePtr>(),
       /*expected_context_languages=*/std::vector<AILanguageCodePtr>(),
       /*output_language=*/AILanguageCode::New(""));
@@ -169,6 +170,7 @@
 
     const auto options = blink::mojom::AISummarizerCreateOptions::New(
         kSharedContextString, type, format, length,
+        blink::mojom::PerformancePreference::kAuto,
         /*expected_input_languages=*/std::vector<AILanguageCodePtr>(),
         /*expected_context_languages=*/std::vector<AILanguageCodePtr>(),
         /*output_language=*/AILanguageCode::New(""));
@@ -245,6 +247,17 @@
                                                callback.Get());
 }
 
+// TODO(https://crbug.com/488092645): Remove once we support speed.
+TEST_F(AISummarizerTest, CanCreateUnsupportedPreference) {
+  auto options = GetDefaultOptions();
+  options->preference = blink::mojom::PerformancePreference::kSpeed;
+  base::MockCallback<AIManager::CanCreateSummarizerCallback> callback;
+  EXPECT_CALL(callback, Run(blink::mojom::ModelAvailabilityCheckResult::
+                                kUnavailableUnsupportedPerformancePreference));
+  GetAIManagerInterface()->CanCreateSummarizer(std::move(options),
+                                               callback.Get());
+}
+
 TEST_F(AISummarizerTest, ToProtoOptionsLanguagesSupported) {
   // Summarizer proto expects a limited set of BCP 47 base language codes.
   std::vector<std::pair<std::string, std::string>> languages = {
@@ -273,6 +286,21 @@
             blink::mojom::AIManagerCreateClientError::kUnableToCreateSession);
 }
 
+// TODO(https://crbug.com/488092645): Remove once we support speed.
+TEST_F(AISummarizerTest, CreateUnsupportedPreference) {
+  auto options = GetDefaultOptions();
+  options->preference = blink::mojom::PerformancePreference::kSpeed;
+
+  TestCreateSummarizerClient create_summarizer_client;
+  GetAIManagerRemote()->CreateSummarizer(
+      create_summarizer_client.BindNewPipeAndPassRemote(), std::move(options));
+
+  CreateSummarizerResult result = create_summarizer_client.result().Take();
+  EXPECT_FALSE(result.has_value());
+  EXPECT_EQ(result.error().error, blink::mojom::AIManagerCreateClientError::
+                                      kUnsupportedPerformancePreference);
+}
+
 TEST_F(AISummarizerTest, CanCreateWaitsForEligibility) {
   base::test::TestFuture<base::OnceCallback<void(
       optimization_guide::OnDeviceModelEligibilityReason)>>
@@ -607,6 +635,7 @@
       "unsafe", blink::mojom::AISummarizerType::kTLDR,
       blink::mojom::AISummarizerFormat::kPlainText,
       blink::mojom::AISummarizerLength::kMedium,
+      blink::mojom::PerformancePreference::kAuto,
       /*expected_input_languages=*/std::vector<AILanguageCodePtr>(),
       /*expected_context_languages=*/std::vector<AILanguageCodePtr>(),
       /*output_language=*/AILanguageCode::New(""));
diff --git a/chrome/browser/android/compositor/scene_layer/tab_strip_scene_layer.cc b/chrome/browser/android/compositor/scene_layer/tab_strip_scene_layer.cc
index 3400b8f7..ef9f23d4 100644
--- a/chrome/browser/android/compositor/scene_layer/tab_strip_scene_layer.cc
+++ b/chrome/browser/android/compositor/scene_layer/tab_strip_scene_layer.cc
@@ -45,7 +45,7 @@
       left_padding_layer_(cc::slim::SolidColorLayer::Create()),
       right_padding_layer_(cc::slim::SolidColorLayer::Create()),
       glic_button_(cc::slim::UIResourceLayer::Create()),
-      glic_button_background_(cc::slim::UIResourceLayer::Create()),
+      glic_button_background_(cc::slim::SolidColorLayer::Create()),
       glic_button_text_(cc::slim::UIResourceLayer::Create()),
       glic_button_keyboard_focus_ring_(cc::slim::UIResourceLayer::Create()),
       model_selector_button_(cc::slim::UIResourceLayer::Create()),
@@ -318,17 +318,16 @@
 //   edge to the button's end.
 //                           (Note: this is implicitly handled by the total
 //                           `button_width`).
-//   d = background_size.height(): The height of the background resource, which
-//   dictates the total height of the button. e = button_width: The total
-//   dynamic width of the button, calculated to wrap the icon, text, and all
-//   paddings.
+//   d = button_height: The total height of the button.
+//   e = button_width: The total dynamic width of the button, calculated to wrap
+//   the icon, text, and all paddings.
 void TabStripSceneLayer::UpdateGlicButton(
     JNIEnv* env,
     int32_t resource_id,
-    int32_t bg_resource_id,
     float x,
     float y,
     float button_width,
+    float button_height,
     bool visible,
     bool should_apply_hover_highlight,
     int32_t tint,
@@ -339,33 +338,33 @@
     int32_t keyboard_focus_ring_color,
     int32_t text_texture_id,
     float button_start_padding,
-    float icon_text_padding) {
+    float icon_text_padding,
+    float corner_radius) {
   DCHECK(resource_manager_);
   ui::Resource* icon_resource =
       resource_manager_->GetStaticResourceWithTint(resource_id, tint);
-  ui::Resource* background_resource =
-      resource_manager_->GetStaticResourceWithTint(bg_resource_id,
-                                                   background_tint, true);
   ui::Resource* text_resource = resource_manager_->GetResource(
       ui::ANDROID_RESOURCE_TYPE_DYNAMIC, text_texture_id);
   ui::Resource* keyboard_focus_ring_drawable =
       resource_manager_->GetStaticResourceWithTint(
           keyboard_focus_ring_resource_id, keyboard_focus_ring_color, true);
 
-  gfx::Size background_size = background_resource->size();
+  gfx::Size background_size(std::round(button_width),
+                            std::round(button_height));
   gfx::Size icon_size = icon_resource->size();
   gfx::Size text_size = text_resource ? text_resource->size() : gfx::Size();
   gfx::Size ring_size = keyboard_focus_ring_drawable->size();
 
   // 1. Background
-  glic_button_background_->SetUIResourceId(
-      background_resource->ui_resource()->id());
-  glic_button_background_->SetBounds(
-      gfx::Size(std::round(button_width), background_size.height()));
+  glic_button_background_->SetBackgroundColor(
+      SkColor4f::FromColor(background_tint));
+  glic_button_background_->SetBounds(background_size);
   glic_button_background_->SetPosition(
       gfx::PointF(std::round(x), std::round(y)));
   glic_button_background_->SetHideLayerAndSubtree(!visible);
   glic_button_background_->SetOpacity(button_alpha);
+  glic_button_background_->SetRoundedCorner(
+      gfx::RoundedCornersF(corner_radius));
 
   // 2. Icon
   float icon_x_pos;
diff --git a/chrome/browser/android/compositor/scene_layer/tab_strip_scene_layer.h b/chrome/browser/android/compositor/scene_layer/tab_strip_scene_layer.h
index 957db7e0..63a1af7b 100644
--- a/chrome/browser/android/compositor/scene_layer/tab_strip_scene_layer.h
+++ b/chrome/browser/android/compositor/scene_layer/tab_strip_scene_layer.h
@@ -88,10 +88,10 @@
 
   void UpdateGlicButton(JNIEnv* env,
                         int32_t resource_id,
-                        int32_t bg_resource_id,
                         float x,
                         float y,
                         float button_width,
+                        float button_height,
                         bool visible,
                         bool should_apply_hover_highlight,
                         int32_t tint,
@@ -102,7 +102,8 @@
                         int32_t keyboard_focus_ring_color,
                         int32_t text_texture_id,
                         float button_start_padding,
-                        float icon_text_padding);
+                        float icon_text_padding,
+                        float corner_radius);
 
   void UpdateModelSelectorButton(JNIEnv* env,
                                  int32_t resource_id,
@@ -253,7 +254,7 @@
   scoped_refptr<cc::slim::SolidColorLayer> right_padding_layer_;
 
   scoped_refptr<cc::slim::UIResourceLayer> glic_button_;
-  scoped_refptr<cc::slim::UIResourceLayer> glic_button_background_;
+  scoped_refptr<cc::slim::SolidColorLayer> glic_button_background_;
   scoped_refptr<cc::slim::UIResourceLayer> glic_button_text_;
   scoped_refptr<cc::slim::UIResourceLayer> glic_button_keyboard_focus_ring_;
 
diff --git a/chrome/browser/android/media_state_observer.cc b/chrome/browser/android/media_state_observer.cc
index b907e75b..505f2728 100644
--- a/chrome/browser/android/media_state_observer.cc
+++ b/chrome/browser/android/media_state_observer.cc
@@ -98,15 +98,16 @@
 
 void MediaStateObserver::UpdateMediaState() {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-  MediaState new_state = MediaState::NONE;
+  tabs::MediaState new_state = tabs::MediaState::kNone;
   if (is_being_mirrored_) {
-    new_state = MediaState::SHARING;
+    new_state = tabs::MediaState::kSharing;
   } else if (is_capturing_video_ || is_capturing_audio_) {
-    new_state = MediaState::RECORDING;
+    new_state = tabs::MediaState::kRecording;
   } else if (is_audible_) {
-    new_state = is_audio_muted_ ? MediaState::MUTED : MediaState::AUDIBLE;
+    new_state =
+        is_audio_muted_ ? tabs::MediaState::kMuted : tabs::MediaState::kAudible;
   } else {
-    new_state = MediaState::NONE;
+    new_state = tabs::MediaState::kNone;
   }
 
   if (media_state_ == new_state) {
@@ -119,7 +120,7 @@
   }
 
   media_state_ = new_state;
-  tab->SetMediaState(new_state);
+  tab->SetMediaState(static_cast<int>(new_state));
 }
 
 WEB_CONTENTS_USER_DATA_KEY_IMPL(MediaStateObserver);
diff --git a/chrome/browser/android/media_state_observer.h b/chrome/browser/android/media_state_observer.h
index 2850fa67..59e84996 100644
--- a/chrome/browser/android/media_state_observer.h
+++ b/chrome/browser/android/media_state_observer.h
@@ -9,6 +9,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/scoped_observation.h"
 #include "chrome/browser/media/webrtc/media_stream_capture_indicator.h"
+#include "chrome/browser/tab/media_state.h"
 #include "content/public/browser/web_contents_observer.h"
 #include "content/public/browser/web_contents_user_data.h"
 
@@ -23,15 +24,6 @@
       public content::WebContentsUserData<MediaStateObserver>,
       public MediaStreamCaptureIndicator::Observer {
  public:
-  // Values defined in Tab.java and must be kept in sync.
-  enum MediaState {
-    NONE = 0,
-    MUTED = 1,
-    AUDIBLE = 2,
-    RECORDING = 3,
-    SHARING = 4,
-  };
-
   explicit MediaStateObserver(content::WebContents* web_contents);
   ~MediaStateObserver() override;
 
@@ -79,7 +71,7 @@
   bool is_audio_muted_ = false;
   bool is_audible_ = false;
 
-  MediaState media_state_ = NONE;
+  tabs::MediaState media_state_ = tabs::MediaState::kNone;
 
   // Subscription to be notified when the recently audible state has changed.
   const base::CallbackListSubscription recently_audible_subscription_;
diff --git a/chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge_unittest.cc b/chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge_unittest.cc
index b6e795b..d86c22bc 100644
--- a/chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge_unittest.cc
+++ b/chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge_unittest.cc
@@ -454,7 +454,7 @@
 
   // Prepare widget to hold it.
   std::unique_ptr<views::Widget> widget =
-      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+      CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
   widget->widget_delegate()->SetCanActivate(false);
   widget->Deactivate();
   widget->SetContentsView(std::move(notification_view));
@@ -519,7 +519,7 @@
 
   // Prepare a widget to hold them.
   std::unique_ptr<views::Widget> widget =
-      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+      CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
   ArcNotificationView* notification_view =
       widget->GetRootView()->AddChildView(std::move(owning_notification_view));
   views::View* focus_stealer =
diff --git a/chrome/browser/ash/arc/nearby_share/ui/nearby_share_overlay_view_unittest.cc b/chrome/browser/ash/arc/nearby_share/ui/nearby_share_overlay_view_unittest.cc
index 36a5184..0fe910b 100644
--- a/chrome/browser/ash/arc/nearby_share/ui/nearby_share_overlay_view_unittest.cc
+++ b/chrome/browser/ash/arc/nearby_share/ui/nearby_share_overlay_view_unittest.cc
@@ -31,7 +31,7 @@
     const bool has_dialog_view = !!dialog_view;
 
     auto widget =
-        CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+        CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
     widget->SetContentsView(CreateTestOverlayView(std::move(dialog_view)));
 
     const auto& view_ax = widget->GetRootView()->GetViewAccessibility();
diff --git a/chrome/browser/ash/arc/nearby_share/ui/progress_bar_dialog_view_unittest.cc b/chrome/browser/ash/arc/nearby_share/ui/progress_bar_dialog_view_unittest.cc
index 4ec3823..13a105c 100644
--- a/chrome/browser/ash/arc/nearby_share/ui/progress_bar_dialog_view_unittest.cc
+++ b/chrome/browser/ash/arc/nearby_share/ui/progress_bar_dialog_view_unittest.cc
@@ -20,7 +20,7 @@
   void SetUp() override {
     CompatModeTestBase::SetUp();
     widget_ =
-        CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+        CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
     dialog_view_ = widget_->SetContentsView(
         std::make_unique<ProgressBarDialogView>(/*is_multiple_files=*/false));
     widget_->Show();
diff --git a/chrome/browser/ash/power/ml/adaptive_screen_brightness_manager_unittest.cc b/chrome/browser/ash/power/ml/adaptive_screen_brightness_manager_unittest.cc
index cefa41e..7db1300a 100644
--- a/chrome/browser/ash/power/ml/adaptive_screen_brightness_manager_unittest.cc
+++ b/chrome/browser/ash/power/ml/adaptive_screen_brightness_manager_unittest.cc
@@ -22,6 +22,7 @@
 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
 #include "chrome/test/base/test_browser_window_aura.h"
 #include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/ui_test_utils.h"
 #include "chromeos/dbus/power/fake_power_manager_client.h"
 #include "chromeos/dbus/power/power_manager_client.h"
 #include "chromeos/dbus/power_manager/backlight.pb.h"
@@ -632,7 +633,7 @@
 TEST_F(AdaptiveScreenBrightnessManagerTest, DISABLED_SingleBrowser) {
   std::unique_ptr<Browser> browser =
       CreateTestBrowser(true /* is_visible */, true /* is_focused */);
-  BrowserList::GetInstance()->SetLastActive(browser.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser.get());
   TabStripModel* tab_strip_model = browser->tab_strip_model();
   CreateTestWebContents(tab_strip_model, kUrl1, false /* is_active */);
   const ukm::SourceId source_id2 =
@@ -664,9 +665,9 @@
   std::unique_ptr<Browser> browser3 =
       CreateTestBrowser(true /* is_visible */, false /* is_focused */);
 
-  BrowserList::GetInstance()->SetLastActive(browser3.get());
-  BrowserList::GetInstance()->SetLastActive(browser2.get());
-  BrowserList::GetInstance()->SetLastActive(browser1.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser3.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser2.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser1.get());
 
   TabStripModel* tab_strip_model1 = browser1->tab_strip_model();
   CreateTestWebContents(tab_strip_model1, kUrl1, true /* is_active */);
@@ -706,9 +707,9 @@
   std::unique_ptr<Browser> browser3 =
       CreateTestBrowser(true /* is_visible */, false /* is_focused */);
 
-  BrowserList::GetInstance()->SetLastActive(browser3.get());
-  BrowserList::GetInstance()->SetLastActive(browser2.get());
-  BrowserList::GetInstance()->SetLastActive(browser1.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser3.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser2.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser1.get());
 
   TabStripModel* tab_strip_model1 = browser1->tab_strip_model();
   CreateTestWebContents(tab_strip_model1, kUrl1, true /* is_active */);
@@ -747,9 +748,9 @@
   std::unique_ptr<Browser> browser3 = CreateTestBrowser(
       true /* is_visible */, true /* is_focused */, true /* is_incognito */);
 
-  BrowserList::GetInstance()->SetLastActive(browser3.get());
-  BrowserList::GetInstance()->SetLastActive(browser2.get());
-  BrowserList::GetInstance()->SetLastActive(browser1.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser3.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser2.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser1.get());
 
   TabStripModel* tab_strip_model1 = browser1->tab_strip_model();
   CreateTestWebContents(tab_strip_model1, kUrl1, true /* is_active */);
diff --git a/chrome/browser/ash/power/ml/user_activity_manager_unittest.cc b/chrome/browser/ash/power/ml/user_activity_manager_unittest.cc
index c63ad33..9274d1c 100644
--- a/chrome/browser/ash/power/ml/user_activity_manager_unittest.cc
+++ b/chrome/browser/ash/power/ml/user_activity_manager_unittest.cc
@@ -30,6 +30,7 @@
 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
 #include "chrome/test/base/test_browser_window_aura.h"
 #include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/ui_test_utils.h"
 #include "chromeos/ash/components/install_attributes/stub_install_attributes.h"
 #include "chromeos/dbus/power/fake_power_manager_client.h"
 #include "chromeos/dbus/power_manager/idle.pb.h"
@@ -1272,7 +1273,7 @@
 
   std::unique_ptr<Browser> browser =
       CreateTestBrowser(true /* is_visible */, true /* is_focused */);
-  BrowserList::GetInstance()->SetLastActive(browser.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser.get());
   TabStripModel* tab_strip_model = browser->tab_strip_model();
   const ukm::SourceId source_id1 = CreateTestWebContents(
       tab_strip_model, url1_, true /* is_active */, "application/pdf");
@@ -1313,9 +1314,9 @@
   std::unique_ptr<Browser> browser3 =
       CreateTestBrowser(true /* is_visible */, false /* is_focused */);
 
-  BrowserList::GetInstance()->SetLastActive(browser3.get());
-  BrowserList::GetInstance()->SetLastActive(browser2.get());
-  BrowserList::GetInstance()->SetLastActive(browser1.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser3.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser2.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser1.get());
 
   TabStripModel* tab_strip_model1 = browser1->tab_strip_model();
   CreateTestWebContents(tab_strip_model1, url1_, false /* is_active */);
@@ -1352,7 +1353,7 @@
 
   std::unique_ptr<Browser> browser = CreateTestBrowser(
       true /* is_visible */, true /* is_focused */, true /* is_incognito */);
-  BrowserList::GetInstance()->SetLastActive(browser.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser.get());
 
   TabStripModel* tab_strip_model = browser->tab_strip_model();
   CreateTestWebContents(tab_strip_model, url1_, true /* is_active */);
diff --git a/chrome/browser/bookmarks/android/java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarMediator.java b/chrome/browser/bookmarks/android/java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarMediator.java
index 773f334..33cfdafb 100644
--- a/chrome/browser/bookmarks/android/java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarMediator.java
+++ b/chrome/browser/bookmarks/android/java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarMediator.java
@@ -102,7 +102,7 @@
     private final RecyclerView mItemsRecyclerView;
     private final BookmarkBar mBookmarkBarView;
     private @StyleRes int mCurrentTextStyleRes = R.style.TextAppearance_TextMedium_Primary_Baseline;
-    @ColorRes private int mCurrentIconTintRes = R.color.default_icon_color_tint_list;
+    private @ColorRes int mCurrentIconTintRes = R.color.default_icon_color_tint_list;
     @DrawableRes private int mCurrentBackgroundId;
 
     // The popup window that displays the contents of a bookmark folder. Instantiated in {@code
diff --git a/chrome/browser/chromeos/policy/dlp/data_transfer_dlp_controller_browsertest.cc b/chrome/browser/chromeos/policy/dlp/data_transfer_dlp_controller_browsertest.cc
index 8bd4ebc4..d7bd63f 100644
--- a/chrome/browser/chromeos/policy/dlp/data_transfer_dlp_controller_browsertest.cc
+++ b/chrome/browser/chromeos/policy/dlp/data_transfer_dlp_controller_browsertest.cc
@@ -256,7 +256,7 @@
     widget_ = std::make_unique<views::Widget>();
 
     views::Widget::InitParams params(
-        views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
+        views::Widget::InitParams::CLIENT_OWNS_WIDGET,
         views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
     widget_->Init(std::move(params));
     textfield_ = widget_->SetContentsView(std::make_unique<views::Textfield>());
diff --git a/chrome/browser/context_sharing/tab_bottom_sheet/android/java/res/layout/tab_bottom_sheet.xml b/chrome/browser/context_sharing/tab_bottom_sheet/android/java/res/layout/tab_bottom_sheet.xml
index 959d8fb..0ccfee87 100644
--- a/chrome/browser/context_sharing/tab_bottom_sheet/android/java/res/layout/tab_bottom_sheet.xml
+++ b/chrome/browser/context_sharing/tab_bottom_sheet/android/java/res/layout/tab_bottom_sheet.xml
@@ -4,45 +4,62 @@
 Use of this source code is governed by a BSD-style license that can be
 found in the LICENSE file.
 -->
-<LinearLayout
+<FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/tab_bottom_sheet"
     tools:ignore="HardcodedText"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
+    android:layout_height="match_parent">
 
-    <!-- Handle Bar -->
-    <ImageView
-        android:id="@+id/handle_bar"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center_horizontal"
-        android:layout_marginTop="7dp"
-        android:src="@drawable/drag_handlebar"
-        app:tint="@macro/drag_handle_color"
-        android:importantForAccessibility="no"/>
-
-    <!-- Toolbar Container -->
-    <FrameLayout
-        android:id="@+id/toolbar_container"
+    <!-- Tab Bottom Sheet Content: Appears when BottomSheet is expanded -->
+    <LinearLayout
+        android:id="@+id/expanded_content_group"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:visibility="visible">
 
-    <!-- Web UI Container -->
-    <FrameLayout
-        android:id="@+id/web_ui_container"
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1"
-        android:padding="5dp" />
+        <!-- Handle Bar -->
+        <ImageView
+            android:id="@+id/handle_bar"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:layout_marginTop="7dp"
+            android:src="@drawable/drag_handlebar"
+            app:tint="@macro/drag_handle_color"
+            android:importantForAccessibility="no"/>
 
-    <!-- Fusebox Container -->
+        <!-- Toolbar Container -->
+        <FrameLayout
+            android:id="@+id/toolbar_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <!-- Web UI Container -->
+        <FrameLayout
+            android:id="@+id/web_ui_container"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:padding="5dp" />
+
+        <!-- Fusebox Container -->
+        <FrameLayout
+            android:id="@+id/fusebox_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:padding="5dp" />
+    </LinearLayout>
+
+    <!-- Actor Control View: Appears when BottomSheet is collapsed -->
     <FrameLayout
-        android:id="@+id/fusebox_container"
+        android:id="@+id/actor_control_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:padding="5dp" />
-
-</LinearLayout>
+        android:layout_gravity="top"
+        android:visibility="gone">
+    </FrameLayout>
+</FrameLayout>
diff --git a/chrome/browser/contextual_tasks/contextual_tasks.mojom b/chrome/browser/contextual_tasks/contextual_tasks.mojom
index b5b8022..4948dff64 100644
--- a/chrome/browser/contextual_tasks/contextual_tasks.mojom
+++ b/chrome/browser/contextual_tasks/contextual_tasks.mojom
@@ -49,6 +49,15 @@
   int32? margin_left;
 };
 
+enum IconType {
+  kUnspecified = 0,
+  kAdd = 9,
+  kCheck = 30,
+  kFormatQuoteFilled = 79,
+  kImage = 84,
+  kDrivePdf = 101,
+};
+
 // Browser-side handler for requests from the WebUI page.
 // The WebUI page calls these methods to interact with the browser process.
 // (TypeScript -> C++)
@@ -189,12 +198,21 @@
   RestoreInput();
 
   // Called by the browser process (C++) when the AIM server sends an
-  // InjectInput command to inject an input into the compose box.
+  // InjectInput command to inject an input into the compose box and specifies
+  // a thumbnail source.
   InjectInput(
     string title,
     string thumbnail,
     mojo_base.mojom.UnguessableToken file_token);
 
+  // Called by the browser process (C++) when the AIM server sends an
+  // InjectInput command to inject an input into the compose box and specifies
+  // an icon type.
+  InjectInputWithIcon(
+    string title,
+    IconType icon_id,
+    mojo_base.mojom.UnguessableToken file_token);
+
   // Called by the browser process (C++) when the AIM server sends a
   // RemoveInjectedInput command to remove an injected input from the compose
   // box.
diff --git a/chrome/browser/contextual_tasks/contextual_tasks_page_handler.cc b/chrome/browser/contextual_tasks/contextual_tasks_page_handler.cc
index 3df5da0..99a139b 100644
--- a/chrome/browser/contextual_tasks/contextual_tasks_page_handler.cc
+++ b/chrome/browser/contextual_tasks/contextual_tasks_page_handler.cc
@@ -11,6 +11,7 @@
 #include "base/uuid.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/contextual_tasks/ai_mode_context_library_converter.h"
+#include "chrome/browser/contextual_tasks/contextual_tasks.mojom-shared.h"
 #include "chrome/browser/contextual_tasks/contextual_tasks_ui_service.h"
 #include "chrome/browser/contextual_tasks/contextual_tasks_utils.h"
 #include "chrome/browser/feedback/public/feedback_source.h"
@@ -110,6 +111,23 @@
   return context_items;
 }
 
+contextual_tasks::mojom::IconType IconTypeToMojom(lens::IconType icon_id) {
+  switch (icon_id) {
+    case lens::IconType::ICON_TYPE_ADD:
+      return contextual_tasks::mojom::IconType::kAdd;
+    case lens::IconType::ICON_TYPE_CHECK:
+      return contextual_tasks::mojom::IconType::kCheck;
+    case lens::IconType::ICON_TYPE_FORMAT_QUOTE_FILLED:
+      return contextual_tasks::mojom::IconType::kFormatQuoteFilled;
+    case lens::IconType::ICON_TYPE_IMAGE:
+      return contextual_tasks::mojom::IconType::kImage;
+    case lens::IconType::ICON_TYPE_DRIVE_PDF:
+      return contextual_tasks::mojom::IconType::kDrivePdf;
+    default:
+      return contextual_tasks::mojom::IconType::kUnspecified;
+  }
+}
+
 }  // namespace
 
 namespace contextual_tasks {
@@ -510,9 +528,15 @@
   contextual_search::ContextualSearchSessionHandle* handle =
       web_ui_controller_->GetOrCreateContextualSessionHandle();
   auto token = handle->CreateContextToken();
-  web_ui_controller_->GetPageRemote()->InjectInput(
-      std::string(modality->title()), std::string(modality->thumbnail_src()),
-      token);
+  if (modality->has_icon_id()) {
+    web_ui_controller_->GetPageRemote()->InjectInputWithIcon(
+        std::string(modality->title()), IconTypeToMojom(modality->icon_id()),
+        token);
+  } else {
+    web_ui_controller_->GetPageRemote()->InjectInput(
+        std::string(modality->title()), std::string(modality->thumbnail_src()),
+        token);
+  }
   // This does not actually upload anything, but allows the injected input to be
   // shown in the chip carousel in the UI.
   handle->StartModalityChipUploadFlow(token, std::move(modality));
diff --git a/chrome/browser/contextual_tasks/contextual_tasks_page_handler_unittest.cc b/chrome/browser/contextual_tasks/contextual_tasks_page_handler_unittest.cc
index 3187874..3f3fb87 100644
--- a/chrome/browser/contextual_tasks/contextual_tasks_page_handler_unittest.cc
+++ b/chrome/browser/contextual_tasks/contextual_tasks_page_handler_unittest.cc
@@ -13,6 +13,7 @@
 #include "base/test/scoped_feature_list.h"
 #include "base/unguessable_token.h"
 #include "chrome/browser/browser_process.h"
+#include "chrome/browser/contextual_tasks/contextual_tasks.mojom.h"
 #include "chrome/browser/contextual_tasks/contextual_tasks_service_factory.h"
 #include "chrome/browser/contextual_tasks/contextual_tasks_ui.h"
 #include "chrome/browser/contextual_tasks/contextual_tasks_ui_service.h"
@@ -96,6 +97,12 @@
                const base::UnguessableToken& file_token),
               (override));
   MOCK_METHOD(void,
+              InjectInputWithIcon,
+              (const std::string& title,
+               contextual_tasks::mojom::IconType icon_id,
+               const base::UnguessableToken& file_token),
+              (override));
+  MOCK_METHOD(void,
               RemoveInjectedInput,
               (const base::UnguessableToken& file_token),
               (override));
diff --git a/chrome/browser/contextual_tasks/contextual_tasks_ui_browsertest.cc b/chrome/browser/contextual_tasks/contextual_tasks_ui_browsertest.cc
index 85f0b68..a092fd6 100644
--- a/chrome/browser/contextual_tasks/contextual_tasks_ui_browsertest.cc
+++ b/chrome/browser/contextual_tasks/contextual_tasks_ui_browsertest.cc
@@ -100,6 +100,12 @@
                const base::UnguessableToken& file_token),
               (override));
   MOCK_METHOD(void,
+              InjectInputWithIcon,
+              (const std::string& title,
+               contextual_tasks::mojom::IconType icon_id,
+               const base::UnguessableToken& file_token),
+              (override));
+  MOCK_METHOD(void,
               RemoveInjectedInput,
               (const base::UnguessableToken& file_token),
               (override));
diff --git a/chrome/browser/device_notifications/device_connection_tracker_unittest.cc b/chrome/browser/device_notifications/device_connection_tracker_unittest.cc
index 978e1ec..32b6790 100644
--- a/chrome/browser/device_notifications/device_connection_tracker_unittest.cc
+++ b/chrome/browser/device_notifications/device_connection_tracker_unittest.cc
@@ -12,6 +12,7 @@
 #include "chrome/browser/extensions/test_extension_system.h"
 #include "chrome/browser/ui/browser_list.h"
 #include "chrome/test/base/testing_profile_manager.h"
+#include "chrome/test/base/ui_test_utils.h"
 #include "extensions/browser/extension_registrar.h"
 
 namespace {
@@ -34,7 +35,7 @@
 
 void DeviceConnectionTrackerTestBase::SetUp() {
   BrowserWithTestWindowTest::SetUp();
-  BrowserList::SetLastActive(browser());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser());
 }
 
 Profile* DeviceConnectionTrackerTestBase::CreateTestingProfile(
diff --git a/chrome/browser/devtools/inspector_protocol_config.json b/chrome/browser/devtools/inspector_protocol_config.json
index 6712a74..ba645de 100644
--- a/chrome/browser/devtools/inspector_protocol_config.json
+++ b/chrome/browser/devtools/inspector_protocol_config.json
@@ -28,7 +28,7 @@
             },
             {
                 "domain": "Emulation",
-                "include": ["disable", "setAutomationOverride", "getScreenInfos", "addScreen", "removeScreen"]
+                "include": ["disable", "setAutomationOverride", "getScreenInfos", "addScreen", "removeScreen", "setPrimaryScreen"]
             },
             {
                 "domain": "WindowManager"
diff --git a/chrome/browser/devtools/protocol/emulation_handler.cc b/chrome/browser/devtools/protocol/emulation_handler.cc
index 9abc875..7705f44 100644
--- a/chrome/browser/devtools/protocol/emulation_handler.cc
+++ b/chrome/browser/devtools/protocol/emulation_handler.cc
@@ -250,6 +250,25 @@
   return Response::Success();
 }
 
+Response EmulationHandler::SetPrimaryScreen(const protocol::String& screen_id) {
+  if (!display::Screen::Get()->IsHeadless()) {
+    return Response::ServerError("Method is only available in headless mode");
+  }
+
+  int64_t display_id;
+  if (!base::StringToInt64(screen_id, &display_id)) {
+    return Response::InvalidParams("Invalid screen id: " + screen_id);
+  }
+
+  if (!GetDisplay(display_id)) {
+    return Response::InvalidParams("Unknown screen id: " + screen_id);
+  }
+
+  display::HeadlessScreenManager::Get()->SetPrimaryDisplay(display_id);
+
+  return Response::Success();
+}
+
 infobars::ContentInfoBarManager* EmulationHandler::GetContentInfoBarManager() {
   content::WebContents* web_contents = agent_host_->GetWebContents();
   if (!web_contents) {
diff --git a/chrome/browser/devtools/protocol/emulation_handler.h b/chrome/browser/devtools/protocol/emulation_handler.h
index 269d623ab..6b0e74a 100644
--- a/chrome/browser/devtools/protocol/emulation_handler.h
+++ b/chrome/browser/devtools/protocol/emulation_handler.h
@@ -41,6 +41,8 @@
       std::unique_ptr<protocol::Emulation::ScreenInfo>* out_screen_info)
       override;
   protocol::Response RemoveScreen(const protocol::String& screen_id) override;
+  protocol::Response SetPrimaryScreen(
+      const protocol::String& screen_id) override;
 
   void OnInfoBarRemoved(infobars::InfoBar* infobar, bool animate) override;
 
diff --git a/chrome/browser/digital_credentials/BUILD.gn b/chrome/browser/digital_credentials/BUILD.gn
index 8e8f1af..d407405 100644
--- a/chrome/browser/digital_credentials/BUILD.gn
+++ b/chrome/browser/digital_credentials/BUILD.gn
@@ -1,14 +1,83 @@
-import("//build/config/android/rules.gni")
+if (is_android) {
+  import("//build/config/android/rules.gni")
+}
+
+source_set("digital_credentials") {
+  sources = [
+    "digital_credentials_keyed_service.h",
+    "digital_identity_interstitial_closed_reason.h",
+    "digital_identity_low_risk_origins.h",
+  ]
+  public_deps = [
+    "//base",
+    "//chrome/browser/profiles:profile",
+    "//components/keyed_service/core",
+    "//url",
+  ]
+
+  if (is_android) {
+    sources += [ "digital_identity_provider_android.h" ]
+    public_deps += [ "//content/public/browser" ]
+  } else {
+    sources += [ "digital_identity_provider_desktop.h" ]
+    public_deps += [ "//content/public/browser" ]
+  }
+}
+
+source_set("impl") {
+  sources = [
+    "digital_credentials_keyed_service.cc",
+    "digital_identity_low_risk_origins.cc",
+  ]
+  deps = [
+    ":digital_credentials",
+    "//base",
+    "//chrome/browser/optimization_guide",
+    "//chrome/browser/profiles:profile",
+    "//components/keyed_service/content",
+    "//components/optimization_guide/core:features",
+    "//components/optimization_guide/proto:optimization_guide_proto",
+    "//content/public/browser",
+    "//url",
+  ]
+
+  if (is_android) {
+    sources += [ "digital_identity_provider_android.cc" ]
+    deps += [
+      "//chrome/browser/ui/digital_credentials",
+      "//chrome/browser/webid:jni_headers",
+      "//ui/android",
+    ]
+  } else {
+    sources += [ "digital_identity_provider_desktop.cc" ]
+    deps += [
+      "//chrome/browser/ui",
+      "//components/constrained_window",
+      "//components/qr_code_generator:bitmap_generator",
+      "//device/fido",
+      "//ui/base",
+      "//ui/views",
+    ]
+  }
+
+  public_deps = [ "//chrome/browser:browser_public_dependencies" ]
+}
 
 source_set("unit_tests") {
   testonly = true
-  sources = [ "digital_identity_provider_android_unittest.cc" ]
+  sources = [ "digital_identity_low_risk_origins_unittest.cc" ]
   deps = [
-    "//base",
-    "//base/test:test_support",
-    "//chrome/browser",
-    "//content/public/browser",
+    ":digital_credentials",
+    "//chrome/browser/optimization_guide",
+    "//chrome/browser/optimization_guide:test_support",
+    "//chrome/test:test_support",
+    "//components/optimization_guide/core",
+    "//content/test:test_support",
+    "//testing/gmock",
     "//testing/gtest",
-    "//url",
   ]
+
+  if (is_android) {
+    sources += [ "digital_identity_provider_android_unittest.cc" ]
+  }
 }
diff --git a/chrome/browser/enterprise/data_protection/data_protection_clipboard_utils.cc b/chrome/browser/enterprise/data_protection/data_protection_clipboard_utils.cc
index d5c2d13..90e4ec5f 100644
--- a/chrome/browser/enterprise/data_protection/data_protection_clipboard_utils.cc
+++ b/chrome/browser/enterprise/data_protection/data_protection_clipboard_utils.cc
@@ -944,12 +944,19 @@
     return;
   }
 
-  std::optional<ui::DataTransferEndpoint> source_dte =
-      clipboard->GetSource(ui::ClipboardBuffer::kCopyPaste);
-  content::GetSourceClipboardEndpoint(
-      base::OptionalToPtr(source_dte), ui::ClipboardBuffer::kCopyPaste,
-      base::BindOnce(&OnGetSourceClipboardEndpointForFindBar,
-                     std::move(*destination), std::move(callback)));
+  clipboard->GetSource(
+      ui::ClipboardBuffer::kCopyPaste,
+      base::BindOnce(
+          [](content::ClipboardEndpoint destination,
+             base::OnceCallback<void(std::optional<std::u16string>)> callback,
+             std::optional<ui::DataTransferEndpoint> source_dte) {
+            content::GetSourceClipboardEndpoint(
+                base::OptionalToPtr(source_dte),
+                ui::ClipboardBuffer::kCopyPaste,
+                base::BindOnce(&OnGetSourceClipboardEndpointForFindBar,
+                               std::move(destination), std::move(callback)));
+          },
+          std::move(*destination), std::move(callback)));
 }
 
 void OnGetSourceClipboardEndpointForFindBar(
diff --git a/chrome/browser/extensions/api/streams_private/OWNERS b/chrome/browser/extensions/api/streams_private/OWNERS
deleted file mode 100644
index 8192dfb0..0000000
--- a/chrome/browser/extensions/api/streams_private/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-zork@chromium.org
diff --git a/chrome/browser/extensions/api/streams_private/streams_private_manifest_unittest.cc b/chrome/browser/extensions/api/streams_private/streams_private_manifest_unittest.cc
deleted file mode 100644
index 5ae8c25..0000000
--- a/chrome/browser/extensions/api/streams_private/streams_private_manifest_unittest.cc
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2012 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include <utility>
-
-#include "base/strings/string_number_conversions.h"
-#include "base/values.h"
-#include "chrome/common/extensions/manifest_tests/chrome_manifest_test.h"
-#include "extensions/common/constants.h"
-#include "extensions/common/error_utils.h"
-#include "extensions/common/extension_builder.h"
-#include "extensions/common/manifest_constants.h"
-#include "extensions/common/manifest_handlers/mime_types_handler.h"
-#include "extensions/common/manifest_url_handlers.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace errors = extensions::manifest_errors;
-
-namespace extensions {
-namespace {
-
-using StreamsPrivateManifestTest = ChromeManifestTest;
-
-TEST_F(StreamsPrivateManifestTest, ValidMimeTypesHandlerMIMETypes) {
-  scoped_refptr<const Extension> extension =
-      ExtensionBuilder()
-          .SetID(extension_misc::kQuickOfficeExtensionId)
-          .SetManifest(
-              base::DictValue()
-                  .Set("name", "MIME type handler test")
-                  .Set("version", "1.0.0")
-                  .Set("manifest_version", 3)
-                  .Set("mime_types", base::ListValue().Append("text/plain")))
-          .Build();
-
-  ASSERT_TRUE(extension.get());
-  MimeTypesHandler* handler = MimeTypesHandler::GetHandler(extension.get());
-  ASSERT_TRUE(handler != nullptr);
-
-  EXPECT_FALSE(handler->CanHandleMIMEType("text/html"));
-  EXPECT_TRUE(handler->CanHandleMIMEType("text/plain"));
-}
-
-TEST_F(StreamsPrivateManifestTest, MimeTypesHandlerMIMETypesNotAllowlisted) {
-  scoped_refptr<const Extension> extension =
-      ExtensionBuilder()
-          .SetManifest(
-              base::DictValue()
-                  .Set("name", "MIME types test")
-                  .Set("version", "1.0.0")
-                  .Set("manifest_version", 3)
-                  .Set("mime_types", base::ListValue().Append("text/plain")))
-          .Build();
-
-  ASSERT_TRUE(extension.get());
-
-  MimeTypesHandler* handler = MimeTypesHandler::GetHandler(extension.get());
-  ASSERT_TRUE(handler == nullptr);
-}
-
-}  // namespace
-}  // namespace extensions
diff --git a/chrome/browser/extensions/api/tab_capture/tab_capture_apitest.cc b/chrome/browser/extensions/api/tab_capture/tab_capture_apitest.cc
index f440a89..a856688 100644
--- a/chrome/browser/extensions/api/tab_capture/tab_capture_apitest.cc
+++ b/chrome/browser/extensions/api/tab_capture/tab_capture_apitest.cc
@@ -202,7 +202,7 @@
 // events to the onStatusChange listener.  The test loads a page that toggles
 // fullscreen mode, using the Fullscreen Javascript API, in response to mouse
 // clicks. The fullscreen API requires a user gesture.
-// TODO(crbug.com/427298135): Port to desktop Android. Currently times out
+// TODO(crbug.com/489494749): Port to desktop Android. Currently times out
 // without useful stack or logs.
 IN_PROC_BROWSER_TEST_F(TabCaptureApiTest, FullscreenEvents) {
   AddExtensionToCommandLineAllowlist();
diff --git a/chrome/browser/extensions/api/tab_groups/tab_groups_api_browsertest.cc b/chrome/browser/extensions/api/tab_groups/tab_groups_api_browsertest.cc
index fcdc0ea1..d8b5d31ed 100644
--- a/chrome/browser/extensions/api/tab_groups/tab_groups_api_browsertest.cc
+++ b/chrome/browser/extensions/api/tab_groups/tab_groups_api_browsertest.cc
@@ -212,7 +212,7 @@
   // Create a new window that doesn't support groups. App windows don't allow
   // tab groups.
   Browser* browser2 = CreateBrowserForApp("some app", profile());
-  BrowserList::SetLastActive(browser2);
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser2);
 
   ASSERT_FALSE(browser2->tab_strip_model()->SupportsTabGroups());
 
diff --git a/chrome/browser/extensions/api/tabs/tabs_api_unittest.cc b/chrome/browser/extensions/api/tabs/tabs_api_unittest.cc
index 4ae55c9..a3549be 100644
--- a/chrome/browser/extensions/api/tabs/tabs_api_unittest.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_api_unittest.cc
@@ -32,6 +32,7 @@
 #include "chrome/browser/ui/ui_features.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/test/base/test_browser_window.h"
+#include "chrome/test/base/ui_test_utils.h"
 #include "components/saved_tab_groups/public/features.h"
 #include "components/saved_tab_groups/public/saved_tab_group.h"
 #include "components/saved_tab_groups/public/tab_group_sync_service.h"
@@ -842,7 +843,7 @@
   params.type = Browser::TYPE_NORMAL;
   params.window = window2.release();
   auto browser2 = Browser::DeprecatedCreateOwnedForTesting(params);
-  BrowserList::SetLastActive(browser2.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser2.get());
   int window_id2 = ExtensionTabUtil::GetWindowId(browser2.get());
 
   constexpr int kNumTabs2 = 3;
@@ -910,7 +911,7 @@
   params.type = Browser::TYPE_NORMAL;
   params.window = window2.release();
   auto browser2 = Browser::DeprecatedCreateOwnedForTesting(params);
-  BrowserList::SetLastActive(browser2.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser2.get());
   int window_id2 = ExtensionTabUtil::GetWindowId(browser2.get());
 
   constexpr int kNumTabs2 = 3;
diff --git a/chrome/browser/extensions/api/tabs/tabs_event_router_platform_delegate_non_android.cc b/chrome/browser/extensions/api/tabs/tabs_event_router_platform_delegate_non_android.cc
index d748406..7a9fb97 100644
--- a/chrome/browser/extensions/api/tabs/tabs_event_router_platform_delegate_non_android.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_event_router_platform_delegate_non_android.cc
@@ -25,10 +25,10 @@
 #include "chrome/browser/resource_coordinator/tab_lifecycle_unit_source.h"
 #include "chrome/browser/resource_coordinator/utils.h"
 #include "chrome/browser/ui/browser.h"
-#include "chrome/browser/ui/browser_list.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_features.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
+#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
 #include "chrome/browser/ui/recently_audible_helper.h"
 #include "chrome/browser/ui/tabs/tab_group_model.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
@@ -66,7 +66,8 @@
       browser_tab_strip_tracker_(this, this) {
   DCHECK(!profile.IsOffTheRecord());
 
-  BrowserList::AddObserver(this);
+  browser_collection_observation_.Observe(
+      GlobalBrowserCollection::GetInstance());
   browser_tab_strip_tracker_.Init();
 
   // Track any existing browsers. The `browser_tab_strip_tracker_` does this for
@@ -74,7 +75,7 @@
   // TabListInterface.
   ForEachCurrentAndNewBrowserWindowInterfaceOrderedByActivation(
       [this](BrowserWindowInterface* browser) {
-        OnBrowserAdded(browser->GetBrowserForMigrationOnly());
+        OnBrowserCreated(browser);
         return true;  // Keep iterating.
       });
 
@@ -82,24 +83,24 @@
       resource_coordinator::GetTabLifecycleUnitSource());
 }
 
-TabsEventRouterPlatformDelegate::~TabsEventRouterPlatformDelegate() {
-  BrowserList::RemoveObserver(this);
-}
+TabsEventRouterPlatformDelegate::~TabsEventRouterPlatformDelegate() = default;
 
 bool TabsEventRouterPlatformDelegate::ShouldTrackBrowser(
     BrowserWindowInterface* browser) {
   return router_->ShouldTrackBrowser(*browser);
 }
 
-void TabsEventRouterPlatformDelegate::OnBrowserSetLastActive(Browser* browser) {
+void TabsEventRouterPlatformDelegate::OnBrowserActivated(
+    BrowserWindowInterface* browser) {
   TabsWindowsAPI* tabs_window_api = TabsWindowsAPI::Get(&(*profile_));
   if (tabs_window_api) {
     tabs_window_api->windows_event_router()->OnActiveWindowChanged(
-        browser ? BrowserExtensionWindowController::From(browser) : nullptr);
+        BrowserExtensionWindowController::From(browser));
   }
 }
 
-void TabsEventRouterPlatformDelegate::OnBrowserAdded(Browser* browser) {
+void TabsEventRouterPlatformDelegate::OnBrowserCreated(
+    BrowserWindowInterface* browser) {
   if (ShouldTrackBrowser(browser)) {
     TabListInterface* tab_list = TabListInterface::From(browser);
     CHECK(tab_list);
diff --git a/chrome/browser/extensions/api/tabs/tabs_event_router_platform_delegate_non_android.h b/chrome/browser/extensions/api/tabs/tabs_event_router_platform_delegate_non_android.h
index 7b6f564..ed95956 100644
--- a/chrome/browser/extensions/api/tabs/tabs_event_router_platform_delegate_non_android.h
+++ b/chrome/browser/extensions/api/tabs/tabs_event_router_platform_delegate_non_android.h
@@ -14,9 +14,9 @@
 #include "base/scoped_observation.h"
 #include "chrome/browser/extensions/api/tabs/tabs_api.h"
 #include "chrome/browser/resource_coordinator/lifecycle_unit_observer.h"
-#include "chrome/browser/ui/browser_list_observer.h"
 #include "chrome/browser/ui/browser_tab_strip_tracker.h"
 #include "chrome/browser/ui/browser_tab_strip_tracker_delegate.h"
+#include "chrome/browser/ui/browser_window/public/browser_collection_observer.h"
 #include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
 #include "content/public/browser/web_contents_observer.h"
 #include "extensions/browser/event_router.h"
@@ -29,6 +29,8 @@
 class TabLifecycleUnitSource;
 }
 
+class GlobalBrowserCollection;
+
 namespace extensions {
 class TabsEventRouter;
 
@@ -38,7 +40,7 @@
 class TabsEventRouterPlatformDelegate
     : public TabStripModelObserver,
       public BrowserTabStripTrackerDelegate,
-      public BrowserListObserver,
+      public BrowserCollectionObserver,
       public resource_coordinator::LifecycleUnitObserver {
  public:
   TabsEventRouterPlatformDelegate(TabsEventRouter& router, Profile& profile);
@@ -53,9 +55,9 @@
   // BrowserTabStripTrackerDelegate:
   bool ShouldTrackBrowser(BrowserWindowInterface* browser) override;
 
-  // BrowserListObserver:
-  void OnBrowserSetLastActive(Browser* browser) override;
-  void OnBrowserAdded(Browser* browser) override;
+  // BrowserCollectionObserver:
+  void OnBrowserActivated(BrowserWindowInterface* browser) override;
+  void OnBrowserCreated(BrowserWindowInterface* browser) override;
 
   // TabStripModelObserver:
   void OnTabStripModelChanged(
@@ -103,6 +105,9 @@
   base::ScopedObservation<resource_coordinator::TabLifecycleUnitSource,
                           resource_coordinator::LifecycleUnitObserver>
       tab_source_scoped_observation_{this};
+
+  base::ScopedObservation<GlobalBrowserCollection, BrowserCollectionObserver>
+      browser_collection_observation_{this};
 };
 
 }  // namespace extensions
diff --git a/chrome/browser/feedback/report_unsafe_site_dialog_views_interactive_uitest.cc b/chrome/browser/feedback/report_unsafe_site_dialog_views_interactive_uitest.cc
index fad0b03..7a3d556 100644
--- a/chrome/browser/feedback/report_unsafe_site_dialog_views_interactive_uitest.cc
+++ b/chrome/browser/feedback/report_unsafe_site_dialog_views_interactive_uitest.cc
@@ -119,7 +119,7 @@
   ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
   RunTestSequence(
       ExecuteReportUnsafeSiteCommand(), WaitForDialog(),
-      CheckJsResultAt(
+      WaitForJsResultAt(
           kDialogWebviewId,
           {"report-unsafe-site-app", ".url-input-container", "input"},
           "(el) => el.value", ::testing::Eq(formatted_origin)),
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index ff66e2eb..6f86707 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -4997,8 +4997,19 @@
     "expiry_milestone": 155
   },
   {
+    "name": "fullscreen-video-picture-in-picture",
+    "owners": [
+      "philyan@chromium.org",
+      "media-dev@chromium.org"
+    ],
+    "expiry_milestone": 160
+  },
+  {
     "name": "fullscreen-viewport-adjustment-experiment",
-    "owners": [ "thegreenfrog@chromium.org", "bling-flags@google.com" ],
+    "owners": [
+      "thegreenfrog@chromium.org",
+      "bling-flags@google.com"
+    ],
     // Needed for manual testing of fallback flow on iOS.
     "expiry_milestone": -1
   },
@@ -9003,6 +9014,12 @@
     "expiry_milestone": 170
   },
   {
+    "name": "summarizer-api-performance-preference",
+    "owners": [
+      "//chrome/browser/ai/OWNERS", "builtin-ai-team@google.com", "jaeone@google.com" ],
+    "expiry_milestone": 159
+  },
+  {
     "name": "supervised-user-block-interstitial-v3",
     "owners": [
       "ddac@google.com",
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 3e9bd983..b1480f4 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -4768,6 +4768,15 @@
     "You must comply with our Prohibited Use Policy [2] which provides "
     "additional details about appropriate use of Generative AI.";
 
+inline constexpr char kSummarizerAPIWithPerformancePreferenceName[] =
+    "Summarizer API with Performance Preference";
+inline constexpr char kSummarizerAPIWithPerformancePreferenceDescription[] =
+    "Adds the \"preference\" create option to the Summarizer API. Allows the "
+    "developer to choose between \"capability\", \"speed\", and \"auto\" to "
+    "hint at the desired balance between performance and capability.";
+inline constexpr const char* kSummarizerAPIWithPerformancePreferenceLink[1] = {
+    "https://chromestatus.com/feature/6309243756085248"};
+
 inline constexpr char kOnDeviceModelLitertLmBackendName[] =
     "LiteRT-LM for On-Device AI";
 inline constexpr char kOnDeviceModelLitertLmBackendDescription[] =
@@ -5467,6 +5476,11 @@
     "Migration from View#setSystemUiVisibility to WindowInsetsController on "
     "automotive.";
 
+inline constexpr char kFullscreenVideoPictureInPictureName[] =
+    "Fullscreen Video Picture-in-Picture";
+inline constexpr char kFullscreenVideoPictureInPictureDescription[] =
+    "Enables fullscreen video Picture-in-Picture on Android.";
+
 inline constexpr char kGridTabSwitcherSurfaceColorUpdateName[] =
     "Grid tab switcher surface color update";
 inline constexpr char kGridTabSwitcherSurfaceColorUpdateDescription[] =
diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc
index 6b2f95c..8b845165 100644
--- a/chrome/browser/flags/android/chrome_feature_list.cc
+++ b/chrome/browser/flags/android/chrome_feature_list.cc
@@ -464,6 +464,7 @@
     &media::kAutoDocPiPPermissionPromptAndroid,
     &media::kAutoPictureInPictureAndroid,
     &media::kContextMenuPictureInPictureAndroid,
+    &media::kFullscreenVideoPictureInPicture,
     &net::features::kVerifyQWACs,
     &network::features::kLocalNetworkAccessChecks,
     &network::features::kLocalNetworkAccessChecksSplitPermissions,
diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
index fd05647..80db548 100644
--- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
+++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
@@ -458,6 +458,8 @@
     public static final String FULLSCREEN_INSETS_API_MIGRATION = "FullscreenInsetsApiMigration";
     public static final String FULLSCREEN_INSETS_API_MIGRATION_ON_AUTOMOTIVE =
             "FullscreenInsetsApiMigrationOnAutomotive";
+    public static final String FULLSCREEN_VIDEO_PICTURE_IN_PICTURE =
+            "FullscreenVideoPictureInPicture";
     public static final String GLIC = "Glic";
     public static final String GRID_TAB_SWITCHER_SURFACE_COLOR_UPDATE =
             "GridTabSwitcherSurfaceColorUpdate";
diff --git a/chrome/browser/glic/API_OWNERS b/chrome/browser/glic/API_OWNERS
new file mode 100644
index 0000000..342cf87
--- /dev/null
+++ b/chrome/browser/glic/API_OWNERS
@@ -0,0 +1,5 @@
+# API owners for sensitive features in glic (e.g. auto submit).
+wry@chromium.org
+carlosk@chromium.org
+bryantchandler@chromium.org
+harringtond@chromium.org
diff --git a/chrome/browser/glic/BUILD.gn b/chrome/browser/glic/BUILD.gn
index 8f7b6bf..5e3de56e 100644
--- a/chrome/browser/glic/BUILD.gn
+++ b/chrome/browser/glic/BUILD.gn
@@ -51,6 +51,7 @@
     "public/glic_invoke_options.h",
     "public/glic_keyed_service.h",
     "public/glic_keyed_service_factory.h",
+    "public/glic_passkeys.h",
     "public/glic_side_panel_coordinator.cc",
     "public/glic_side_panel_coordinator.h",
     "service/glic_instance_helper.cc",
diff --git a/chrome/browser/glic/android/BUILD.gn b/chrome/browser/glic/android/BUILD.gn
index d2dff81..1780389 100644
--- a/chrome/browser/glic/android/BUILD.gn
+++ b/chrome/browser/glic/android/BUILD.gn
@@ -44,6 +44,7 @@
     "//chrome/browser/settings:java",
     "//chrome/browser/tab:java",
     "//chrome/browser/ui/android/toolbar:java",
+    "//chrome/browser/ui/browser_window/public/android:java",
     "//chrome/browser/user_education:java",
     "//components/browser_ui/settings/android:java",
     "//components/browser_ui/styles/android:java",
@@ -76,24 +77,22 @@
 }
 
 robolectric_library("junit") {
-  sources =
-      [ "java/src/org/chromium/chrome/browser/glic/GlicSettingsUnitTest.java" ]
+  sources = [
+    "java/src/org/chromium/chrome/browser/glic/GlicSettingsUnitTest.java",
+    "java/src/org/chromium/chrome/browser/glic/GlicToolbarButtonControllerTest.java",
+  ]
   deps = [
     ":java",
     ":java_resources",
     "//base:base_java",
     "//base:base_junit_test_support",
-    "//build/android:build_java",
-    "//chrome/app:java_strings_grd",
     "//chrome/browser/profiles/android:java",
-    "//chrome/browser/settings:java",
-    "//chrome/browser/ui/android/theme:java",
+    "//chrome/browser/tab:java",
+    "//chrome/browser/ui/android/toolbar:java",
     "//components/browser_ui/settings/android:java",
     "//components/prefs/android:java",
-    "//components/signin/public/android:java",
     "//components/user_prefs/android:java",
     "//content/public/android:content_full_java",
-    "//third_party/androidx:androidx_annotation_annotation_java",
     "//third_party/androidx:androidx_fragment_fragment_java",
     "//third_party/androidx:androidx_lifecycle_lifecycle_common_jvm_java",
     "//third_party/androidx:androidx_preference_preference_java",
diff --git a/chrome/browser/glic/android/java/src/org/chromium/chrome/browser/glic/GlicToolbarButtonController.java b/chrome/browser/glic/android/java/src/org/chromium/chrome/browser/glic/GlicToolbarButtonController.java
index 1ac4eaf..fb561de3 100644
--- a/chrome/browser/glic/android/java/src/org/chromium/chrome/browser/glic/GlicToolbarButtonController.java
+++ b/chrome/browser/glic/android/java/src/org/chromium/chrome/browser/glic/GlicToolbarButtonController.java
@@ -15,31 +15,49 @@
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarButtonVariant;
 import org.chromium.chrome.browser.toolbar.optional_button.BaseButtonDataProvider;
+import org.chromium.chrome.browser.toolbar.optional_button.ButtonData;
 
 import java.util.function.Supplier;
 
 /** Defines a toolbar button to open the Glic bottom sheet. */
 @NullMarked
 public class GlicToolbarButtonController extends BaseButtonDataProvider {
-    // TODO(crbug.com/482372270): Add correct styling to button including Nudge state text, active
-    // state shape change, and appropriate colors.
-    public GlicToolbarButtonController(Context context, Supplier<@Nullable Tab> activeTabSupplier) {
+    private final Runnable mToggleGlicCallback;
+
+    /**
+     * @param context The Android context.
+     * @param activeTabSupplier The currently active tab.
+     * @param toggleGlicCallback Callback to run when the button is clicked to open Glic.
+     */
+    public GlicToolbarButtonController(
+            Context context,
+            Supplier<@Nullable Tab> activeTabSupplier,
+            Runnable toggleGlicCallback) {
+        // TODO(crbug.com/482372270): Add correct styling to button including Nudge state text,
+        // active state shape change, and appropriate colors.
         super(
                 activeTabSupplier,
                 /* modalDialogManager= */ null,
                 AppCompatResources.getDrawable(context, R.drawable.ic_spark_24dp),
-                /* contentDescription= */ "",
+                context.getString(R.string.glic_button_entrypoint_ask_gemini_label),
                 /* actionChipLabelResId= */ Resources.ID_NULL,
                 /* supportsTinting= */ true,
                 /* iphCommandBuilder= */ null,
                 AdaptiveToolbarButtonVariant.GLIC,
                 /* tooltipTextResId= */ Resources.ID_NULL);
+        mToggleGlicCallback = toggleGlicCallback;
+    }
+
+    @Override
+    public ButtonData get(@Nullable Tab tab) {
+        // TODO(haileywang): We should double check the tab profile and whether Glic is enabled.
+        mButtonData.setCanShow(true);
+        mButtonData.setEnabled(true);
+        return super.get(tab);
     }
 
     @Override
     public void onClick(View view) {
-        // TODO(crbug.com/482375066): Hook up to bottom sheet.
-        Tab tab = mActiveTabSupplier.get();
-        if (tab == null) return;
+        mToggleGlicCallback.run();
     }
 }
diff --git a/chrome/browser/glic/android/java/src/org/chromium/chrome/browser/glic/GlicToolbarButtonControllerTest.java b/chrome/browser/glic/android/java/src/org/chromium/chrome/browser/glic/GlicToolbarButtonControllerTest.java
new file mode 100644
index 0000000..a90ab71
--- /dev/null
+++ b/chrome/browser/glic/android/java/src/org/chromium/chrome/browser/glic/GlicToolbarButtonControllerTest.java
@@ -0,0 +1,65 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.glic;
+
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.ContextUtils;
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.browser.tab.Tab;
+import org.chromium.chrome.browser.toolbar.optional_button.ButtonData;
+
+/** Unit tests for {@link GlicToolbarButtonController} */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class GlicToolbarButtonControllerTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock private Tab mTab;
+    @Mock private Runnable mToggleGlicCallback;
+
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = ContextUtils.getApplicationContext();
+    }
+
+    @Test
+    public void testButtonData() {
+        GlicToolbarButtonController controller =
+                new GlicToolbarButtonController(mContext, () -> mTab, mToggleGlicCallback);
+        ButtonData buttonData = controller.get(mTab);
+
+        Assert.assertTrue(buttonData.canShow());
+        Assert.assertTrue(buttonData.isEnabled());
+        Assert.assertNotNull(buttonData.getButtonSpec());
+        Assert.assertEquals(
+                mContext.getString(R.string.glic_button_entrypoint_ask_gemini_label),
+                buttonData.getButtonSpec().getContentDescription());
+    }
+
+    @Test
+    public void testOnClick() {
+        GlicToolbarButtonController controller =
+                new GlicToolbarButtonController(mContext, () -> mTab, mToggleGlicCallback);
+
+        controller.onClick(null);
+
+        verify(mToggleGlicCallback).run();
+    }
+}
diff --git a/chrome/browser/glic/host/host.cc b/chrome/browser/glic/host/host.cc
index 7e74794..36868e0 100644
--- a/chrome/browser/glic/host/host.cc
+++ b/chrome/browser/glic/host/host.cc
@@ -236,6 +236,18 @@
 }
 
 void Host::Invoke(mojom::InvokeOptionsPtr options, base::OnceClosure callback) {
+  CHECK(!options->auto_submit) << "Use InvokeWithAutoSubmit instead.";
+  InvokeInternal(std::move(options), std::move(callback));
+}
+
+void Host::InvokeWithAutoSubmit(InvokeWithAutoSubmitPasskey auto_submit_passkey,
+                                mojom::InvokeOptionsPtr options,
+                                base::OnceClosure callback) {
+  InvokeInternal(std::move(options), std::move(callback));
+}
+
+void Host::InvokeInternal(mojom::InvokeOptionsPtr options,
+                          base::OnceClosure callback) {
   if (auto* client = GetPrimaryWebClient()) {
     client->Invoke(std::move(options), std::move(callback));
   } else {
diff --git a/chrome/browser/glic/host/host.h b/chrome/browser/glic/host/host.h
index 6018c4c2..139fe5b 100644
--- a/chrome/browser/glic/host/host.h
+++ b/chrome/browser/glic/host/host.h
@@ -18,6 +18,7 @@
 #include "chrome/browser/glic/host/glic_web_client_access.h"
 #include "chrome/browser/glic/host/host_metrics.h"
 #include "chrome/browser/glic/public/glic_instance.h"
+#include "chrome/browser/glic/public/glic_passkeys.h"
 #include "chrome/common/actor/task_id.h"
 #include "components/autofill/core/browser/integrators/actor/actor_form_filling_types.h"
 #include "components/tabs/public/tab_interface.h"
@@ -428,6 +429,9 @@
   void NotifySkillToInvokeChanged(mojom::SkillPtr skill);
 
   void Invoke(mojom::InvokeOptionsPtr options, base::OnceClosure callback);
+  void InvokeWithAutoSubmit(InvokeWithAutoSubmitPasskey auto_submit_passkey,
+                            mojom::InvokeOptionsPtr options,
+                            base::OnceClosure callback);
 
   void NotifyContextualSkillsChanged(
       std::vector<mojom::SkillPreviewPtr> contextual_skill_previews);
@@ -435,6 +439,9 @@
  private:
   friend class HostManager;
 
+  void InvokeInternal(mojom::InvokeOptionsPtr options,
+                      base::OnceClosure callback);
+
   void WebUIPageHandlerAdded(GlicPageHandler* page_handler);
   void WebUIPageHandlerRemoved(GlicPageHandler* page_handler);
   GlicKeyedService& glic_service();
diff --git a/chrome/browser/glic/public/OWNERS b/chrome/browser/glic/public/OWNERS
new file mode 100644
index 0000000..1b529d9
--- /dev/null
+++ b/chrome/browser/glic/public/OWNERS
@@ -0,0 +1,2 @@
+per-file glic_passkeys.h=set noparent
+per-file glic_passkeys.h=file://chrome/browser/glic/API_OWNERS
diff --git a/chrome/browser/glic/public/glic_invoke_options.h b/chrome/browser/glic/public/glic_invoke_options.h
index d4f3e21..036ca640 100644
--- a/chrome/browser/glic/public/glic_invoke_options.h
+++ b/chrome/browser/glic/public/glic_invoke_options.h
@@ -75,11 +75,6 @@
   std::variant<DefaultConversation, NewConversation, ConversationId>
       conversation = DefaultConversation();
 
-  // Whether or not to automatically submit the conversation turn.
-  // Note: This is "best effort", not all invocations (e.g., multi-prompts)
-  // support automatic submission.
-  bool auto_submit = false;
-
   // The feature mode to use for the invocation, triggering specific client
   // behaviours like actuation or image generation.
   std::optional<glic::mojom::FeatureMode> feature_mode;
diff --git a/chrome/browser/glic/public/glic_keyed_service.cc b/chrome/browser/glic/public/glic_keyed_service.cc
index b938928e..7578a01 100644
--- a/chrome/browser/glic/public/glic_keyed_service.cc
+++ b/chrome/browser/glic/public/glic_keyed_service.cc
@@ -384,6 +384,21 @@
                              auto_send, conversation_id);
 }
 
+void GlicKeyedService::InvokeWithAutoSubmit(
+    InvokeWithAutoSubmitPasskey auto_submit_passkey,
+    tabs::TabInterface* tab,
+    GlicInvokeOptions options) {
+  CHECK(GlicEnabling::IsEnabledForProfile(profile_));
+
+  GlicProfileManager* glic_profile_manager = GlicProfileManager::GetInstance();
+  if (glic_profile_manager) {
+    glic_profile_manager->SetActiveGlic(this);
+  }
+
+  static_cast<GlicInstanceCoordinatorImpl&>(window_controller())
+      .InvokeWithAutoSubmit(auto_submit_passkey, tab, std::move(options));
+}
+
 void GlicKeyedService::OpenFreDialogInNewTab(BrowserWindowInterface* bwi,
                                              mojom::InvocationSource source) {
 #if !BUILDFLAG(IS_ANDROID)
diff --git a/chrome/browser/glic/public/glic_keyed_service.h b/chrome/browser/glic/public/glic_keyed_service.h
index 2f2112d..6c991d6 100644
--- a/chrome/browser/glic/public/glic_keyed_service.h
+++ b/chrome/browser/glic/public/glic_keyed_service.h
@@ -25,6 +25,8 @@
 #include "chrome/browser/glic/public/context/glic_sharing_manager.h"
 #include "chrome/browser/glic/public/glic_enabling.h"
 #include "chrome/browser/glic/public/glic_instance.h"
+#include "chrome/browser/glic/public/glic_invoke_options.h"
+#include "chrome/browser/glic/public/glic_passkeys.h"
 #include "chrome/common/actor.mojom-forward.h"
 #include "chrome/common/actor/task_id.h"
 #include "chrome/common/actor_webui.mojom-forward.h"
@@ -144,6 +146,12 @@
                 bool prevent_close,
                 mojom::InvocationSource source);
 
+  // Invokes Glic with the given options and automatically submits the prompt.
+  // Access is restricted to authorized callers via InvokeWithAutoSubmitPasskey.
+  void InvokeWithAutoSubmit(InvokeWithAutoSubmitPasskey auto_submit_passkey,
+                            tabs::TabInterface* tab,
+                            GlicInvokeOptions options);
+
   // Show the panel with the given conversation id. Used only by web continuity.
   // Deprecated: See go/gic:invoke for full solution, this existing version will
   // be removed in the future.
diff --git a/chrome/browser/glic/public/glic_passkeys.h b/chrome/browser/glic/public/glic_passkeys.h
new file mode 100644
index 0000000..c91c0e86
--- /dev/null
+++ b/chrome/browser/glic/public/glic_passkeys.h
@@ -0,0 +1,28 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_GLIC_PUBLIC_GLIC_PASSKEYS_H_
+#define CHROME_BROWSER_GLIC_PUBLIC_GLIC_PASSKEYS_H_
+
+#include "base/types/pass_key.h"
+
+namespace glic {
+
+// Passkey for invoking glic with auto submit. Reach out to OWNERS before
+// adding new callers.
+class InvokeWithAutoSubmitPasskey {
+ public:
+  using PassKey = base::PassKey<InvokeWithAutoSubmitPasskey>;
+
+ private:
+  static PassKey GetPassKey() { return PassKey(); }
+
+  // Example of how to add new friends:
+  // friend class SomeClassThatNeedsAutoSubmit;
+  // friend void SomeClass::SomeFunctionThatNeedsAutoSubmit();
+};
+
+}  // namespace glic
+
+#endif  // CHROME_BROWSER_GLIC_PUBLIC_GLIC_PASSKEYS_H_
diff --git a/chrome/browser/glic/selection/selection_overlay.mojom b/chrome/browser/glic/selection/selection_overlay.mojom
index 335a127..01ebaf8 100644
--- a/chrome/browser/glic/selection/selection_overlay.mojom
+++ b/chrome/browser/glic/selection/selection_overlay.mojom
@@ -45,6 +45,16 @@
 
   // The user deletes a selected region. No-op if `id` does not match.
   DeleteRegion(mojo_base.mojom.UnguessableToken id);
+
+  // When called, the C++ coordinator closes the preselection toast bubble.
+  ClosePreselectionBubble();
+
+  // When this method is called, the C++ coordinator will add a blur to the
+  // tab contents.
+  AddBackgroundBlur();
+
+  // Enables/disables the live blurring of the background
+  SetLiveBlur(bool enabled);
 };
 
 // `chrome-untrusted://glic/selection-overlay/` WebUI page handler that receives
@@ -55,4 +65,7 @@
   // used for anything else than the selection overlay.
   ScreenshotReceived(
       skia.mojom.BitmapMappedFromTrustedProcess screenshot);
+
+  // Sets post selection regions to be rendered as selected on the page.
+  SetPostRegionSelections(array<SelectedRegion> regions);
 };
diff --git a/chrome/browser/glic/selection/selection_overlay_controller.cc b/chrome/browser/glic/selection/selection_overlay_controller.cc
index 04efcb8..e2e258a 100644
--- a/chrome/browser/glic/selection/selection_overlay_controller.cc
+++ b/chrome/browser/glic/selection/selection_overlay_controller.cc
@@ -51,6 +51,14 @@
 
 namespace {
 
+gfx::RectF GetRectForRegion(const SkBitmap& image, const gfx::RectF& region) {
+  double x_scale = image.width();
+  double y_scale = image.height();
+  return gfx::RectF((region.x() - 0.5 * region.width()) * x_scale,
+                    (region.y() - 0.5 * region.height()) * y_scale,
+                    region.width() * x_scale, region.height() * y_scale);
+}
+
 class SelectionOverlayFetchPageProgressListener
     : public page_content_annotations::FetchPageProgressListener {
  public:
@@ -279,6 +287,18 @@
   }
 }
 
+void SelectionOverlayController::ClosePreselectionBubble() {
+  ClosePreselectionBubbleImpl();
+}
+
+void SelectionOverlayController::AddBackgroundBlur() {
+  AddBackgroundBlurImpl();
+}
+
+void SelectionOverlayController::SetLiveBlur(bool enabled) {
+  SetLiveBlurImpl(enabled);
+}
+
 void SelectionOverlayController::Reset() {
   receiver_.reset();
   page_.reset();
@@ -295,21 +315,26 @@
   }
 
   std::vector<SkRect> regions;
+  std::vector<selection::SelectedRegionPtr> regions_mojo;
   // TODO(http://b/452032491): Reconsider what happens if the regions overlap.
   // TODO(http://b/452032491): Currently this class is only used once per
   // selection and only one region is supported, so it is fine to always loop
   // through all the regions. Revisit once we expand the selections.
   for (const auto& [id, region] : selected_regions_) {
-    SkRect rect_on_canvas = gfx::RectFToSkRect(region->region);
+    SkRect rect_on_canvas = gfx::RectFToSkRect(
+        GetRectForRegion(redacted_screenshot_, region->region));
     if (!rect_on_canvas.isEmpty() &&
         redacted_screenshot_.bounds().contains(rect_on_canvas)) {
       regions.push_back(rect_on_canvas);
+      regions_mojo.push_back(region.Clone());
     } else {
       // TODO(http://b/485358530): Record proper histograms for the error case.
       LOG(ERROR) << "Invalid region selected " << region->region.ToString();
     }
   }
 
+  page_->SetPostRegionSelections(std::move(regions_mojo));
+
   base::ThreadPool::PostTaskAndReplyWithResult(
       FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
       base::BindOnce(
diff --git a/chrome/browser/glic/selection/selection_overlay_controller.h b/chrome/browser/glic/selection/selection_overlay_controller.h
index ccb5ac2..9bc6a7f 100644
--- a/chrome/browser/glic/selection/selection_overlay_controller.h
+++ b/chrome/browser/glic/selection/selection_overlay_controller.h
@@ -80,6 +80,9 @@
   void DismissOverlay(selection::DismissOverlayReason reason) override;
   void AdjustRegion(selection::SelectedRegionPtr target) override;
   void DeleteRegion(const base::UnguessableToken& id) override;
+  void ClosePreselectionBubble() override;
+  void AddBackgroundBlur() override;
+  void SetLiveBlur(bool enabled) override;
 
  private:
   void OnScreenshotTaken(const SkBitmap& bitmap);
diff --git a/chrome/browser/glic/service/glic_instance_coordinator_impl.cc b/chrome/browser/glic/service/glic_instance_coordinator_impl.cc
index 679ff16..29d390c 100644
--- a/chrome/browser/glic/service/glic_instance_coordinator_impl.cc
+++ b/chrome/browser/glic/service/glic_instance_coordinator_impl.cc
@@ -290,6 +290,20 @@
 
 void GlicInstanceCoordinatorImpl::Invoke(tabs::TabInterface* tab,
                                          GlicInvokeOptions options) {
+  InvokeInternal(std::nullopt, tab, std::move(options));
+}
+
+void GlicInstanceCoordinatorImpl::InvokeWithAutoSubmit(
+    InvokeWithAutoSubmitPasskey auto_submit_passkey,
+    tabs::TabInterface* tab,
+    GlicInvokeOptions options) {
+  InvokeInternal(auto_submit_passkey, tab, std::move(options));
+}
+
+void GlicInstanceCoordinatorImpl::InvokeInternal(
+    std::optional<InvokeWithAutoSubmitPasskey> auto_submit_passkey,
+    tabs::TabInterface* tab,
+    GlicInvokeOptions options) {
   if (!tab || !GlicInstanceHelper::From(tab)) {
     if (options.on_error) {
       std::move(options.on_error).Run(GlicInvokeError::kInvalidTab);
@@ -336,7 +350,7 @@
       *tab, GlicPinTrigger::kInstanceCreation, options.invocation_source));
 
   invoke_handlers_[instance] = std::make_unique<GlicInvokeHandler>(
-      *instance, tab, std::move(options),
+      *instance, tab, std::move(options), auto_submit_passkey,
       base::BindOnce(&GlicInstanceCoordinatorImpl::OnInvokeHandlerComplete,
                      base::Unretained(this)));
   invoke_handlers_[instance]->Invoke();
diff --git a/chrome/browser/glic/service/glic_instance_coordinator_impl.h b/chrome/browser/glic/service/glic_instance_coordinator_impl.h
index 066101a..49d42fe 100644
--- a/chrome/browser/glic/service/glic_instance_coordinator_impl.h
+++ b/chrome/browser/glic/service/glic_instance_coordinator_impl.h
@@ -25,6 +25,7 @@
 #include "chrome/browser/glic/public/context/glic_sharing_manager.h"
 #include "chrome/browser/glic/public/glic_enabling.h"
 #include "chrome/browser/glic/public/glic_invoke_options.h"
+#include "chrome/browser/glic/public/glic_passkeys.h"
 #include "chrome/browser/glic/service/glic_instance_impl.h"
 #include "chrome/browser/glic/service/glic_invoke_handler.h"
 #include "chrome/browser/glic/service/metrics/glic_instance_coordinator_metrics.h"
@@ -137,6 +138,9 @@
   void Shutdown() override;
   void Close(const CloseOptions& options) override;
   void Invoke(tabs::TabInterface* tab, GlicInvokeOptions options);
+  void InvokeWithAutoSubmit(InvokeWithAutoSubmitPasskey auto_submit_passkey,
+                            tabs::TabInterface* tab,
+                            GlicInvokeOptions options);
   void CloseInstanceWithFrame(
       content::RenderFrameHost* render_frame_host) override;
   void CloseAndShutdownInstanceWithFrame(
@@ -189,6 +193,11 @@
   GlicInstanceImpl* GetInstanceImplForTab(const tabs::TabInterface* tab) const;
 
  private:
+  void InvokeInternal(
+      std::optional<InvokeWithAutoSubmitPasskey> auto_submit_passkey,
+      tabs::TabInterface* tab,
+      GlicInvokeOptions options);
+
   void OnTabEvent(const GlicTabEvent& event);
   // Returns a pointer to an instance with the given conversation id or nullptr
   // if no such instance exists.
diff --git a/chrome/browser/glic/service/glic_invoke_handler.cc b/chrome/browser/glic/service/glic_invoke_handler.cc
index 34e5cfa3..a6acc1b 100644
--- a/chrome/browser/glic/service/glic_invoke_handler.cc
+++ b/chrome/browser/glic/service/glic_invoke_handler.cc
@@ -18,12 +18,15 @@
 
 constexpr base::TimeDelta kDefaultTimeout = base::Minutes(1);
 
-GlicInvokeHandler::GlicInvokeHandler(GlicInstanceImpl& instance,
-                                     tabs::TabInterface* tab,
-                                     GlicInvokeOptions options,
-                                     CompletionCallback completion_callback)
+GlicInvokeHandler::GlicInvokeHandler(
+    GlicInstanceImpl& instance,
+    tabs::TabInterface* tab,
+    GlicInvokeOptions options,
+    std::optional<InvokeWithAutoSubmitPasskey> auto_submit_passkey,
+    CompletionCallback completion_callback)
     : instance_(instance),
       options_(std::move(options)),
+      auto_submit_passkey_(auto_submit_passkey),
       completion_callback_(std::move(completion_callback)) {
   if (tab && GlicInstanceHelper::From(tab)) {
     tab_destruction_subscription_ =
@@ -67,9 +70,16 @@
     return;
   }
 
-  instance_->host().Invoke(CreateMojoOptions(),
-                           base::BindOnce(&GlicInvokeHandler::OnSuccess,
-                                          weak_ptr_factory_.GetWeakPtr()));
+  if (auto_submit_passkey_) {
+    instance_->host().InvokeWithAutoSubmit(
+        *auto_submit_passkey_, CreateMojoOptions(),
+        base::BindOnce(&GlicInvokeHandler::OnSuccess,
+                       weak_ptr_factory_.GetWeakPtr()));
+  } else {
+    instance_->host().Invoke(CreateMojoOptions(),
+                             base::BindOnce(&GlicInvokeHandler::OnSuccess,
+                                            weak_ptr_factory_.GetWeakPtr()));
+  }
 }
 
 void GlicInvokeHandler::OnTabClosed(tabs::TabInterface* tab) {
@@ -110,7 +120,7 @@
     mojo_options->context = std::move(options_.additional_context);
   }
 
-  mojo_options->auto_submit = options_.auto_submit;
+  mojo_options->auto_submit = auto_submit_passkey_.has_value();
   mojo_options->feature_mode =
       options_.feature_mode.value_or(mojom::FeatureMode::kUnspecified);
   mojo_options->disable_zero_state_suggestions = options_.disable_zss;
diff --git a/chrome/browser/glic/service/glic_invoke_handler.h b/chrome/browser/glic/service/glic_invoke_handler.h
index 488be37..1024b9c 100644
--- a/chrome/browser/glic/service/glic_invoke_handler.h
+++ b/chrome/browser/glic/service/glic_invoke_handler.h
@@ -14,6 +14,7 @@
 #include "chrome/browser/glic/host/host.h"
 #include "chrome/browser/glic/public/glic_instance.h"
 #include "chrome/browser/glic/public/glic_invoke_options.h"
+#include "chrome/browser/glic/public/glic_passkeys.h"
 
 namespace tabs {
 class TabInterface;
@@ -30,10 +31,12 @@
   using CompletionCallback =
       base::OnceCallback<void(GlicInstance*, GlicInvokeHandler*)>;
 
-  GlicInvokeHandler(GlicInstanceImpl& instance,
-                    tabs::TabInterface* tab,
-                    GlicInvokeOptions options,
-                    CompletionCallback completion_callback);
+  GlicInvokeHandler(
+      GlicInstanceImpl& instance,
+      tabs::TabInterface* tab,
+      GlicInvokeOptions options,
+      std::optional<InvokeWithAutoSubmitPasskey> auto_submit_passkey,
+      CompletionCallback completion_callback);
   ~GlicInvokeHandler() override;
 
   GlicInvokeHandler(const GlicInvokeHandler&) = delete;
@@ -58,6 +61,7 @@
 
   const base::raw_ref<GlicInstanceImpl> instance_;
   GlicInvokeOptions options_;
+  std::optional<InvokeWithAutoSubmitPasskey> auto_submit_passkey_;
   CompletionCallback completion_callback_;
 
   base::CallbackListSubscription tab_destruction_subscription_;
diff --git a/chrome/browser/headless/headless_mode_protocol_browsertest.cc b/chrome/browser/headless/headless_mode_protocol_browsertest.cc
index db363b7..633202e7 100644
--- a/chrome/browser/headless/headless_mode_protocol_browsertest.cc
+++ b/chrome/browser/headless/headless_mode_protocol_browsertest.cc
@@ -483,6 +483,14 @@
 
 HEADLESS_MODE_PROTOCOL_TEST(AddRemoveScreen, "shared/add-remove-screen.js")
 
+// Emulation.SetPrimaryScreen is not yet supported on macOS and Windows.
+#if !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_WIN)
+HEADLESS_MODE_PROTOCOL_TEST(SetPrimaryScreen, "shared/set-primary-screen.js")
+
+HEADLESS_MODE_PROTOCOL_TEST(SetPrimaryScreenScaled,
+                            "shared/set-primary-screen-scaled.js")
+#endif
+
 HEADLESS_MODE_PROTOCOL_TEST(RangeMouseEventAfterNodeRemoval,
                             "shared/range-mouse-event-after-node-removal.js")
 
diff --git a/chrome/browser/interstitials/BUILD.gn b/chrome/browser/interstitials/BUILD.gn
new file mode 100644
index 0000000..cf6c622
--- /dev/null
+++ b/chrome/browser/interstitials/BUILD.gn
@@ -0,0 +1,77 @@
+# Copyright 2026 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//components/safe_browsing/buildflags.gni")
+import("//extensions/buildflags/buildflags.gni")
+
+source_set("interstitials") {
+  sources = [
+    "chrome_settings_page_helper.h",
+    "enterprise_util.h",
+  ]
+
+  public_deps = [
+    "//chrome/browser/safe_browsing",
+    "//components/safe_browsing/core/browser/db:v4_protocol_manager_util",
+    "//components/safe_browsing/core/common",
+    "//components/safe_browsing/core/common/proto:realtimeapi_proto",
+    "//components/security_interstitials/content:security_interstitial_page",
+  ]
+
+  # safe_browsing includes interstitials headers but can't have interstitials
+  # as a dep (cycle: interstitials → safe_browsing → interstitials).
+  allow_circular_includes_from =
+      [ "//chrome/browser/safe_browsing:safe_browsing" ]
+}
+
+source_set("impl") {
+  sources = [
+    "chrome_settings_page_helper.cc",
+    "enterprise_util.cc",
+  ]
+
+  public_deps = [
+    ":interstitials",
+    "//chrome/browser:browser_public_dependencies",
+  ]
+
+  deps = [
+    "//base",
+    "//chrome/browser/profiles:profile",
+    "//components/enterprise/connectors/core",
+    "//components/prefs",
+    "//components/safe_browsing/content/browser",
+    "//components/safe_browsing/core/common:features",
+    "//components/sessions",
+    "//content/public/browser",
+  ]
+
+  if (!is_android) {
+    deps += [ "//chrome/browser/ui" ]
+  }
+
+  if (enable_extensions) {
+    deps += [ "//chrome/browser/extensions" ]
+  }
+}
+
+source_set("test_support") {
+  testonly = true
+
+  sources = [
+    "security_interstitial_page_test_utils.cc",
+    "security_interstitial_page_test_utils.h",
+  ]
+
+  public_deps = [
+    "//components/security_interstitials/content:security_interstitial_page",
+    "//content/public/browser",
+  ]
+
+  deps = [
+    "//base",
+    "//components/security_interstitials/core",
+    "//content/test:test_support",
+  ]
+}
diff --git a/chrome/browser/new_tab_page/modules/v2/tab_groups/tab_groups_page_handler_unittest.cc b/chrome/browser/new_tab_page/modules/v2/tab_groups/tab_groups_page_handler_unittest.cc
index 26e1380..287189ca 100644
--- a/chrome/browser/new_tab_page/modules/v2/tab_groups/tab_groups_page_handler_unittest.cc
+++ b/chrome/browser/new_tab_page/modules/v2/tab_groups/tab_groups_page_handler_unittest.cc
@@ -60,7 +60,13 @@
               UpdateGroupPosition,
               (const base::Uuid& sync_id,
                std::optional<bool> is_pinned,
-               std ::optional<int> new_index));
+               std::optional<int> new_index));
+  MOCK_METHOD(void,
+              ReorderGroupBefore,
+              (const base::Uuid& sync_id, const base::Uuid& next_sync_id));
+  MOCK_METHOD(void,
+              ReorderGroupAfter,
+              (const base::Uuid& sync_id, const base::Uuid& prev_sync_id));
   MOCK_METHOD(void,
               UpdateBookmarkNodeId,
               (const base::Uuid&, std::optional<base::Uuid>));
diff --git a/chrome/browser/notifications/android/java/src/org/chromium/chrome/browser/notifications/NotificationUmaTracker.java b/chrome/browser/notifications/android/java/src/org/chromium/chrome/browser/notifications/NotificationUmaTracker.java
index b286766..412337e 100644
--- a/chrome/browser/notifications/android/java/src/org/chromium/chrome/browser/notifications/NotificationUmaTracker.java
+++ b/chrome/browser/notifications/android/java/src/org/chromium/chrome/browser/notifications/NotificationUmaTracker.java
@@ -87,6 +87,7 @@
         SystemNotificationType.TRACING,
         SystemNotificationType.SERIAL,
         SystemNotificationType.SAFETY_HUB_UNSUBSCRIBED_NOTIFICATIONS,
+        SystemNotificationType.ACTOR,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SystemNotificationType {
@@ -135,8 +136,9 @@
         int TRACING = 41;
         int SERIAL = 42;
         int SAFETY_HUB_UNSUBSCRIBED_NOTIFICATIONS = 43;
+        int ACTOR = 44;
 
-        int NUM_ENTRIES = 44;
+        int NUM_ENTRIES = 45;
     }
 
     /*
@@ -176,7 +178,10 @@
         ActionType.REPORT_AS_SAFE,
         ActionType.REPORT_WARNED_NOTIFICATION_AS_SPAM,
         ActionType.REPORT_UNWARNED_NOTIFICATION_AS_SPAM,
-        ActionType.DOWNLOAD_DELETE_FROM_HISTORY
+        ActionType.DOWNLOAD_DELETE_FROM_HISTORY,
+        ActionType.ACTOR_PAUSE,
+        ActionType.ACTOR_RESUME,
+        ActionType.ACTOR_CANCEL,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ActionType {
@@ -279,8 +284,15 @@
         // Delete from history button on user download notification.
         int DOWNLOAD_DELETE_FROM_HISTORY = 41;
 
+        // Pause button on actor notification.
+        int ACTOR_PAUSE = 42;
+        // Resume button on actor notification.
+        int ACTOR_RESUME = 43;
+        // Cancel button on actor notification.
+        int ACTOR_CANCEL = 44;
+
         // Number of real entries, excluding `UNKNOWN`.
-        int NUM_ENTRIES = 41;
+        int NUM_ENTRIES = 45;
     }
 
     /**
@@ -456,6 +468,10 @@
                         "Mobile.SystemNotification.Content.Click.Age.PriceDropUserManaged",
                         createTime);
                 break;
+            case SystemNotificationType.ACTOR:
+                recordNotificationAgeHistogram(
+                        "Mobile.SystemNotification.Content.Click.Age.Actor", createTime);
+                break;
         }
     }
 
@@ -499,6 +515,10 @@
                 recordNotificationAgeHistogram(
                         "Mobile.SystemNotification.Dismiss.Age.PriceDropUserManaged", createTime);
                 break;
+            case SystemNotificationType.ACTOR:
+                recordNotificationAgeHistogram(
+                        "Mobile.SystemNotification.Dismiss.Age.Actor", createTime);
+                break;
         }
     }
 
@@ -548,6 +568,10 @@
                         "Mobile.SystemNotification.Action.Click.Age.PriceDropUserManaged",
                         createTime);
                 break;
+            case SystemNotificationType.ACTOR:
+                recordNotificationAgeHistogram(
+                        "Mobile.SystemNotification.Action.Click.Age.Actor", createTime);
+                break;
         }
     }
 
@@ -822,6 +846,8 @@
         switch (channelId) {
             case ChannelId.BROWSER:
                 return "Browser";
+            case ChannelId.ACTOR:
+                return "Actor";
             case ChannelId.COLLABORATION:
                 return "Collaboration";
             case ChannelId.DOWNLOADS:
diff --git a/chrome/browser/notifications/android/java/src/org/chromium/chrome/browser/notifications/channels/ChromeChannelDefinitions.java b/chrome/browser/notifications/android/java/src/org/chromium/chrome/browser/notifications/channels/ChromeChannelDefinitions.java
index f76e0f2d..a1d4a79 100644
--- a/chrome/browser/notifications/android/java/src/org/chromium/chrome/browser/notifications/channels/ChromeChannelDefinitions.java
+++ b/chrome/browser/notifications/android/java/src/org/chromium/chrome/browser/notifications/channels/ChromeChannelDefinitions.java
@@ -43,7 +43,7 @@
      * set of channels returned by {@link #getStartupChannelIds()} or {@link #getLegacyChannelIds()}
      * changes.
      */
-    static final int CHANNELS_VERSION = 6;
+    static final int CHANNELS_VERSION = 7;
 
     private static class LazyHolder {
         private static final ChromeChannelDefinitions sInstance = new ChromeChannelDefinitions();
@@ -71,6 +71,7 @@
     // LINT.IfChange(ChannelId)
     @StringDef({
         ChannelId.BROWSER,
+        ChannelId.ACTOR,
         ChannelId.COLLABORATION,
         ChannelId.DOWNLOADS,
         ChannelId.INCOGNITO,
@@ -100,6 +101,7 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface ChannelId {
         String BROWSER = "browser";
+        String ACTOR = "actor";
         String COLLABORATION = "collaboration";
         String DOWNLOADS = "downloads";
         String INCOGNITO = "incognito";
@@ -133,8 +135,12 @@
         String TIPS = "tips";
     }
 
-    // LINT.ThenChange(//tools/metrics/histograms/metadata/mobile/histograms.xml:NotificationChannelId)
-    // LINT.ThenChange(//chrome/browser/notifications/android/java/src/org/chromium/chrome/browser/notifications/NotificationUmaTracker.java:NotificationChannelId)
+    // clang-format off
+    // LINT.ThenChange(
+    //   //tools/metrics/histograms/metadata/mobile/histograms.xml:NotificationChannelId,
+    //   //chrome/browser/notifications/android/java/src/org/chromium/chrome/browser/notifications/NotificationUmaTracker.java:NotificationChannelId
+    // )
+    // clang-format on
 
     @StringDef({ChannelGroupId.GENERAL, ChannelGroupId.SITES})
     @Retention(RetentionPolicy.SOURCE)
@@ -174,6 +180,14 @@
             startup.add(ChannelId.BROWSER);
 
             map.put(
+                    ChannelId.ACTOR,
+                    PredefinedChannel.create(
+                            ChannelId.ACTOR,
+                            R.string.notification_category_actor,
+                            NotificationManager.IMPORTANCE_HIGH,
+                            ChannelGroupId.GENERAL));
+
+            map.put(
                     ChannelId.COLLABORATION,
                     PredefinedChannel.create(
                             ChannelId.COLLABORATION,
diff --git a/chrome/browser/obsolete_system/BUILD.gn b/chrome/browser/obsolete_system/BUILD.gn
new file mode 100644
index 0000000..1e3ebcf4
--- /dev/null
+++ b/chrome/browser/obsolete_system/BUILD.gn
@@ -0,0 +1,44 @@
+# Copyright 2026 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+source_set("obsolete_system") {
+  sources = [ "obsolete_system.h" ]
+}
+
+source_set("impl") {
+  sources = []
+  deps = [ ":obsolete_system" ]
+
+  if (!is_android) {
+    if (is_win) {
+      sources += [ "obsolete_system_win.cc" ]
+      deps += [
+        "//base",
+        "//chrome/app:branded_strings",
+        "//chrome/common",
+        "//chrome/common:version_header",
+        "//ui/base",
+      ]
+    } else if (is_mac) {
+      sources += [ "obsolete_system_mac.cc" ]
+      deps += [
+        "//base",
+        "//chrome/app:branded_strings",
+        "//chrome/common",
+        "//chrome/common:version_header",
+        "//ui/base",
+      ]
+    } else if (is_linux) {
+      sources += [ "obsolete_system_linux.cc" ]
+      deps += [
+        "//base",
+        "//chrome/app:branded_strings",
+        "//ui/base",
+      ]
+    } else {
+      # ChromeOS and other non-Android platforms use a stub implementation.
+      sources += [ "obsolete_system_stub.cc" ]
+    }
+  }
+}
diff --git a/chrome/browser/pdf/pdf_viewer_stream_manager.cc b/chrome/browser/pdf/pdf_viewer_stream_manager.cc
index ef498a6..15a01b4 100644
--- a/chrome/browser/pdf/pdf_viewer_stream_manager.cc
+++ b/chrome/browser/pdf/pdf_viewer_stream_manager.cc
@@ -519,37 +519,36 @@
     return;
   }
 
-  // If the extension host has already started its navigation to the PDF
-  // extension URL, set the extension as finished navigating, ignoring other
-  // children of the embedder host.
+  const GURL& url = navigation_handle->GetURL();
   if (stream_info->DidPdfExtensionStartNavigation()) {
-    if (stream_info->extension_host_frame_tree_node_id() ==
-        navigation_handle->GetFrameTreeNodeId()) {
-      const GURL& url = navigation_handle->GetURL();
-
+    // If the extension host has already started its navigation to the PDF
+    // extension URL, set the extension as finished navigating. Ignore
+    // navigations in other children of the embedder host. Ignore all other
+    // URLs, which was shown to be possible in crbug.com/432497344.
+    const GURL pdf_extension_url = stream_info->stream()->handler_url();
+    if (url == pdf_extension_url &&
+        stream_info->extension_host_frame_tree_node_id() ==
+            navigation_handle->GetFrameTreeNodeId() &&
+        navigation_handle->HasCommitted() &&
+        !navigation_handle->IsErrorPage()) {
       // TODO(crbug.com/432497344, crbug.com/479589477): Remove debugging data.
       crash_reporter::ScopedCrashKeyString
           scoped_crash_key_did_finish_navigation_url(
               &crash_key_did_finish_navigation_url, url.spec());
       stream_info->SetDidExtensionFinishNavigation();
-      if (navigation_handle->HasCommitted() &&
-          !navigation_handle->IsErrorPage()) {
-        // Setup zoom level for the PDF extension. Zoom level 0 corresponds
-        // to zoom factor of 1, or 100%. This is done so the PDF viewer UI
-        // does not change if the page zoom does. This is analogous to page
-        // zoom not affecting the browser UI.
-        const GURL pdf_extension_url = stream_info->stream()->handler_url();
-        CHECK_EQ(extensions::kExtensionScheme, url.GetScheme());
-        CHECK_EQ(pdf_extension_url, url);
-        content::HostZoomMap::Get(
-            navigation_handle->GetRenderFrameHost()->GetSiteInstance())
-            ->SetZoomLevelForHostAndScheme(pdf_extension_url.GetScheme(),
-                                           pdf_extension_url.GetHost(), 0);
-        // Set ZoomController on the extension host.
-        zoom::ZoomController::CreateForWebContentsAndRenderFrameHost(
-            web_contents(),
-            navigation_handle->GetRenderFrameHost()->GetGlobalId());
-      }
+
+      // Setup zoom level for the PDF extension. Zoom level 0 corresponds
+      // to zoom factor of 1, or 100%. This is done so the PDF viewer UI
+      // does not change if the page zoom does. This is analogous to page
+      // zoom not affecting the browser UI.
+      content::HostZoomMap::Get(
+          navigation_handle->GetRenderFrameHost()->GetSiteInstance())
+          ->SetZoomLevelForHostAndScheme(pdf_extension_url.GetScheme(),
+                                         pdf_extension_url.GetHost(), 0);
+      // Set ZoomController on the extension host.
+      zoom::ZoomController::CreateForWebContentsAndRenderFrameHost(
+          web_contents(),
+          navigation_handle->GetRenderFrameHost()->GetGlobalId());
     }
     return;
   }
@@ -558,7 +557,7 @@
   // inserted in a synthetic HTML document as a placeholder for the PDF
   // extension. Navigate the about:blank embed to the PDF extension URL to load
   // the PDF extension.
-  if (!navigation_handle->GetURL().IsAboutBlank()) {
+  if (!url.IsAboutBlank()) {
     return;
   }
 
diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc
index 40afd77..6ac3d06 100644
--- a/chrome/browser/prefs/browser_prefs.cc
+++ b/chrome/browser/prefs/browser_prefs.cc
@@ -995,6 +995,9 @@
 // Deprecated 02/2026.
 inline constexpr char kTabSearchOpened[] = "tab_search.opened";
 
+// Deprecated 02/2026.
+constexpr char kTabOrganizationFeature[] = "tab_organization.feature";
+
 // Register local state used only for migration (clearing or moving to a new
 // key).
 void RegisterLocalStatePrefsForMigration(PrefRegistrySimple* registry) {
@@ -1387,6 +1390,9 @@
 
   // Deprecated 02/2026.
   registry->RegisterBooleanPref(kTabSearchOpened, false);
+
+  // Deprecated 02/2026.
+  registry->RegisterIntegerPref(kTabOrganizationFeature, 0);
 }
 
 }  // namespace
diff --git a/chrome/browser/preloading/preview/preview_tab.cc b/chrome/browser/preloading/preview/preview_tab.cc
index 7aa0177..0d48ab8 100644
--- a/chrome/browser/preloading/preview/preview_tab.cc
+++ b/chrome/browser/preloading/preview/preview_tab.cc
@@ -45,7 +45,7 @@
   // TODO(b:292184832): Create with own buttons
 
   views::Widget::InitParams params(
-      views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
+      views::Widget::InitParams::CLIENT_OWNS_WIDGET,
       views::Widget::InitParams::TYPE_WINDOW);
   params.shadow_type = views::Widget::InitParams::ShadowType::kDrop;
   const gfx::Rect& rect = parent.GetViewBounds();
diff --git a/chrome/browser/profiles/BUILD.gn b/chrome/browser/profiles/BUILD.gn
index da62d46..c2ee0ee 100644
--- a/chrome/browser/profiles/BUILD.gn
+++ b/chrome/browser/profiles/BUILD.gn
@@ -305,6 +305,7 @@
     "//chrome/browser/contextual_tasks:ui",
     "//chrome/browser/custom_handlers",
     "//chrome/browser/data_sharing",
+    "//chrome/browser/digital_credentials",
     "//chrome/browser/dom_distiller",
     "//chrome/browser/domain_reliability",
     "//chrome/browser/engagement",
diff --git a/chrome/browser/resources/contextual_tasks/app.css b/chrome/browser/resources/contextual_tasks/app.css
index 689c1af..3fe9f91 100644
--- a/chrome/browser/resources/contextual_tasks/app.css
+++ b/chrome/browser/resources/contextual_tasks/app.css
@@ -302,23 +302,23 @@
   width: var(--composebox-width);
 }
 
-:host([is-zero-state_][is-shown-in-tab_]) {
-  --composebox-max-width: 750px;
-}
-
 :host([is-zero-state_]) #composebox {
   animation: zero-state-intro 600ms ease
       forwards;
   opacity: 0;
 }
 
-:host([is-zero-state_][is-shown-in-tab_]) #composebox {
+:host([is-zero-state_][is-shown-in-tab_]) {
   /* Since is flexbox centered in this mode,
    * does not need start indent. Just make composebox width
    * shorter by desired amount of indent on each side.
    */
   --composebox-start-indent: 0px;
   --composebox-width: calc(100% - 40px);
+  --composebox-max-width: 750px;
+}
+
+:host([is-zero-state_][is-shown-in-tab_]) #composebox {
   bottom: 0;
   pointer-events: auto;
   position: relative;
@@ -336,6 +336,7 @@
   grid-column: 1;
   grid-row: 2;
   position: relative;
+  pointer-events: none;
 }
 
 :host([is-zero-state_]) #flexCenterContainer {
@@ -403,7 +404,7 @@
 }
 
 @media (min-width: 789px) {
-  :host([is-zero-state_]:not([is-shown-in-tab_])) #flexCenterContainer {
+  :host([is-zero-state_]:not([is-shown-in-tab_])[is-ai-page_]) #flexCenterContainer {
     /* 96px gutter + 5px for text indentation in thread. */
     --wrapper-indent_: 101px;
     inset-inline-start: var(--wrapper-indent_);
@@ -417,7 +418,7 @@
  * composebox.css min-width: 1420px for full explanation.
  */
 @media (min-width: 1420px) {
-  :host(:not([is-shown-in-tab_])[is-zero-state_]) #flexCenterContainer {
+  :host(:not([is-shown-in-tab_])[is-zero-state_][is-ai-page_]) #flexCenterContainer {
     --wrapper-indent_: max(285px, calc(50% - 570px));
     inset-inline-start: var(--wrapper-indent_);
     width: calc(100% - var(--wrapper-indent_));
@@ -429,7 +430,7 @@
  */
 
 @media (min-width: 789px) {
-  :host(:not([is-zero-state_]):not([is-shown-in-tab_])) #composebox {
+  :host(:not([is-zero-state_]):not([is-shown-in-tab_])[is-ai-page_]) {
     /* 96px gutter + 5px for text indentation in thread. */
     --composebox-start-indent: 101px;
     /* Refresh the width variable since we define --composebox-start-indent in
@@ -441,7 +442,7 @@
 }
 
 @media (min-width: 480px) {
-  :host(:not([is-zero-state_])[is-shown-in-tab_]) #composebox {
+  :host(:not([is-zero-state_])[is-shown-in-tab_][is-ai-page_]) {
     /* Google.com uses 96px for indent column, and 5px left padding.
      * Aligns the input text of the composebox with the AIM thread.
      */
@@ -453,8 +454,8 @@
 /* W/more space, indent further to match AIM for both side panel and full tab.*/
 @media (min-width: 1420px) {
   /* Need both selectors to have higher specificity. */
-  :host(:not([is-zero-state_])[is-shown-in-tab_]) #composebox,
-  :host(:not([is-zero-state_]):not([is-shown-in-tab_])) #composebox {
+  :host(:not([is-zero-state_])[is-shown-in-tab_][is-ai-page_]),
+  :host(:not([is-zero-state_]):not([is-shown-in-tab_])[is-ai-page_]) {
     /* At this width, indent column should be 280px + 5px left padding.
      * Google.com AIM has 20 other 36px columns as "inset end space",
      * with 20px gaps, which = 720px of cols, 21 * 20px = 420px of gaps.
@@ -471,6 +472,14 @@
   }
 }
 
+/* Lens results specific styles. They are always left aligned and the same
+ * width as the results column. */
+:host(:not([is-ai-page_])) {
+  --composebox-max-width: 652px;
+  --composebox-width: calc(100% - var(--composebox-start-indent) * 2);
+}
+
+
 @media (max-height: 800px) {
   :host([is-zero-state_]:not([enable-native-zero-state-suggestions_]))
       #flexCenterContainer {
@@ -601,7 +610,10 @@
   grid-row: 2;
   height: 100%;
   overflow: hidden;
-  width: 100%;
+  width: var(--composebox-width);
+  max-width: var(--composebox-max-width);
+  inset-inline-start: var(--composebox-start-indent);
+  position: relative;
 }
 
 @keyframes zero-state-intro {
diff --git a/chrome/browser/resources/contextual_tasks/app.ts b/chrome/browser/resources/contextual_tasks/app.ts
index aba5e3d..f61fc69 100644
--- a/chrome/browser/resources/contextual_tasks/app.ts
+++ b/chrome/browser/resources/contextual_tasks/app.ts
@@ -24,7 +24,7 @@
 import {getHtml} from './app.html.js';
 // <if expr="not is_android">
 import type {ContextualTasksComposeboxElement} from './composebox.js';
-import type {ComposeboxPosition} from './contextual_tasks.mojom-webui.js';
+import type {ComposeboxPosition, IconType} from './contextual_tasks.mojom-webui.js';
 // </if>
 import type {BrowserProxy} from './contextual_tasks_browser_proxy.js';
 import {BrowserProxyImpl} from './contextual_tasks_browser_proxy.js';
@@ -32,6 +32,7 @@
 // <if expr="not is_android">
 import type {Rect} from './post_message_handler.js';
 import {getNonOccludedClipPath} from './utils/clip_path.js';
+
 // </if>
 
 declare global {
@@ -348,6 +349,10 @@
                 title, 'chrome://image?url=' + encodeURIComponent(thumbnail),
                 fileToken);
           }),
+      callbackRouter.injectInputWithIcon.addListener(
+          (title: string, iconId: IconType, fileToken: UnguessableToken) => {
+            this.$.composebox.injectInputWithIcon(title, iconId, fileToken);
+          }),
       callbackRouter.removeInjectedInput.addListener(
           (fileToken: UnguessableToken) => {
             this.$.composebox.deleteFile(fileToken);
@@ -370,7 +375,6 @@
           this.forcedComposeboxBounds_ = null;
           // </if>
         }
-
       }),
       callbackRouter.onLensOverlayStateChanged.addListener(
           (isOverlayShowing: boolean, maybeShowOverlayHintText: boolean) => {
@@ -611,6 +615,9 @@
 
     if (!isAiPage) {
       // If this is not an AI page, show the ghost loader.
+      // Update the isAiPage_ property so the ghost loader doesn't jump when
+      // the property is updated later.
+      this.isAiPage_ = isAiPage;
       this.setIsGhostLoaderVisible(true);
     } else if (this.enableBasicMode_ && wasAiPage) {
       // Since this is a navigation from one AI page to another,
diff --git a/chrome/browser/resources/contextual_tasks/composebox.html.ts b/chrome/browser/resources/contextual_tasks/composebox.html.ts
index 30f21ff4..934e2e27 100644
--- a/chrome/browser/resources/contextual_tasks/composebox.html.ts
+++ b/chrome/browser/resources/contextual_tasks/composebox.html.ts
@@ -62,6 +62,7 @@
           .lensButtonTriggersOverlay="${true}"
           .enableCarouselScrolling="${true}"
           .isFollowupQuery="${!this.isZeroState}"
+          .enableFileHint="${true}"
           @result-changed="${this.onSuggestionsResultChanged_}"
           @open-image-upload="${this.onOpenImageUpload_}"
           @open-file-upload="${this.onOpenFileUpload_}"
diff --git a/chrome/browser/resources/contextual_tasks/composebox.ts b/chrome/browser/resources/contextual_tasks/composebox.ts
index d4594f5..9bf99ba 100644
--- a/chrome/browser/resources/contextual_tasks/composebox.ts
+++ b/chrome/browser/resources/contextual_tasks/composebox.ts
@@ -25,8 +25,18 @@
 import {getCss} from './composebox.css.js';
 import {getHtml} from './composebox.html.js';
 import {VoiceSearchState} from './constants.js';
+import {IconType} from './contextual_tasks.mojom-webui.js';
 import type {ContextualTasksOnboardingTooltipElement} from './onboarding_tooltip.js';
 
+const ICON_TYPE_TO_NAME: {[id: number]: string} = {
+  [IconType.kUnspecified]: 'unspecified',
+  [IconType.kAdd]: 'add',
+  [IconType.kFormatQuoteFilled]: 'quoteFilled',
+  [IconType.kImage]: 'image',
+  [IconType.kDrivePdf]: 'drivePdf',
+  [IconType.kCheck]: 'check',
+};
+
 function recordVoiceSearchAction(voiceSearchState: VoiceSearchState) {
   // Safety return statement in rare case chrome metrics is not available.
   if (!chrome.metricsPrivate) {
@@ -294,7 +304,7 @@
   }
 
   protected getInputPlaceholder_() {
-    return this.maybeShowOverlayHintText ?
+    return this.maybeShowOverlayHintText && !this.$.composebox.hasFiles() ?
         loadTimeData.getString('composeboxHintTextLensOverlay') :
         '';
   }
@@ -427,6 +437,13 @@
     this.$.composebox.injectInput(title, thumbnail, fileToken);
   }
 
+  injectInputWithIcon(
+      title: string, iconId: IconType, fileToken: UnguessableToken) {
+    this.$.composebox.injectInput(
+        title, '', fileToken,
+        ICON_TYPE_TO_NAME[iconId as number] ?? 'unspecified');
+  }
+
   deleteFile(fileToken: UnguessableToken) {
     this.$.composebox.deleteFile(fileToken);
   }
diff --git a/chrome/browser/resources/contextual_tasks/ghost_loader.css b/chrome/browser/resources/contextual_tasks/ghost_loader.css
index af684828..b386b437 100644
--- a/chrome/browser/resources/contextual_tasks/ghost_loader.css
+++ b/chrome/browser/resources/contextual_tasks/ghost_loader.css
@@ -16,7 +16,7 @@
   display: flex;
   flex-direction: column;
   gap: 12px;
-  padding: 28px 15px;
+  padding-block-start: 28px;
 }
 
 .row {
diff --git a/chrome/browser/resources/lens/overlay/BUILD.gn b/chrome/browser/resources/lens/overlay/BUILD.gn
index 6503139d..4e7ae20 100644
--- a/chrome/browser/resources/lens/overlay/BUILD.gn
+++ b/chrome/browser/resources/lens/overlay/BUILD.gn
@@ -76,6 +76,8 @@
     "screenshot_bitmap_browser_proxy.ts",
     "screenshot_utils.ts",
     "searchbox_utils.ts",
+    "selection_overlay_base_element.ts",
+    "selection_overlay_base_handler.ts",
     "selection_utils.ts",
     "side_panel/feedback_toast.html.ts",
     "side_panel/feedback_toast.ts",
diff --git a/chrome/browser/resources/lens/overlay/browser_proxy.ts b/chrome/browser/resources/lens/overlay/browser_proxy.ts
index cb8d34d..5d1df61 100644
--- a/chrome/browser/resources/lens/overlay/browser_proxy.ts
+++ b/chrome/browser/resources/lens/overlay/browser_proxy.ts
@@ -2,8 +2,16 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import type {BitmapMappedFromTrustedProcess} from '//resources/mojo/skia/public/mojom/bitmap.mojom-webui.js';
+import type {RectF} from '//resources/mojo/ui/gfx/geometry/mojom/geometry.mojom-webui.js';
+
+import {CenterRotatedBox_CoordinateType} from './geometry.mojom-webui.js';
+import type {CenterRotatedBox} from './geometry.mojom-webui.js';
 import type {LensPageHandlerInterface} from './lens.mojom-webui.js';
-import {LensPageCallbackRouter, LensPageHandlerFactory, LensPageHandlerRemote} from './lens.mojom-webui.js';
+import {LensPageCallbackRouter, LensPageHandlerFactory, LensPageHandlerRemote, UserAction} from './lens.mojom-webui.js';
+import {INVOCATION_SOURCE} from './lens_overlay_app.js';
+import {recordLensOverlayInteraction} from './metrics_utils.js';
+import {RegionSource, SelectionOverlayBaseHandler} from './selection_overlay_base_handler.js';
 
 let instance: BrowserProxy|null = null;
 
@@ -12,6 +20,105 @@
   handler: LensPageHandlerInterface;
 }
 
+class SelectionOverlayBaseHandlerImpl implements SelectionOverlayBaseHandler {
+  addNotifyOverlayClosingListener(callback: () => void): number {
+    return BrowserProxyImpl.getInstance()
+        .callbackRouter.notifyOverlayClosing.addListener(callback);
+  }
+
+  closePreselectionBubble(): void {
+    BrowserProxyImpl.getInstance().handler.closePreselectionBubble();
+  }
+
+  notifyOverlayInitialized(): void {
+    BrowserProxyImpl.getInstance().handler.notifyOverlayInitialized();
+  }
+
+  addBackgroundBlur(): void {
+    BrowserProxyImpl.getInstance().handler.addBackgroundBlur();
+  }
+
+  setLiveBlur(enabled: boolean): void {
+    BrowserProxyImpl.getInstance().handler.setLiveBlur(enabled);
+  }
+
+  removeListener(id: number): boolean {
+    return BrowserProxyImpl.getInstance().callbackRouter.removeListener(id);
+  }
+
+  addOnOverlayReshownListener(
+      callback: (screenshotData: BitmapMappedFromTrustedProcess) => void):
+      number {
+    return BrowserProxyImpl.getInstance()
+        .callbackRouter.onOverlayReshown.addListener(callback);
+  }
+
+  addScreenshotDataReceivedListener(
+      callback:
+          (screenshotData: BitmapMappedFromTrustedProcess,
+           isSidePanelOpen: boolean) => void,
+      ): number {
+    return BrowserProxyImpl.getInstance()
+        .callbackRouter.screenshotDataReceived.addListener(callback);
+  }
+
+  addClearRegionSelectionListener(callback: () => void): number {
+    return BrowserProxyImpl.getInstance()
+        .callbackRouter.clearRegionSelection.addListener(callback);
+  }
+
+  addClearAllSelectionsListener(callback: () => void): number {
+    return BrowserProxyImpl.getInstance()
+        .callbackRouter.clearAllSelections.addListener(callback);
+  }
+
+  addNotifyResultsPanelOpenedListener(callback: () => void): number {
+    return BrowserProxyImpl.getInstance()
+        .callbackRouter.notifyResultsPanelOpened.addListener(callback);
+  }
+
+  addSetPostRegionSelectionListener(callback: (region: RectF) => void): number {
+    return BrowserProxyImpl.getInstance()
+        .callbackRouter.setPostRegionSelection.addListener(
+            this.postRegionSelectionCallback.bind(this, callback));
+  }
+
+  postRegionSelectionCallback(
+      callback: (region: RectF) => void, region: CenterRotatedBox): void {
+    callback(region.box);
+  }
+
+  adjustRegionSelected(rect: RectF, source: RegionSource): void {
+    let interaction = UserAction.kRegionSelection;
+    let isClick = false;
+    switch (source) {
+      case RegionSource.KEYBOARD:
+        interaction = UserAction.kFullScreenshotRegionSelection;
+        break;
+      case RegionSource.CLICK:
+        interaction = UserAction.kTapRegionSelection;
+        isClick = true;
+        break;
+      case RegionSource.SELECTION:
+        interaction = UserAction.kRegionSelection;
+        break;
+      case RegionSource.SELECTION_CHANGE:
+        interaction = UserAction.kRegionSelectionChange;
+        break;
+      default:
+        break;
+    }
+    recordLensOverlayInteraction(INVOCATION_SOURCE, interaction);
+    BrowserProxyImpl.getInstance().handler.issueLensRegionRequest(
+        {
+          box: rect,
+          rotation: 0,
+          coordinateType: CenterRotatedBox_CoordinateType.kNormalized,
+        },
+        isClick);
+  }
+}
+
 export class BrowserProxyImpl implements BrowserProxy {
   callbackRouter: LensPageCallbackRouter = new LensPageCallbackRouter();
   handler: LensPageHandlerRemote = new LensPageHandlerRemote();
@@ -21,6 +128,8 @@
     factory.createPageHandler(
         this.handler.$.bindNewPipeAndPassReceiver(),
         this.callbackRouter.$.bindNewPipeAndPassRemote());
+    SelectionOverlayBaseHandler.setInstance(
+        new SelectionOverlayBaseHandlerImpl());
   }
 
   static getInstance(): BrowserProxy {
@@ -29,5 +138,7 @@
 
   static setInstance(obj: BrowserProxy) {
     instance = obj;
+    SelectionOverlayBaseHandler.setInstance(
+        new SelectionOverlayBaseHandlerImpl());
   }
 }
diff --git a/chrome/browser/resources/lens/overlay/object_layer.ts b/chrome/browser/resources/lens/overlay/object_layer.ts
index 9aee501..13b447b 100644
--- a/chrome/browser/resources/lens/overlay/object_layer.ts
+++ b/chrome/browser/resources/lens/overlay/object_layer.ts
@@ -27,7 +27,7 @@
 import type {PostSelectionBoundingBox} from './post_selection_renderer.js';
 import {ScreenshotBitmapBrowserProxyImpl} from './screenshot_bitmap_browser_proxy.js';
 import {renderScreenshot} from './screenshot_utils.js';
-import type {CursorData} from './selection_overlay.js';
+import type {CursorData} from './selection_overlay_base_element.js';
 import {CursorType, focusShimmerOnRegion, type GestureEvent, ShimmerControlRequester, unfocusShimmer} from './selection_utils.js';
 import {toPercent} from './values_converter.js';
 
diff --git a/chrome/browser/resources/lens/overlay/overlay_shimmer_canvas.ts b/chrome/browser/resources/lens/overlay/overlay_shimmer_canvas.ts
index 69c71fd..9fe9cbb5 100644
--- a/chrome/browser/resources/lens/overlay/overlay_shimmer_canvas.ts
+++ b/chrome/browser/resources/lens/overlay/overlay_shimmer_canvas.ts
@@ -7,11 +7,11 @@
 import {loadTimeData} from '//resources/js/load_time_data.js';
 import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
-import {BrowserProxyImpl} from './browser_proxy.js';
 import {getFallbackTheme, getShaderLayerColorRgbas, modifyRgbaTransparency} from './color_utils.js';
 import {CubicBezier} from './cubic_bezier.js';
 import type {OverlayTheme} from './lens.mojom-webui.js';
 import {getTemplate} from './overlay_shimmer_canvas.html.js';
+import {SelectionOverlayBaseHandler} from './selection_overlay_base_handler.js';
 import type {OverlayShimmerFocusedRegion, OverlayShimmerUnfocusRegion, Point} from './selection_utils.js';
 import {ShimmerControlRequester} from './selection_utils.js';
 import {Wiggle} from './wiggle.js';
@@ -362,8 +362,8 @@
         });
 
     this.listenerIds = [
-      BrowserProxyImpl.getInstance()
-          .callbackRouter.notifyResultsPanelOpened.addListener(() => {
+      SelectionOverlayBaseHandler.getInstance()
+          .addNotifyResultsPanelOpenedListener(() => {
             this.areResultsShowing = true;
           }),
     ];
@@ -374,7 +374,7 @@
     this.eventTracker_.removeAll();
     this.listenerIds.forEach(
         id => assert(
-            BrowserProxyImpl.getInstance().callbackRouter.removeListener(id)));
+            SelectionOverlayBaseHandler.getInstance().removeListener(id)));
     this.listenerIds = [];
 
     // Stop updating the sparkles if they are currently updating.
diff --git a/chrome/browser/resources/lens/overlay/post_selection_renderer.ts b/chrome/browser/resources/lens/overlay/post_selection_renderer.ts
index e8f42c4f..de208df 100644
--- a/chrome/browser/resources/lens/overlay/post_selection_renderer.ts
+++ b/chrome/browser/resources/lens/overlay/post_selection_renderer.ts
@@ -6,19 +6,16 @@
 import {assert, assertInstanceof, assertNotReached} from '//resources/js/assert.js';
 import {EventTracker} from '//resources/js/event_tracker.js';
 import {loadTimeData} from '//resources/js/load_time_data.js';
+import type {RectF} from '//resources/mojo/ui/gfx/geometry/mojom/geometry.mojom-webui.js';
 import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
-import {BrowserProxyImpl} from './browser_proxy.js';
-import type {BrowserProxy} from './browser_proxy.js';
 import {GLIF_HEX_COLORS} from './color_utils.js';
 import {CenterRotatedBox_CoordinateType} from './geometry.mojom-webui.js';
 import type {CenterRotatedBox} from './geometry.mojom-webui.js';
-import {UserAction} from './lens.mojom-webui.js';
-import {INVOCATION_SOURCE} from './lens_overlay_app.js';
-import {recordLensOverlayInteraction} from './metrics_utils.js';
 import {getTemplate} from './post_selection_renderer.html.js';
 import {ScreenshotBitmapBrowserProxyImpl} from './screenshot_bitmap_browser_proxy.js';
 import {renderScreenshot} from './screenshot_utils.js';
+import {RegionSource, SelectionOverlayBaseHandler} from './selection_overlay_base_handler.js';
 import {focusShimmerOnRegion, ShimmerControlRequester, unfocusShimmer} from './selection_utils.js';
 import type {GestureEvent} from './selection_utils.js';
 import {toPercent, toPixels} from './values_converter.js';
@@ -167,7 +164,8 @@
   // The original bounds from the start of a drag or slider change.
   private originalBounds:
       PostSelectionBoundingBox = {left: 0, top: 0, width: 0, height: 0};
-  private browserProxy: BrowserProxy = BrowserProxyImpl.getInstance();
+  private baseHandler: SelectionOverlayBaseHandler =
+      SelectionOverlayBaseHandler.getInstance();
   private resizeObserver: ResizeObserver = new ResizeObserver(() => {
     this.handleResize();
   });
@@ -216,11 +214,11 @@
     this.resizeObserver.observe(this);
     // Set up listener to listen to events from C++.
     this.listenerIds = [
-      this.browserProxy.callbackRouter.clearAllSelections.addListener(
+      this.baseHandler.addClearAllSelectionsListener(
           this.clearSelection.bind(this)),
-      this.browserProxy.callbackRouter.clearRegionSelection.addListener(
+      this.baseHandler.addClearRegionSelectionListener(
           this.clearRegionSelection.bind(this)),
-      this.browserProxy.callbackRouter.setPostRegionSelection.addListener(
+      this.baseHandler.addSetPostRegionSelectionListener(
           this.setSelection.bind(this)),
     ];
   }
@@ -229,8 +227,7 @@
     super.disconnectedCallback();
     this.eventTracker_.removeAll();
     this.resizeObserver.unobserve(this);
-    this.listenerIds.forEach(
-        id => assert(this.browserProxy.callbackRouter.removeListener(id)));
+    this.listenerIds.forEach(id => assert(this.baseHandler.removeListener(id)));
     this.listenerIds = [];
   }
 
@@ -344,8 +341,9 @@
   handleGestureEnd() {
     if (this.areBoundsChanging()) {
       // Issue Lens request for new bounds
-      BrowserProxyImpl.getInstance().handler.issueLensRegionRequest(
-          this.getNormalizedCenterRotatedBox(), /*is_click=*/ false);
+      this.baseHandler.adjustRegionSelected(
+          this.getNormalizedCenterRotatedBox().box,
+          RegionSource.SELECTION_CHANGE);
 
       // Check for selectable text
       this.dispatchEvent(new CustomEvent('detect-text-in-region', {
@@ -353,9 +351,6 @@
         composed: true,
         detail: this.getNormalizedCenterRotatedBox(),
       }));
-
-      recordLensOverlayInteraction(
-          INVOCATION_SOURCE, UserAction.kRegionSelectionChange);
     }
 
     this.originalBounds = {left: 0, top: 0, width: 0, height: 0};
@@ -544,12 +539,12 @@
     }
   }
 
-  private setSelection(region: CenterRotatedBox) {
-    const normalizedTop = region.box.y - (region.box.height / 2);
-    const normalizedLeft = region.box.x - (region.box.width / 2);
+  private setSelection(region: RectF) {
+    const normalizedTop = region.y - (region.height / 2);
+    const normalizedLeft = region.x - (region.width / 2);
 
     this.setDimensions(
-        normalizedTop, normalizedLeft, region.box.height, region.box.width);
+        normalizedTop, normalizedLeft, region.height, region.width);
     this.originalBounds = {left: 0, top: 0, width: 0, height: 0};
 
     this.rerender();
diff --git a/chrome/browser/resources/lens/overlay/region_selection.ts b/chrome/browser/resources/lens/overlay/region_selection.ts
index 5e1bdae..78505bb 100644
--- a/chrome/browser/resources/lens/overlay/region_selection.ts
+++ b/chrome/browser/resources/lens/overlay/region_selection.ts
@@ -7,19 +7,15 @@
 import {loadTimeData} from '//resources/js/load_time_data.js';
 import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
-import {BrowserProxyImpl} from './browser_proxy.js';
-import type {BrowserProxy} from './browser_proxy.js';
 import {getFallbackTheme, getShaderLayerColorHexes, GLIF_HEX_COLORS} from './color_utils.js';
 import {CenterRotatedBox_CoordinateType} from './geometry.mojom-webui.js';
 import type {CenterRotatedBox} from './geometry.mojom-webui.js';
 import type {OverlayTheme} from './lens.mojom-webui.js';
-import {UserAction} from './lens.mojom-webui.js';
-import {INVOCATION_SOURCE} from './lens_overlay_app.js';
-import {recordLensOverlayInteraction} from './metrics_utils.js';
 import type {PostSelectionBoundingBox} from './post_selection_renderer.js';
 import {getTemplate} from './region_selection.html.js';
 import {ScreenshotBitmapBrowserProxyImpl} from './screenshot_bitmap_browser_proxy.js';
 import {renderScreenshot} from './screenshot_utils.js';
+import {RegionSource, SelectionOverlayBaseHandler} from './selection_overlay_base_handler.js';
 import {focusShimmerOnRegion, type GestureEvent, GestureState, getRelativeCoordinate, ShimmerControlRequester, unfocusShimmer} from './selection_utils.js';
 import type {Point} from './selection_utils.js';
 
@@ -140,7 +136,8 @@
   // Whether keyboard selection should be displayed.
   declare private displayKeyboardSelection: boolean;
 
-  private browserProxy: BrowserProxy = BrowserProxyImpl.getInstance();
+  private baseHandler: SelectionOverlayBaseHandler =
+      SelectionOverlayBaseHandler.getInstance();
 
   private readonly gradientRegionStrokeEnabled: boolean =
       loadTimeData.getBoolean('enableGradientRegionStroke');
@@ -205,9 +202,8 @@
     const isClick = event.state === GestureState.STARTING;
     const box = this.getNormalizedCenterRotatedBoxFromGesture(event);
     const region = this.getPostSelectionRegion(event);
-    const interaction =
-        isClick ? UserAction.kTapRegionSelection : UserAction.kRegionSelection;
-    this.issueRequest(isClick, box, region, interaction);
+    const interaction = isClick ? RegionSource.CLICK : RegionSource.SELECTION;
+    this.issueRequest(box, region, interaction);
     return true;
   }
 
@@ -217,19 +213,17 @@
       return false;
     }
 
-    this.issueRequest(/*isClick=*/ false,
-                      fullscreenNormalizedCenterRotatedBox(),
-                      fullscreenPostSelectionRegion(),
-                      UserAction.kFullScreenshotRegionSelection);
+    this.issueRequest(
+        fullscreenNormalizedCenterRotatedBox(), fullscreenPostSelectionRegion(),
+        RegionSource.KEYBOARD);
     return true;
   }
 
   private issueRequest(
-      isClick: boolean, box: CenterRotatedBox, region: PostSelectionBoundingBox,
-      interaction: UserAction) {
-    recordLensOverlayInteraction(INVOCATION_SOURCE, interaction);
+      box: CenterRotatedBox, region: PostSelectionBoundingBox,
+      source: RegionSource) {
     // Issue the Lens request.
-    this.browserProxy.handler.issueLensRegionRequest(box, isClick);
+    this.baseHandler.adjustRegionSelected(box.box, source);
 
     // Relinquish control from the shimmer.
     unfocusShimmer(this, ShimmerControlRequester.MANUAL_REGION);
diff --git a/chrome/browser/resources/lens/overlay/screenshot_bitmap_browser_proxy.ts b/chrome/browser/resources/lens/overlay/screenshot_bitmap_browser_proxy.ts
index 867036e6..a12ecbf 100644
--- a/chrome/browser/resources/lens/overlay/screenshot_bitmap_browser_proxy.ts
+++ b/chrome/browser/resources/lens/overlay/screenshot_bitmap_browser_proxy.ts
@@ -6,7 +6,7 @@
 import type {BigBuffer} from '//resources/mojo/mojo/public/mojom/base/big_buffer.mojom-webui.js';
 import type {BitmapMappedFromTrustedProcess} from '//resources/mojo/skia/public/mojom/bitmap.mojom-webui.js';
 
-import {BrowserProxyImpl} from './browser_proxy.js';
+import {SelectionOverlayBaseHandler} from './selection_overlay_base_handler.js';
 
 /**
  * @fileoverview A browser proxy for receiving the viewport screenshot from the
@@ -36,14 +36,12 @@
   private onOverlayReshownCallbacks: OverlayReshownCallback[] = [];
 
   constructor() {
-    this.screenshotListenerId =
-        BrowserProxyImpl.getInstance()
-            .callbackRouter.screenshotDataReceived.addListener(
-                this.screenshotDataReceived.bind(this));
+    this.screenshotListenerId = SelectionOverlayBaseHandler.getInstance()
+                                    .addScreenshotDataReceivedListener(
+                                        this.screenshotDataReceived.bind(this));
     this.onOverlayReshownListenerId =
-        BrowserProxyImpl.getInstance()
-            .callbackRouter.onOverlayReshown.addListener(
-                this.onOverlayReshown.bind(this));
+        SelectionOverlayBaseHandler.getInstance().addOnOverlayReshownListener(
+            this.onOverlayReshown.bind(this));
   }
 
   static getInstance(): ScreenshotBitmapBrowserProxy {
diff --git a/chrome/browser/resources/lens/overlay/selection_overlay.ts b/chrome/browser/resources/lens/overlay/selection_overlay.ts
index 0ef6b7e..cd2dad0 100644
--- a/chrome/browser/resources/lens/overlay/selection_overlay.ts
+++ b/chrome/browser/resources/lens/overlay/selection_overlay.ts
@@ -13,61 +13,36 @@
 import '//resources/cr_elements/cr_button/cr_button.js';
 import '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
 import '//resources/cr_elements/cr_toast/cr_toast.js';
+import '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import type {CrIconButtonElement} from '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
-import {I18nMixin} from '//resources/cr_elements/i18n_mixin.js';
 import {assert} from '//resources/js/assert.js';
-import {EventTracker} from '//resources/js/event_tracker.js';
 import {loadTimeData} from '//resources/js/load_time_data.js';
-import {afterNextRender, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {BrowserProxyImpl} from './browser_proxy.js';
 import type {BrowserProxy} from './browser_proxy.js';
-import {getFallbackTheme} from './color_utils.js';
 import {type CursorTooltipData, CursorTooltipType} from './cursor_tooltip.js';
 import type {CenterRotatedBox} from './geometry.mojom-webui.js';
 import {UserAction} from './lens.mojom-webui.js';
-import type {OverlayTheme} from './lens.mojom-webui.js';
 import {INVOCATION_SOURCE} from './lens_overlay_app.js';
 import {ContextMenuOption, recordContextMenuOptionShown, recordLensOverlayInteraction, recordLensOverlaySelectionCloseButtonShown, recordLensOverlaySelectionCloseButtonUsed} from './metrics_utils.js';
 import type {ObjectLayerElement} from './object_layer.js';
-import type {OverlayBorderGlowElement} from './overlay_border_glow.js';
 import type {OverlayShimmerCanvasElement} from './overlay_shimmer_canvas.js';
-import type {PostSelectionBoundingBox, PostSelectionRendererElement} from './post_selection_renderer.js';
+import type {PostSelectionRendererElement} from './post_selection_renderer.js';
 import type {RegionSelectionElement} from './region_selection.js';
-import {ScreenshotBitmapBrowserProxyImpl} from './screenshot_bitmap_browser_proxy.js';
-import {renderScreenshot} from './screenshot_utils.js';
 import {getTemplate} from './selection_overlay.html.js';
-import {CursorType, DRAG_THRESHOLD, DragFeature, emptyGestureEvent, focusShimmerOnRegion, GestureState, ShimmerControlRequester} from './selection_utils.js';
-import type {GestureEvent, OverlayShimmerFocusedRegion} from './selection_utils.js';
+import {CURSOR_IMG_URL, SelectionOverlayBaseElement} from './selection_overlay_base_element.js';
+import {DragFeature, GestureState} from './selection_utils.js';
 import type {SimplifiedTextLayerElement} from './simplified_text_layer.js';
 import type {TranslateState} from './translate_button.js';
 import {toPercent} from './values_converter.js';
 
-// The amount of margins in pixels to add to the screenshot when the window is
-// resized.
-const SCREENSHOT_FULLSIZE_MARGIN_PIXEL = 24;
-
-// The number of pixels the screenshot can differ from the viewport before
-// adding margins.
-const SCREENSHOT_RESIZE_TOLERANCE_PIXELS = 2;
-
-// The size of our custom cursor.
-export const CURSOR_SIZE_PIXEL = 32;
-
-// The cursor image url css variable name.
-export const CURSOR_IMG_URL = '--cursor-img-url';
-
 // Returns true if the event is a keystroke that should not activate a control.
 function shouldIgnoreKeyboardEvent(event: Event|undefined): boolean {
   return event instanceof KeyboardEvent &&
       !(event.key === 'Enter' || event.key === ' ');
 }
 
-export interface CursorData {
-  cursor: CursorType;
-}
-
 export interface SelectedTextContextMenuData {
   // The text selection that the context menu commands will act on.
   text: string;
@@ -116,8 +91,6 @@
   };
 }
 
-const SelectionOverlayElementBase = I18nMixin(PolymerElement);
-
 /*
  * Element responsible for coordinating selections between the various selection
  * features. This includes:
@@ -125,7 +98,7 @@
  *   - Listening to mouse/tap events and delegating them to the correct features
  *   - Coordinating animations between the different features
  */
-export class SelectionOverlayElement extends SelectionOverlayElementBase {
+export class SelectionOverlayElement extends SelectionOverlayBaseElement {
   static get is() {
     return 'lens-selection-overlay';
   }
@@ -134,24 +107,8 @@
     return getTemplate();
   }
 
-  static get properties() {
+  static override get properties() {
     return {
-      isScreenshotRendered: {
-        type: Boolean,
-        reflectToAttribute: true,
-        value: false,
-      },
-      isResized: {
-        type: Boolean,
-        reflectToAttribute: true,
-        value: false,
-        observer: 'onIsResizedChanged',
-      },
-      isInitialSize: {
-        type: Boolean,
-        reflectToAttribute: true,
-        value: true,
-      },
       showTranslateContextMenuItem: {
         type: Boolean,
         reflectToAttribute: true,
@@ -177,25 +134,6 @@
       selectedRegionContextMenuY: Number,
       selectedRegionContextMenuHorizontalStyle: String,
       selectedRegionContextMenuVerticalStyle: String,
-      canvasHeight: Number,
-      canvasWidth: Number,
-      isPointerInside: {
-        type: Boolean,
-        value: false,
-      },
-      currentGesture: {
-        type: Object,
-        value: () => emptyGestureEvent(),
-      },
-      disableShimmer: {
-        type: Boolean,
-        readOnly: true,
-        value: !loadTimeData.getBoolean('enableShimmer'),
-      },
-      enableBorderGlow: {
-        type: Boolean,
-        value: () => loadTimeData.getBoolean('enableBorderGlow'),
-      },
       enableCopyAsImage: {
         type: Boolean,
         reflectToAttribute: true,
@@ -206,11 +144,6 @@
         reflectToAttribute: true,
         value: () => loadTimeData.getBoolean('enableSaveAsImage'),
       },
-      isClosing: {
-        type: Boolean,
-        reflectToAttribute: true,
-        value: false,
-      },
       suppressCopyAndSaveAsImage: {
         type: Boolean,
         reflectToAttribute: true,
@@ -219,56 +152,16 @@
               'ContentAreaContextMenuImage';
         },
       },
-      shimmerOnSegmentation: {
-        type: Boolean,
-        reflectToAttribute: true,
-        value: false,
-      },
-      shimmerFadeOutComplete: {
-        type: Boolean,
-        reflectToAttribute: true,
-        value: true,
-      },
-      darkenExtraScrim: {
-        type: Boolean,
-        reflectToAttribute: true,
-        value: false,
-      },
-      theme: {
-        type: Object,
-        value: getFallbackTheme,
-      },
       translateModeEnabled: {
         type: Boolean,
         reflectToAttribute: true,
         value: false,
       },
-      selectionOverlayRect: Object,
       isSearchboxFocused: Boolean,
       areLanguagePickersOpen: Boolean,
-      sidePanelOpened: {
-        type: Boolean,
-        reflectToAttribute: true,
-        value: false,
-      },
-      hideBackgroundImageCanvas: {
-        type: Boolean,
-        reflectToAttribute: true,
-        value: false,
-      },
-      enableRegionContextMenu: {
-        type: Boolean,
-        value: true,
-        reflectToAttribute: true,
-      },
     };
   }
 
-  // Whether the screenshot has finished loading in.
-  declare private isScreenshotRendered: boolean;
-  // Whether the selection overlay is its initial size, or has changed size.
-  declare private isResized: boolean;
-  declare private isInitialSize: boolean;
   declare private showTranslateContextMenuItem: boolean;
   declare private showSelectedTextContextMenu: boolean;
   declare private showSelectedRegionContextMenu: boolean;
@@ -278,16 +171,8 @@
   declare private selectedTextContextMenuY: number;
   declare private selectedRegionContextMenuX: number;
   declare private selectedRegionContextMenuY: number;
-  // Width and height values for rendering the background image canvas as the
-  // proper dimensions.
-  declare private canvasHeight: number;
-  declare private canvasWidth: number;
   declare private selectedRegionContextMenuHorizontalStyle: string;
   declare private selectedRegionContextMenuVerticalStyle: string;
-  // The current content rectangle of the selection elements DIV. This is the
-  // bounds of the screenshot and the part the user interacts with. This should
-  // be used instead of call getBoundingClientRect().
-  declare private selectionOverlayRect: DOMRect;
   // Whether the users focus is currently in the overlay searchbox. Passed in
   // from parent.
   declare private isSearchboxFocused: boolean;
@@ -304,98 +189,58 @@
   private textSelectionEndIndex: number = -1;
   private detectedTextStartIndex: number = -1;
   private detectedTextEndIndex: number = -1;
-  declare private isPointerInside;
   private isPointerInsideButton = false;
-  // The current gesture event. The coordinate values are only accurate if a
-  // gesture has started.
-  declare private currentGesture: GestureEvent;
-  declare private disableShimmer: boolean;
-  declare private enableBorderGlow: boolean;
   declare private enableCopyAsImage: boolean;
   declare private enableSaveAsImage: boolean;
   declare private suppressCopyAndSaveAsImage: boolean;
-  // Whether the overlay is being shut down.
-  declare private isClosing: boolean;
-  // Whether the default background scrim is currently being darkened.
-  declare private darkenExtraScrim: boolean;
-  // Whether the shimmer is currently focused on a segmentation mask.
-  declare private shimmerOnSegmentation: boolean;
-  declare private shimmerFadeOutComplete: boolean;
-  // Whether the side panel is currently opened.
-  declare private sidePanelOpened: boolean;
-  // Whether the background image canvas should currently be shown.
-  declare private hideBackgroundImageCanvas: boolean;
-  // Whether the region context menu is enabled.
-  declare private enableRegionContextMenu: boolean;
 
-  // The border glow layer rendered on the selection overlay if it exists.
-  private overlayBorderGlow: OverlayBorderGlowElement;
-
-  private eventTracker_: EventTracker = new EventTracker();
-  // Listener ids for events from the browser side.
-  private listenerIds: number[];
-  // The feature currently being dragged. Once a feature responds to a drag
-  // event, no other feature will receive gesture events.
-  private draggingRespondent = DragFeature.NONE;
-  private resizeObserver: ResizeObserver =
-      new ResizeObserver(this.handleResize.bind(this));
-  // Used to listen for changes in the window.devicePixelRatio. Stored as a
-  // variable so we can easily add and remove the listener.
-  private matchMedia?: MediaQueryList;
-  private cursorOffsetX: number = 3;
-  private cursorOffsetY: number = 6;
-  private hasInitialFlashAnimationEnded = false;
   private browserProxy: BrowserProxy = BrowserProxyImpl.getInstance();
 
-  // The ID returned by requestAnimationFrame for the updateCursorPosition,
-  // onPointerMove, and handleResize functions.
-  private updateCursorPositionRequestId?: number;
-  private onPointerMoveRequestId?: number;
-  private handleResizeRequestId?: number;
-
   // Whether the close button used metric was recorded in this session.
   private closeButtonUsedRecorded = false;
 
-  declare private theme: OverlayTheme;
-
   // Whether or not translate mode is enabled. If true, only text should
   // be selectable, and it should be selectable from any point in the
   // overlay.
   declare private translateModeEnabled: boolean;
 
+  protected backgroundImageCanvas(): HTMLCanvasElement {
+    return this.$.backgroundImageCanvas;
+  }
+
+  protected cursor(): HTMLElement {
+    return this.$.cursor;
+  }
+
+  protected initialFlashScrim(): HTMLElement {
+    return this.$.initialFlashScrim;
+  }
+
+  protected overlayShimmerCanvas(): OverlayShimmerCanvasElement {
+    return this.$.overlayShimmerCanvas;
+  }
+
+  protected postSelectionRenderer(): PostSelectionRendererElement {
+    return this.$.postSelectionRenderer;
+  }
+
+  protected regionSelectionLayer(): RegionSelectionElement {
+    return this.$.regionSelectionLayer;
+  }
+
+  protected selectionOverlay(): HTMLElement {
+    return this.$.selectionOverlay;
+  }
+
   override connectedCallback() {
     super.connectedCallback();
-    this.resizeObserver.observe(this);
     this.listenerIds = [
-      this.browserProxy.callbackRouter.notifyOverlayClosing.addListener(() => {
-        this.isClosing = true;
-        this.removeDragListeners();
-      }),
+      ...this.listenerIds,
       this.browserProxy.callbackRouter.onCopyCommand.addListener(
           this.onCopyCommand.bind(this)),
       this.browserProxy.callbackRouter.notifyResultsPanelOpened.addListener(
           this.onNotifyResultsPanelOpened.bind(this)),
     ];
-    ScreenshotBitmapBrowserProxyImpl.getInstance().fetchScreenshot(
-        this.screenshotDataReceived.bind(this));
-    ScreenshotBitmapBrowserProxyImpl.getInstance().addOnOverlayReshownListener(
-        this.onOverlayReshown.bind(this));
-    this.eventTracker_.add(
-        document, 'shimmer-fade-out-complete', (e: CustomEvent<boolean>) => {
-          this.shimmerFadeOutComplete = e.detail;
-        });
-    this.eventTracker_.add(
-        document, 'set-cursor', (e: CustomEvent<CursorData>) => {
-          if (e.detail.cursor === CursorType.POINTER) {
-            this.setCursorToPointer();
-          } else if (e.detail.cursor === CursorType.CROSSHAIR) {
-            this.setCursorToCrosshair();
-          } else if (e.detail.cursor === CursorType.TEXT) {
-            this.setCursorToText();
-          } else {
-            this.resetCursor();
-          }
-        });
     this.eventTracker_.add(
         document, 'translate-mode-state-changed',
         (e: CustomEvent<TranslateState>) => {
@@ -481,27 +326,6 @@
           this.detectedTextStartIndex = -1;
           this.detectedTextEndIndex = -1;
         });
-    this.eventTracker_.add(document, 'darken-extra-scrim-opacity', () => {
-      this.darkenExtraScrim = true;
-    });
-    this.eventTracker_.add(document, 'lighten-extra-scrim-opacity', () => {
-      this.darkenExtraScrim = false;
-    });
-    this.eventTracker_.add(
-        this.$.initialFlashScrim, 'animationend', (event: AnimationEvent) => {
-          // The flash animation is the longest animation.
-          if (event.animationName !== 'initial-inset-animation') {
-            return;
-          }
-          this.onInitialFlashAnimationEnd();
-        });
-    this.eventTracker_.add(
-        document, 'focus-region',
-        (e: CustomEvent<OverlayShimmerFocusedRegion>) => {
-          if (e.detail.requester === ShimmerControlRequester.SEGMENTATION) {
-            this.shimmerOnSegmentation = true;
-          }
-        });
       this.eventTracker_.add(
           document, 'post-selection-updated', (e: CustomEvent) => {
             this.selectedRegionContextMenuBox = e.detail.centerRotatedBox;
@@ -512,180 +336,25 @@
                 this.selectedRegionContextMenuBox.box.y +
                 this.selectedRegionContextMenuBox.box.height / 2;
           });
-    this.eventTracker_.add(document, 'unfocus-region', () => {
-      this.shimmerOnSegmentation = false;
-    });
-    if (this.enableBorderGlow) {
-      this.eventTracker_.add(
-          document, 'post-selection-updated',
-          (e: CustomEvent<PostSelectionBoundingBox>) => {
-            this.handlePostSelectionUpdated(e.detail.height, e.detail.width);
-          });
+  }
+
+  protected override shouldIgnoreEvent(event: PointerEvent) {
+    if (super.shouldIgnoreEvent(event)) {
+      return true;
     }
-
-    this.updateSelectionOverlayRect();
-    this.updateDevicePixelRatioListener();
-  }
-
-  override disconnectedCallback() {
-    super.disconnectedCallback();
-    this.resizeObserver.unobserve(this);
-    this.eventTracker_.removeAll();
-    this.listenerIds.forEach(
-        id => assert(this.browserProxy.callbackRouter.removeListener(id)));
-    this.listenerIds = [];
-
-    assert(this.matchMedia);
-    this.matchMedia.removeEventListener(
-        'change', this.onDevicePixelRatioChanged.bind(this));
-  }
-
-  override ready() {
-    super.ready();
-    this.addEventListener('pointerdown', this.onPointerDown.bind(this));
-    this.addEventListener('pointermove', this.updateCursorPosition.bind(this));
-  }
-
-  private addDragListeners() {
-    this.addEventListener('pointerup', this.onPointerUp);
-    this.addEventListener('pointermove', this.onPointerMove);
-    this.addEventListener('pointercancel', this.onPointerCancel);
-  }
-
-  private removeDragListeners() {
-    this.removeEventListener('pointerup', this.onPointerUp);
-    this.removeEventListener('pointermove', this.onPointerMove);
-    this.removeEventListener('pointercancel', this.onPointerCancel);
-  }
-
-  private updateDevicePixelRatioListener() {
-    // Remove the previous listener since we are now listening for a different
-    // pixel ratio change.
-    if (this.matchMedia) {
-      this.eventTracker_.remove(this.matchMedia, 'change');
+    const elementsAtPoint =
+        this.shadowRoot!.elementsFromPoint(event.clientX, event.clientY);
+    // Do not intercept events that should go to the following elements.
+    if (elementsAtPoint.includes(this.$.selectedTextContextMenu) ||
+        elementsAtPoint.includes(this.$.selectedRegionContextMenu) ||
+        elementsAtPoint.includes(this.$.closeButton)) {
+      return true;
     }
-
-    // Listen to changes to the current device pixel ratio.
-    const queryString = `(resolution: ${window.devicePixelRatio}dppx)`;
-    this.matchMedia = matchMedia(queryString);
-    this.eventTracker_.add(
-        this.matchMedia, 'change', this.onDevicePixelRatioChanged.bind(this));
+    return false;
   }
 
-  private onDevicePixelRatioChanged() {
-    // Update the listener to the new pixel ratio.
-    this.updateDevicePixelRatioListener();
-    // Resize the canvases to take the new pixel ratio change.\
-    this.resizeSelectionCanvases(
-        this.selectionOverlayRect.width, this.selectionOverlayRect.height);
-  }
-
-  private updateCursorPosition(event: PointerEvent) {
-    // Cancel a pending event to prevent multiple updates per frame.
-    if (this.updateCursorPositionRequestId) {
-      cancelAnimationFrame(this.updateCursorPositionRequestId);
-    }
-
-    // Use requestAnimationFrame to only update the cursor once a frame instead
-    // of multiple times per frame. This helps ensure the cursor is being
-    // updated to the latest received pointer event.
-    this.updateCursorPositionRequestId = requestAnimationFrame(() => {
-      const mouseX = event.clientX;
-      const mouseY = event.clientY;
-
-      const cursorOffsetX = mouseX + this.cursorOffsetX;
-      const cursorOffsetY = mouseY + this.cursorOffsetY;
-
-      if (!this.disableShimmer &&
-          (this.isPointerInside ||
-           this.currentGesture.state === GestureState.DRAGGING)) {
-        this.updateShimmerForCursor(cursorOffsetX, cursorOffsetY);
-      }
-
-      this.$.cursor.style.transform =
-          `translate3d(${cursorOffsetX}px, ${cursorOffsetY}px, 0)`;
-      this.updateCursorPositionRequestId = undefined;
-    });
-  }
-
-  private updateShimmerForCursor(cursorLeft: number, cursorTop: number) {
-    const relativeXPercent =
-        Math.max(
-            0,
-            Math.min(cursorLeft, this.selectionOverlayRect.right) -
-                this.selectionOverlayRect.left) /
-        this.selectionOverlayRect.width;
-    const relativeYPercent =
-        Math.max(
-            0,
-            Math.min(cursorTop, this.selectionOverlayRect.bottom) -
-                this.selectionOverlayRect.top) /
-        this.selectionOverlayRect.height;
-
-    focusShimmerOnRegion(
-        this, relativeYPercent, relativeXPercent,
-        CURSOR_SIZE_PIXEL / this.selectionOverlayRect.width,
-        CURSOR_SIZE_PIXEL / this.selectionOverlayRect.height,
-        ShimmerControlRequester.CURSOR);
-  }
-
-  protected onIsResizedChanged(newValue: boolean): void {
-    this.browserProxy.handler.setLiveBlur(newValue);
-  }
-
-  private getHiddenCursorClass(isPointerInside: boolean, state: GestureState):
-      string {
-    // Always show when dragging, even if outside the selection overlay.
-    if (!isPointerInside && state !== GestureState.DRAGGING) {
-      return 'hidden';
-    } else {
-      return '';
-    }
-  }
-
-  // LINT.IfChange(CursorOffsetValues)
-  // Called on text hover and drag.
-  private setCursorToText() {
-    // Set body cursor style to handle dragging.
-    document.body.style.cursor = 'text';
-    this.cursorOffsetX = 3;
-    this.cursorOffsetY = 8;
-    this.style.setProperty(CURSOR_IMG_URL, 'url("text.svg")');
-  }
-
-  // Called on region selection drag.
-  private setCursorToCrosshair() {
-    // Set body cursor style to handle dragging.
-    document.body.style.cursor = 'crosshair';
-    this.cursorOffsetX = 3;
-    this.cursorOffsetY = 6;
-    this.style.setProperty(CURSOR_IMG_URL, 'url("lens.svg")');
-  }
-
-  // Called on object hover.
-  private setCursorToPointer() {
-    // No dragging for objects, so no need to set body cursor style.
-    this.cursorOffsetX = 11;
-    this.cursorOffsetY = 17;
-    this.style.setProperty(CURSOR_IMG_URL, 'url("lens.svg")');
-  }
-
-  private resetCursor() {
-    if (this.translateModeEnabled) {
-      // If translate mode is enabled, the default cursor state should be
-      // text.
-      this.setCursorToText();
-      return;
-    }
-    document.body.style.cursor = 'unset';
-    this.cursorOffsetX = 3;
-    this.cursorOffsetY = 6;
-    this.style.setProperty(CURSOR_IMG_URL, 'url("lens.svg")');
-  }
-  // LINT.ThenChange(//chrome/browser/resources/lens/overlay/cursor_tooltip.ts:CursorOffsetValues)
-
-  private handlePointerEnter() {
-    this.isPointerInside = true;
+  protected override handlePointerEnter() {
+    super.handlePointerEnter();
     if (!this.isPointerInsideButton) {
       this.dispatchEvent(
           new CustomEvent<CursorTooltipData>('set-cursor-tooltip', {
@@ -700,43 +369,55 @@
     }
   }
 
-  private handlePointerLeave() {
-    this.isPointerInside = false;
-  }
-
-  private onImageRendered() {
-    // Let the parent know it is safe to blur the background.
-    this.dispatchEvent(new CustomEvent('screenshot-rendered', {
-      bubbles: true,
-      composed: true,
-      detail: {isSidePanelOpen: this.sidePanelOpened},
-    }));
-    this.browserProxy.handler.notifyOverlayInitialized();
-  }
-
-  private onPointerDown(event: PointerEvent) {
-    if (this.shouldIgnoreEvent(event)) {
+  protected override handleRightClick(event: PointerEvent) {
+    if (this.$.textLayer.handleRightClick(event)) {
       return;
     }
+    super.handleRightClick(event);
+  }
 
-    if (event.button === 2 /* right button */) {
-      if (this.$.textLayer.handleRightClick(event)) {
-        return;
-      }
-      this.$.postSelectionRenderer.handleRightClick(event);
+  // LINT.IfChange(CursorOffsetValues)
+  // Called on text hover and drag.
+  protected override setCursorToText() {
+    // Set body cursor style to handle dragging.
+    document.body.style.cursor = 'text';
+    this.cursorOffsetX = 3;
+    this.cursorOffsetY = 8;
+    this.style.setProperty(CURSOR_IMG_URL, 'url("text.svg")');
+  }
+
+  // Called on region selection drag.
+  protected override setCursorToCrosshair() {
+    // Set body cursor style to handle dragging.
+    document.body.style.cursor = 'crosshair';
+    this.cursorOffsetX = 3;
+    this.cursorOffsetY = 6;
+    this.style.setProperty(CURSOR_IMG_URL, 'url("lens.svg")');
+  }
+
+  // Called on object hover.
+  protected override setCursorToPointer() {
+    // No dragging for objects, so no need to set body cursor style.
+    this.cursorOffsetX = 11;
+    this.cursorOffsetY = 17;
+    this.style.setProperty(CURSOR_IMG_URL, 'url("lens.svg")');
+  }
+
+  protected override resetCursor() {
+    if (this.translateModeEnabled) {
+      // If translate mode is enabled, the default cursor state should be
+      // text.
+      this.setCursorToText();
       return;
     }
+    document.body.style.cursor = 'unset';
+    this.cursorOffsetX = 3;
+    this.cursorOffsetY = 6;
+    this.style.setProperty(CURSOR_IMG_URL, 'url("lens.svg")');
+  }
+  // LINT.ThenChange(//chrome/browser/resources/lens/overlay/cursor_tooltip.ts:CursorOffsetValues)
 
-    this.addDragListeners();
-
-    this.currentGesture = {
-      state: GestureState.NOT_STARTED,
-      startX: event.clientX,
-      startY: event.clientY,
-      clientX: event.clientX,
-      clientY: event.clientY,
-    };
-
+  protected override pointerDownHandled() {
     // Try to close the translate feature promo if it is currently active. No-op
     // if it is not active.
     this.browserProxy.handler.maybeCloseTranslateFeaturePromo(
@@ -751,79 +432,9 @@
     }
   }
 
-  private onPointerUp(event: PointerEvent) {
-    this.updateGestureCoordinates(event);
-
-    if (this.currentGesture.state === GestureState.DRAGGING) {
-      // Cancel the animation frame and handle the drag event immediately so
-      // handleGestureEnd is not in an unexpected state.
-      this.cancelPendingDragAnimationFrame();
-      this.handleGestureDrag(event);
-    }
-
-    // Allow the clients to respond to the gesture, IFF a gesture has started.
-    if (this.currentGesture.state !== GestureState.NOT_STARTED) {
-      this.handleGestureEnd();
-    }
-
-    // After features have responded to the event, reset the current drag state.
-    this.currentGesture = emptyGestureEvent();
-    this.draggingRespondent = DragFeature.NONE;
-    this.removeDragListeners();
-  }
-
-  private onPointerMove(event: PointerEvent) {
-    this.updateGestureCoordinates(event);
-
-    // Ignore the event if the user isn't explicitly dragging yet.
-    if (!this.isDragging()) {
-      return;
-    }
-
-    if (this.currentGesture.state === GestureState.NOT_STARTED) {
-      // If a gesture hasn't started, start the gesture now that the user is
-      // dragging.
-      this.handleGestureStart();
-    }
-
-    if (this.currentGesture.state === GestureState.STARTING) {
-      // If the gesture just started, move into the dragging state.
-      this.set('currentGesture.state', GestureState.DRAGGING);
-    }
-
-    // If we haven't exited early, we must be in the dragging state.
-    assert(this.currentGesture.state === GestureState.DRAGGING);
-
-    // Handle the drag.
-    this.cancelPendingDragAnimationFrame();
-    this.onPointerMoveRequestId = requestAnimationFrame(() => {
-      this.handleGestureDrag(event);
-      this.onPointerMoveRequestId = undefined;
-    });
-  }
-
-  private cancelPendingDragAnimationFrame() {
-    if (this.onPointerMoveRequestId) {
-      cancelAnimationFrame(this.onPointerMoveRequestId);
-    }
-  }
-
-  private onPointerCancel() {
-    // Pointer cancelled, so cancel any pending gestures.
-    this.handleGestureCancel();
-
-    this.currentGesture = emptyGestureEvent();
-    this.draggingRespondent = DragFeature.NONE;
-    this.removeDragListeners();
-    this.resetCursor();
-  }
-
-  private handleGestureStart() {
-    this.set('currentGesture.state', GestureState.STARTING);
-
-    // Send events to hide UI.
-    this.browserProxy.handler.closePreselectionBubble();
+  override handleGestureStart() {
     this.suppressCopyAndSaveAsImage = false;
+    super.handleGestureStart();
     this.dispatchEvent(
         new CustomEvent('selection-started', {bubbles: true, composed: true}));
 
@@ -834,19 +445,6 @@
     this.showDetectedTextContextMenuOptions = false;
 
     this.$.textLayer.onSelectionStart();
-    if (this.enableBorderGlow) {
-      this.getOverlayBorderGlow().handleGestureStart();
-
-      // If there is no post selection, fade the scrim from the region selection
-      // back in.
-      if (!this.$.postSelectionRenderer.hasSelection()) {
-        // TODO(crbug.com/421002691): follow the convention where the layer
-        // should return true if its handling the gesture, and
-        // draggingRespondent should be updated. Currently used to trigger the
-        // fade in of the darkened scrim.
-        this.$.regionSelectionLayer.handleGestureStart();
-      }
-    }
 
     if (this.$.postSelectionRenderer.handleGestureStart(this.currentGesture)) {
       this.draggingRespondent = DragFeature.POST_SELECTION;
@@ -857,7 +455,7 @@
     }
   }
 
-  private handleGestureDrag(event: PointerEvent) {
+  protected override handleGestureDrag(event: PointerEvent) {
     assert(this.currentGesture.state === GestureState.DRAGGING);
     // Capture pointer events so gestures still work if the users pointer
     // leaves the selection overlay div. Pointer capture is implicitly
@@ -887,7 +485,7 @@
     }
   }
 
-  private handleGestureEnd() {
+  protected override handleGestureEnd() {
     // Call onSelectionFinish before gesture is handled so the simplified text
     // layer can reset the context menu.
     this.$.textLayer.onSelectionFinish();
@@ -936,169 +534,21 @@
     }));
   }
 
-  private handleGestureCancel() {
+  protected override handleGestureCancel() {
     this.$.textLayer.cancelGesture();
-    this.$.regionSelectionLayer.cancelGesture();
-    this.$.postSelectionRenderer.cancelGesture();
+    super.handleGestureCancel();
   }
 
-  private handlePostSelectionUpdated(height: number, width: number) {
-    const overlayBorderGlow = this.getOverlayBorderGlow();
-    // If there is no selection happening, fade the glow back in.
-    if (width === 0 && height === 0 &&
-        this.draggingRespondent === DragFeature.NONE) {
-      overlayBorderGlow.handleClearSelection();
-      this.$.regionSelectionLayer.handlePostSelectionCleared();
-      return;
-    }
-
-    overlayBorderGlow.handlePostSelectionUpdated();
-  }
-
-  private updateCanvasSize(containerWidth: number, containerHeight: number) {
-    // Set our own canvas size while preserving the canvas aspect ratio.
-    const screenshotHeight = this.$.backgroundImageCanvas.height;
-    const screenshotWidth = this.$.backgroundImageCanvas.width;
-
-    const doesScreenshotFillContainer =
-        Math.abs(
-            containerWidth - (screenshotWidth / window.devicePixelRatio)) <=
-            SCREENSHOT_RESIZE_TOLERANCE_PIXELS &&
-        Math.abs(
-            containerHeight - (screenshotHeight / window.devicePixelRatio)) <=
-            SCREENSHOT_RESIZE_TOLERANCE_PIXELS;
-    const shouldApplyMargins =
-        !doesScreenshotFillContainer || this.sidePanelOpened;
-
-    // Apply margins if the page is resized / side panel is opened and not
-    // closing.
-    const margins = shouldApplyMargins && !this.isClosing ?
-        SCREENSHOT_FULLSIZE_MARGIN_PIXEL * 2 :
-        0;
-    const newContainerWidth = containerWidth - margins;
-    const newContainerHeight = containerHeight - margins;
-
-    // Get the aspect ratio to force the image to conform to.
-    const aspectRatio = this.$.backgroundImageCanvas.width /
-        this.$.backgroundImageCanvas.height;
-
-    // Calculate potential dimensions based on width and height
-    const widthBasedHeight = Math.round(newContainerWidth / aspectRatio);
-    const heightBasedWidth = Math.round(newContainerHeight * aspectRatio);
-
-    // Choose dimensions that fit within the container while preserving aspect
-    // ratio
-    if (widthBasedHeight <= newContainerHeight) {
-      // Width-based dimensions fit
-      this.canvasHeight = widthBasedHeight;
-      this.canvasWidth = newContainerWidth;
-    } else {
-      // Height-based dimensions fit
-      this.canvasWidth = heightBasedWidth;
-      this.canvasHeight = newContainerHeight;
-    }
-
-    this.isResized = shouldApplyMargins;
-    if (this.isResized) {
-      this.isInitialSize = false;
-      // The flash animation is cut short but animationend is never called if
-      // the overlay is resized before animationend is called. This is because
-      // the flash scrim is hidden on resize.
-      this.onInitialFlashAnimationEnd();
-    }
-  }
-
-  private handleResize(entries: ResizeObserverEntry[]) {
-    // Cancel a pending event to prevent multiple updates per frame.
-    if (this.handleResizeRequestId) {
-      cancelAnimationFrame(this.handleResizeRequestId);
-    }
-
-    // Use requestAnimationFrame to only calculate the screenshot size once
-    // a frame instead of multiple times per frame.
-    this.handleResizeRequestId = requestAnimationFrame(() => {
-      assert(entries.length === 1);
-      const newRect = entries[0].contentRect;
-
-      // If the screenshot is not rendered yet, there is nothing to do yet.
-      if (!this.isScreenshotRendered ||
-          (newRect.width === 0 && newRect.height === 0)) {
-        this.handleResizeRequestId = undefined;
-        return;
-      }
-
-      this.updateCanvasSize(newRect.width, newRect.height);
-      this.positionSelectedRegionContextMenu();
-
-      // Update our cached selection overlay rect to the new bounds.
-      this.updateSelectionOverlayRect();
-
-      // TODO(b/361798599): Since we now pass selectionOverlayRect, we can use
-      // polymer events to allow each client to resize their canvas once
-      // selectionOverlayRect changes. We should remove this and do the
-      // resizing via polymer techniques.
-      this.resizeSelectionCanvases(
-          this.selectionOverlayRect.width, this.selectionOverlayRect.height);
-
-      this.handleResizeRequestId = undefined;
-    });
-  }
-
-  private updateSelectionOverlayRect(): void {
-    // We use offsetXXX instead of call this.getBoundingClientRect() because
-    // offsetXXX is a cached DOM property, while this.getBoundingClientRect()
-    // recalculates the layout every time it is called. Since we have no
-    // scrolling, these calls should be equivalent.
-    this.selectionOverlayRect = new DOMRect(
-        this.$.selectionOverlay.offsetLeft, this.$.selectionOverlay.offsetTop,
-        this.$.selectionOverlay.offsetWidth,
-        this.$.selectionOverlay.offsetHeight);
-  }
-
-  private resizeSelectionCanvases(newWidth: number, newHeight: number) {
-    this.$.regionSelectionLayer.setCanvasSizeTo(newWidth, newHeight);
-    this.$.postSelectionRenderer.setCanvasSizeTo(newWidth, newHeight);
+  protected override resizeSelectionCanvases(
+      newWidth: number, newHeight: number) {
+    this.positionSelectedRegionContextMenu();
     this.$.objectSelectionLayer.setCanvasSizeTo(newWidth, newHeight);
-    this.$.overlayShimmerCanvas.setCanvasSizeTo(newWidth, newHeight);
+    super.resizeSelectionCanvases(newWidth, newHeight);
   }
 
-  // Updates the currentGesture to correspond with the given PointerEvent.
-  private updateGestureCoordinates(event: PointerEvent) {
-    this.currentGesture.clientX = event.clientX;
-    this.currentGesture.clientY = event.clientY;
-  }
-
-  // Returns if the given PointerEvent should be ignored.
-  private shouldIgnoreEvent(event: PointerEvent) {
-    if (this.isClosing) {
-      return true;
-    }
-    const elementsAtPoint =
-        this.shadowRoot!.elementsFromPoint(event.clientX, event.clientY);
-    // Do not intercept events that should go to the following elements.
-    if (elementsAtPoint.includes(this.$.selectedTextContextMenu) ||
-        elementsAtPoint.includes(this.$.selectedRegionContextMenu) ||
-        elementsAtPoint.includes(this.$.closeButton)) {
-      return true;
-    }
-    // Ignore multi touch events and non-left/right click events.
-    return !event.isPrimary || (event.button !== 0 && event.button !== 2);
-  }
-
-  // Returns whether the current gesture event is a drag.
-  private isDragging() {
-    if (this.currentGesture.state === GestureState.DRAGGING) {
-      return true;
-    }
-
-    // TODO(b/329514345): Revisit if pointer movement is enough of an indicator,
-    // or if we also need a timelimit on how long a tap can last before starting
-    // a drag.
-    const xMovement =
-        Math.abs(this.currentGesture.clientX - this.currentGesture.startX);
-    const yMovement =
-        Math.abs(this.currentGesture.clientY - this.currentGesture.startY);
-    return xMovement > DRAG_THRESHOLD || yMovement > DRAG_THRESHOLD;
+  protected override setSidePanelOpened() {
+    super.setSidePanelOpened();
+    this.$.textLayer.disableHighlights();
   }
 
   // Repositions the context menu to keep it inside the viewport.
@@ -1327,91 +777,6 @@
     this.showSelectedRegionContextMenu = shouldShow;
   }
 
-  private onInitialFlashAnimationEnd() {
-    if (this.hasInitialFlashAnimationEnded) {
-      return;
-    }
-    this.hasInitialFlashAnimationEnded = true;
-    this.eventTracker_.remove(this.$.initialFlashScrim, 'animationend');
-
-    this.browserProxy.handler.addBackgroundBlur();
-
-    // Let the parent know the initial flash image animation has finished.
-    this.dispatchEvent(new CustomEvent(
-        'initial-flash-animation-end', {bubbles: true, composed: true}));
-
-    // Don't start the shimmer animation until the initial flash animation is
-    // finished.
-    if (!this.disableShimmer && !this.enableBorderGlow) {
-      this.$.overlayShimmerCanvas.startAnimation();
-    }
-  }
-
-  private screenshotDataReceived(
-      screenshotBitmap: ImageBitmap, isSidePanelOpen: boolean) {
-    renderScreenshot(this.$.backgroundImageCanvas, screenshotBitmap);
-    // Start the canvas as the same dimensions as the viewport, since we are
-    // assuming the screenshot takes up the viewport dimensions. Our resize
-    // handler will adjust as needed.
-    this.canvasWidth = window.innerWidth;
-    this.canvasHeight = window.innerHeight;
-
-    // This is the first time the screenshot has been rendered.
-    this.isScreenshotRendered = true;
-    if (isSidePanelOpen) {
-      // Reset the state of the selection overlay to represent the overlay being
-      // opened with the side panel open.
-      this.sidePanelOpened = true;
-      this.isResized = true;
-      this.isInitialSize = false;
-
-      // In the case of an overlay being shown with an already open side panel,
-      // the region context menu should not be shown. Disable text highlights
-      // as the text is not actionable anymore.
-      this.enableRegionContextMenu = false;
-      this.$.textLayer.disableHighlights();
-    }
-    this.onImageRendered();
-  }
-
-  private onOverlayReshown(screenshotBitmap: ImageBitmap) {
-    // Render the new screenshot.
-    renderScreenshot(this.$.backgroundImageCanvas, screenshotBitmap);
-
-    // Reset the state of the selection overlay to represent the overlay being
-    // opened with the side panel open.
-    this.isClosing = false;
-    this.sidePanelOpened = true;
-    this.hideBackgroundImageCanvas = true;
-    this.enableRegionContextMenu = false;
-
-    this.updateCanvasSize(window.innerWidth, window.innerHeight);
-
-    // Update our cached selection overlay rect to the new bounds.
-    this.updateSelectionOverlayRect();
-    this.resizeSelectionCanvases(
-        this.selectionOverlayRect.width, this.selectionOverlayRect.height);
-
-    // Allow the new screenshot to render / allow any resizing that needs to
-    // happen before finishing the reshow overlay flow. This needs an extra
-    // animation frame after the next render to ensure the new screenshot is
-    // painted at least once.
-    afterNextRender(this.$.backgroundImageCanvas, () => {
-      requestAnimationFrame(() => {
-        this.onFinishReshowOverlay();
-      });
-    });
-  }
-
-  private getOverlayBorderGlow(): OverlayBorderGlowElement {
-    if (this.overlayBorderGlow) {
-      return this.overlayBorderGlow;
-    }
-    this.overlayBorderGlow =
-        this.shadowRoot!.querySelector('overlay-border-glow')!;
-    return this.overlayBorderGlow;
-  }
-
   private onCopyCommand() {
     this.$.textLayer.onCopyDetectedText(
         this.detectedTextStartIndex, this.detectedTextEndIndex,
@@ -1451,25 +816,9 @@
     this.browserProxy.handler.closeRequestedByOverlayCloseButton();
   }
 
-  private onFinishReshowOverlay() {
-    this.hideBackgroundImageCanvas = false;
+  protected override onFinishReshowOverlay() {
     recordLensOverlaySelectionCloseButtonShown(INVOCATION_SOURCE);
-    this.dispatchEvent(new CustomEvent(
-        'on-finish-reshow-overlay', {bubbles: true, composed: true}));
-  }
-
-  /**
-   * Returns the bounding rect of the selection overlay. This is preferred over
-   * using getBoundingClientRect() because it is a cached DOM property which
-   * doesn't need to be recalculated every time.
-   */
-  getBoundingRect() {
-    return this.selectionOverlayRect;
-  }
-
-  fetchNewScreenshotForTesting() {
-    ScreenshotBitmapBrowserProxyImpl.getInstance().fetchScreenshot(
-        this.screenshotDataReceived.bind(this));
+    super.onFinishReshowOverlay();
   }
 
   getShowSelectedRegionContextMenuForTesting() {
@@ -1523,10 +872,6 @@
   setLanguagePickersOpenForTesting(open: boolean) {
     this.areLanguagePickersOpen = open;
   }
-
-  getHideBackgroundImageCanvasForTesting() {
-    return this.hideBackgroundImageCanvas;
-  }
 }
 
 declare global {
diff --git a/chrome/browser/resources/lens/overlay/selection_overlay_base_element.ts b/chrome/browser/resources/lens/overlay/selection_overlay_base_element.ts
new file mode 100644
index 0000000..e94f643
--- /dev/null
+++ b/chrome/browser/resources/lens/overlay/selection_overlay_base_element.ts
@@ -0,0 +1,841 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import './region_selection.js';
+import './post_selection_renderer.js';
+import './overlay_border_glow.js';
+import './overlay_shimmer_canvas.js';
+import '/strings.m.js';
+import '//resources/cr_elements/cr_button/cr_button.js';
+import '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
+import '//resources/cr_elements/cr_toast/cr_toast.js';
+
+import {I18nMixin} from '//resources/cr_elements/i18n_mixin.js';
+import {assert} from '//resources/js/assert.js';
+import {EventTracker} from '//resources/js/event_tracker.js';
+import {loadTimeData} from '//resources/js/load_time_data.js';
+import type {PolymerElementProperties} from '//resources/polymer/v3_0/polymer/interfaces.js';
+import {afterNextRender, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+
+import {getFallbackTheme} from './color_utils.js';
+import type {OverlayTheme} from './lens.mojom-webui.js';
+import type {OverlayBorderGlowElement} from './overlay_border_glow.js';
+import type {OverlayShimmerCanvasElement} from './overlay_shimmer_canvas.js';
+import type {PostSelectionBoundingBox, PostSelectionRendererElement} from './post_selection_renderer.js';
+import type {RegionSelectionElement} from './region_selection.js';
+import {ScreenshotBitmapBrowserProxyImpl} from './screenshot_bitmap_browser_proxy.js';
+import {renderScreenshot} from './screenshot_utils.js';
+import {SelectionOverlayBaseHandler} from './selection_overlay_base_handler.js';
+import {CursorType, DRAG_THRESHOLD, DragFeature, emptyGestureEvent, focusShimmerOnRegion, GestureState, ShimmerControlRequester} from './selection_utils.js';
+import type {GestureEvent, OverlayShimmerFocusedRegion} from './selection_utils.js';
+
+// The amountf of margins in pixels to add to the screenshot when the window is
+// resized.
+const SCREENSHOT_FULLSIZE_MARGIN_PIXEL = 24;
+
+// The number of pixels the screenshot can differ from the viewport before
+// adding margins.
+const SCREENSHOT_RESIZE_TOLERANCE_PIXELS = 2;
+
+// The size of our custom cursor.
+export const CURSOR_SIZE_PIXEL = 32;
+
+// The cursor image url css variable name.
+export const CURSOR_IMG_URL = '--cursor-img-url';
+
+export interface CursorData {
+  cursor: CursorType;
+}
+
+const SelectionOverlayElementBase = I18nMixin(PolymerElement);
+
+/*
+ * Element responsible for coordinating selections between the various selection
+ * features. This includes:
+ *   - Storing state needed to coordinate selections across features
+ *   - Listening to mouse/tap events and delegating them to the correct features
+ *   - Coordinating animations between the different features
+ */
+export abstract class SelectionOverlayBaseElement extends
+    SelectionOverlayElementBase {
+  protected abstract backgroundImageCanvas(): HTMLCanvasElement;
+  protected abstract cursor(): HTMLElement;
+  protected abstract initialFlashScrim(): HTMLElement;
+  protected abstract overlayShimmerCanvas(): OverlayShimmerCanvasElement;
+  protected abstract postSelectionRenderer(): PostSelectionRendererElement;
+  protected abstract regionSelectionLayer(): RegionSelectionElement;
+  protected abstract selectionOverlay(): HTMLElement;
+
+  static get properties(): PolymerElementProperties {
+    return {
+      isScreenshotRendered: {
+        type: Boolean,
+        reflectToAttribute: true,
+        value: false,
+      },
+      isResized: {
+        type: Boolean,
+        reflectToAttribute: true,
+        value: false,
+        observer: 'onIsResizedChanged',
+      },
+      isInitialSize: {
+        type: Boolean,
+        reflectToAttribute: true,
+        value: true,
+      },
+      canvasHeight: Number,
+      canvasWidth: Number,
+      isPointerInside: {
+        type: Boolean,
+        value: false,
+      },
+      currentGesture: {
+        type: Object,
+        value: () => emptyGestureEvent(),
+      },
+      disableShimmer: {
+        type: Boolean,
+        readOnly: true,
+        value: !loadTimeData.getBoolean('enableShimmer'),
+      },
+      enableBorderGlow: {
+        type: Boolean,
+        value: () => loadTimeData.getBoolean('enableBorderGlow'),
+      },
+      isClosing: {
+        type: Boolean,
+        reflectToAttribute: true,
+        value: false,
+      },
+      shimmerOnSegmentation: {
+        type: Boolean,
+        reflectToAttribute: true,
+        value: false,
+      },
+      shimmerFadeOutComplete: {
+        type: Boolean,
+        reflectToAttribute: true,
+        value: true,
+      },
+      darkenExtraScrim: {
+        type: Boolean,
+        reflectToAttribute: true,
+        value: false,
+      },
+      theme: {
+        type: Object,
+        value: getFallbackTheme,
+      },
+      selectionOverlayRect: Object,
+      hideBackgroundImageCanvas: {
+        type: Boolean,
+        reflectToAttribute: true,
+        value: false,
+      },
+      sidePanelOpened: {
+        type: Boolean,
+        reflectToAttribute: true,
+        value: false,
+      },
+      enableRegionContextMenu: {
+        type: Boolean,
+        value: true,
+        reflectToAttribute: true,
+      },
+    };
+  }
+
+  // Whether the screenshot has finished loading in.
+  declare private isScreenshotRendered: boolean;
+  // Whether the selection overlay is its initial size, or has changed size.
+  declare private isResized: boolean;
+  declare private isInitialSize: boolean;
+  // Width and height values for rendering the background image canvas as the
+  // proper dimensions.
+  declare protected canvasHeight: number;
+  declare protected canvasWidth: number;
+  // The current content rectangle of the selection elements DIV. This is the
+  // bounds of the screenshot and the part the user interacts with. This should
+  // be used instead of call getBoundingClientRect().
+  declare private selectionOverlayRect: DOMRect;
+
+  // The current gesture event. The coordinate values are only accurate if a
+  // gesture has started.
+  declare protected currentGesture: GestureEvent;
+  declare private disableShimmer: boolean;
+  declare private enableBorderGlow: boolean;
+  // Whether the overlay is being shut down.
+  declare private isClosing: boolean;
+  // Whether the default background scrim is currently being darkened.
+  declare private darkenExtraScrim: boolean;
+  // Whether the shimmer is currently focused on a segmentation mask.
+  declare private shimmerOnSegmentation: boolean;
+  declare private shimmerFadeOutComplete: boolean;
+  // Whether the side panel is currently opened.
+  declare protected sidePanelOpened: boolean;
+  // Whether the background image canvas should currently be shown.
+  declare private hideBackgroundImageCanvas: boolean;
+  // Whether the region context menu is enabled.
+  declare protected enableRegionContextMenu: boolean;
+
+  declare protected isPointerInside: boolean;
+
+  // The border glow layer rendered on the selection overlay if it exists.
+  private overlayBorderGlow: OverlayBorderGlowElement;
+
+  protected eventTracker_: EventTracker = new EventTracker();
+  // Listener ids for events from the browser side.
+  protected listenerIds: number[];
+  // The feature currently being dragged. Once a feature responds to a drag
+  // event, no other feature will receive gesture events.
+  protected draggingRespondent = DragFeature.NONE;
+  private resizeObserver: ResizeObserver =
+      new ResizeObserver(this.handleResize.bind(this));
+  // Used to listen for changes in the window.devicePixelRatio. Stored as a
+  // variable so we can easily add and remove the listener.
+  private matchMedia?: MediaQueryList;
+  protected cursorOffsetX: number = 3;
+  protected cursorOffsetY: number = 6;
+  private hasInitialFlashAnimationEnded = false;
+  private baseHandler: SelectionOverlayBaseHandler =
+      SelectionOverlayBaseHandler.getInstance();
+
+  // The ID returned by requestAnimationFrame for the updateCursorPosition,
+  // onPointerMove, and handleResize functions.
+  private updateCursorPositionRequestId?: number;
+  private onPointerMoveRequestId?: number;
+  private handleResizeRequestId?: number;
+
+  declare private theme: OverlayTheme;
+
+  override connectedCallback() {
+    super.connectedCallback();
+    this.resizeObserver.observe(this);
+    this.listenerIds = [
+      this.baseHandler.addNotifyOverlayClosingListener(() => {
+        this.isClosing = true;
+        this.removeDragListeners();
+      }),
+    ];
+    ScreenshotBitmapBrowserProxyImpl.getInstance().fetchScreenshot(
+        this.screenshotDataReceived.bind(this));
+    ScreenshotBitmapBrowserProxyImpl.getInstance().addOnOverlayReshownListener(
+        this.onOverlayReshown.bind(this));
+    this.eventTracker_.add(
+        document, 'shimmer-fade-out-complete', (e: CustomEvent<boolean>) => {
+          this.shimmerFadeOutComplete = e.detail;
+        });
+    this.eventTracker_.add(
+        document, 'set-cursor', (e: CustomEvent<CursorData>) => {
+          if (e.detail.cursor === CursorType.POINTER) {
+            this.setCursorToPointer();
+          } else if (e.detail.cursor === CursorType.CROSSHAIR) {
+            this.setCursorToCrosshair();
+          } else if (e.detail.cursor === CursorType.TEXT) {
+            this.setCursorToText();
+          } else {
+            this.resetCursor();
+          }
+        });
+    this.eventTracker_.add(document, 'darken-extra-scrim-opacity', () => {
+      this.darkenExtraScrim = true;
+    });
+    this.eventTracker_.add(document, 'lighten-extra-scrim-opacity', () => {
+      this.darkenExtraScrim = false;
+    });
+    this.eventTracker_.add(
+        this.initialFlashScrim(), 'animationend', (event: AnimationEvent) => {
+          // The flash animation is the longest animation.
+          if (event.animationName !== 'initial-inset-animation') {
+            return;
+          }
+          this.onInitialFlashAnimationEnd();
+        });
+    this.eventTracker_.add(
+        document, 'focus-region',
+        (e: CustomEvent<OverlayShimmerFocusedRegion>) => {
+          if (e.detail.requester === ShimmerControlRequester.SEGMENTATION) {
+            this.shimmerOnSegmentation = true;
+          }
+        });
+    this.eventTracker_.add(document, 'unfocus-region', () => {
+      this.shimmerOnSegmentation = false;
+    });
+    if (this.enableBorderGlow) {
+      this.eventTracker_.add(
+          document, 'post-selection-updated',
+          (e: CustomEvent<PostSelectionBoundingBox>) => {
+            this.handlePostSelectionUpdated(e.detail.height, e.detail.width);
+          });
+    }
+
+    this.updateSelectionOverlayRect();
+    this.updateDevicePixelRatioListener();
+  }
+
+  override disconnectedCallback() {
+    super.disconnectedCallback();
+    this.resizeObserver.unobserve(this);
+    this.eventTracker_.removeAll();
+    this.listenerIds.forEach(id => assert(this.baseHandler.removeListener(id)));
+    this.listenerIds = [];
+
+    assert(this.matchMedia);
+    this.matchMedia.removeEventListener(
+        'change', this.onDevicePixelRatioChanged.bind(this));
+  }
+
+  override ready() {
+    super.ready();
+    this.addEventListener('pointerdown', this.onPointerDown.bind(this));
+    this.addEventListener('pointermove', this.updateCursorPosition.bind(this));
+  }
+
+  private addDragListeners() {
+    this.addEventListener('pointerup', this.onPointerUp);
+    this.addEventListener('pointermove', this.onPointerMove);
+    this.addEventListener('pointercancel', this.onPointerCancel);
+  }
+
+  private removeDragListeners() {
+    this.removeEventListener('pointerup', this.onPointerUp);
+    this.removeEventListener('pointermove', this.onPointerMove);
+    this.removeEventListener('pointercancel', this.onPointerCancel);
+  }
+
+  private updateDevicePixelRatioListener() {
+    // Remove the previous listener since we are now listening for a different
+    // pixel ratio change.
+    if (this.matchMedia) {
+      this.eventTracker_.remove(this.matchMedia, 'change');
+    }
+
+    // Listen to changes to the current device pixel ratio.
+    const queryString = `(resolution: ${window.devicePixelRatio}dppx)`;
+    this.matchMedia = matchMedia(queryString);
+    this.eventTracker_.add(
+        this.matchMedia, 'change', this.onDevicePixelRatioChanged.bind(this));
+  }
+
+  private onDevicePixelRatioChanged() {
+    // Update the listener to the new pixel ratio.
+    this.updateDevicePixelRatioListener();
+    // Resize the canvases to take the new pixel ratio change.\
+    this.resizeSelectionCanvases(
+        this.selectionOverlayRect.width, this.selectionOverlayRect.height);
+  }
+
+  private updateCursorPosition(event: PointerEvent) {
+    // Cancel a pending event to prevent multiple updates per frame.
+    if (this.updateCursorPositionRequestId) {
+      cancelAnimationFrame(this.updateCursorPositionRequestId);
+    }
+
+    // Use requestAnimationFrame to only update the cursor once a frame instead
+    // of multiple times per frame. This helps ensure the cursor is being
+    // updated to the latest received pointer event.
+    this.updateCursorPositionRequestId = requestAnimationFrame(() => {
+      const mouseX = event.clientX;
+      const mouseY = event.clientY;
+
+      const cursorOffsetX = mouseX + this.cursorOffsetX;
+      const cursorOffsetY = mouseY + this.cursorOffsetY;
+
+      if (!this.disableShimmer &&
+          (this.isPointerInside ||
+           this.currentGesture.state === GestureState.DRAGGING)) {
+        this.updateShimmerForCursor(cursorOffsetX, cursorOffsetY);
+      }
+
+      this.cursor().style.transform =
+          `translate3d(${cursorOffsetX}px, ${cursorOffsetY}px, 0)`;
+      this.updateCursorPositionRequestId = undefined;
+    });
+  }
+
+  private updateShimmerForCursor(cursorLeft: number, cursorTop: number) {
+    const relativeXPercent =
+        Math.max(
+            0,
+            Math.min(cursorLeft, this.selectionOverlayRect.right) -
+                this.selectionOverlayRect.left) /
+        this.selectionOverlayRect.width;
+    const relativeYPercent =
+        Math.max(
+            0,
+            Math.min(cursorTop, this.selectionOverlayRect.bottom) -
+                this.selectionOverlayRect.top) /
+        this.selectionOverlayRect.height;
+
+    focusShimmerOnRegion(
+        this, relativeYPercent, relativeXPercent,
+        CURSOR_SIZE_PIXEL / this.selectionOverlayRect.width,
+        CURSOR_SIZE_PIXEL / this.selectionOverlayRect.height,
+        ShimmerControlRequester.CURSOR);
+  }
+
+  protected onIsResizedChanged(newValue: boolean): void {
+    this.baseHandler.setLiveBlur(newValue);
+  }
+
+  private getHiddenCursorClass(isPointerInside: boolean, state: GestureState):
+      string {
+    // Always show when dragging, even if outside the selection overlay.
+    if (!isPointerInside && state !== GestureState.DRAGGING) {
+      return 'hidden';
+    } else {
+      return '';
+    }
+  }
+
+  // LINT.IfChange(CursorOffsetValues)
+  // Called on text hover and drag.
+  protected setCursorToText() {
+    // Set body cursor style to handle dragging.
+    document.body.style.cursor = 'text';
+    this.cursorOffsetX = 3;
+    this.cursorOffsetY = 8;
+    this.style.setProperty(CURSOR_IMG_URL, 'url("text.svg")');
+  }
+
+  // Called on region selection drag.
+  protected setCursorToCrosshair() {
+    // Set body cursor style to handle dragging.
+    document.body.style.cursor = 'crosshair';
+    this.cursorOffsetX = 3;
+    this.cursorOffsetY = 6;
+    this.style.setProperty(CURSOR_IMG_URL, 'url("lens.svg")');
+  }
+
+  // Called on object hover.
+  protected setCursorToPointer() {
+    // No dragging for objects, so no need to set body cursor style.
+    this.cursorOffsetX = 11;
+    this.cursorOffsetY = 17;
+    this.style.setProperty(CURSOR_IMG_URL, 'url("lens.svg")');
+  }
+
+  protected resetCursor() {
+    document.body.style.cursor = 'unset';
+    this.cursorOffsetX = 3;
+    this.cursorOffsetY = 6;
+    this.style.setProperty(CURSOR_IMG_URL, 'url("lens.svg")');
+  }
+  // LINT.ThenChange(//chrome/browser/resources/lens/overlay/cursor_tooltip.ts:CursorOffsetValues)
+
+
+  private onImageRendered() {
+    // Let the parent know it is safe to blur the background.
+    this.dispatchEvent(new CustomEvent('screenshot-rendered', {
+      bubbles: true,
+      composed: true,
+      detail: {isSidePanelOpen: this.sidePanelOpened},
+    }));
+    this.baseHandler.notifyOverlayInitialized();
+  }
+
+  protected handlePointerEnter() {
+    this.isPointerInside = true;
+  }
+
+  private handlePointerLeave() {
+    this.isPointerInside = false;
+  }
+
+  private onPointerDown(event: PointerEvent) {
+    if (this.shouldIgnoreEvent(event)) {
+      return;
+    }
+
+    if (event.button === 2 /* right button */) {
+      this.handleRightClick(event);
+      return;
+    }
+
+    this.addDragListeners();
+
+    this.currentGesture = {
+      state: GestureState.NOT_STARTED,
+      startX: event.clientX,
+      startY: event.clientY,
+      clientX: event.clientX,
+      clientY: event.clientY,
+    };
+
+    this.pointerDownHandled();
+  }
+
+  protected pointerDownHandled() {}
+
+  protected handleRightClick(event: PointerEvent) {
+    this.postSelectionRenderer().handleRightClick(event);
+  }
+
+  private onPointerUp(event: PointerEvent) {
+    this.updateGestureCoordinates(event);
+
+    if (this.currentGesture.state === GestureState.DRAGGING) {
+      // Cancel the animation frame and handle the drag event immediately so
+      // handleGestureEnd is not in an unexpected state.
+      this.cancelPendingDragAnimationFrame();
+      this.handleGestureDrag(event);
+    }
+
+    // Allow the clients to respond to the gesture, IFF a gesture has started.
+    if (this.currentGesture.state !== GestureState.NOT_STARTED) {
+      this.handleGestureEnd();
+    }
+
+    // After features have responded to the event, reset the current drag state.
+    this.currentGesture = emptyGestureEvent();
+    this.draggingRespondent = DragFeature.NONE;
+    this.removeDragListeners();
+  }
+
+  private onPointerMove(event: PointerEvent) {
+    this.updateGestureCoordinates(event);
+
+    // Ignore the event if the user isn't explicitly dragging yet.
+    if (!this.isDragging()) {
+      return;
+    }
+
+    if (this.currentGesture.state === GestureState.NOT_STARTED) {
+      // If a gesture hasn't started, start the gesture now that the user is
+      // dragging.
+      this.handleGestureStart();
+    }
+
+    if (this.currentGesture.state === GestureState.STARTING) {
+      // If the gesture just started, move into the dragging state.
+      this.set('currentGesture.state', GestureState.DRAGGING);
+    }
+
+    // If we haven't exited early, we must be in the dragging state.
+    assert(this.currentGesture.state === GestureState.DRAGGING);
+
+    // Handle the drag.
+    this.cancelPendingDragAnimationFrame();
+    this.onPointerMoveRequestId = requestAnimationFrame(() => {
+      this.handleGestureDrag(event);
+      this.onPointerMoveRequestId = undefined;
+    });
+  }
+
+  private cancelPendingDragAnimationFrame() {
+    if (this.onPointerMoveRequestId) {
+      cancelAnimationFrame(this.onPointerMoveRequestId);
+    }
+  }
+
+  private onPointerCancel() {
+    // Pointer cancelled, so cancel any pending gestures.
+    this.handleGestureCancel();
+
+    this.currentGesture = emptyGestureEvent();
+    this.draggingRespondent = DragFeature.NONE;
+    this.removeDragListeners();
+    this.resetCursor();
+  }
+
+  protected handleGestureStart() {
+    this.set('currentGesture.state', GestureState.STARTING);
+
+    // Send events to hide UI.
+    this.baseHandler.closePreselectionBubble();
+    this.dispatchEvent(
+        new CustomEvent('selection-started', {bubbles: true, composed: true}));
+
+    if (this.enableBorderGlow) {
+      this.getOverlayBorderGlow().handleGestureStart();
+
+      // If there is no post selection, fade the scrim from the region selection
+      // back in.
+      if (!this.postSelectionRenderer().hasSelection()) {
+        // TODO(crbug.com/421002691): follow the convention where the layer
+        // should return true if its handling the gesture, and
+        // draggingRespondent should be updated. Currently used to trigger the
+        // fade in of the darkened scrim.
+        this.regionSelectionLayer().handleGestureStart();
+      }
+    }
+  }
+
+  protected handleGestureDrag(event: PointerEvent) {
+    assert(this.currentGesture.state === GestureState.DRAGGING);
+    // Capture pointer events so gestures still work if the users pointer
+    // leaves the selection overlay div. Pointer capture is implicitly
+    // released after pointerup or pointercancel events.
+    this.setPointerCapture(event.pointerId);
+  }
+
+  protected handleGestureEnd() {}
+
+  protected handleGestureCancel() {
+    this.regionSelectionLayer().cancelGesture();
+    this.postSelectionRenderer().cancelGesture();
+  }
+
+  private handlePostSelectionUpdated(height: number, width: number) {
+    const overlayBorderGlow = this.getOverlayBorderGlow();
+    // If there is no selection happening, fade the glow back in.
+    if (width === 0 && height === 0 &&
+        this.draggingRespondent === DragFeature.NONE) {
+      overlayBorderGlow.handleClearSelection();
+      this.regionSelectionLayer().handlePostSelectionCleared();
+      return;
+    }
+
+    overlayBorderGlow.handlePostSelectionUpdated();
+  }
+
+  private updateCanvasSize(containerWidth: number, containerHeight: number) {
+    // Set our own canvas size while preserving the canvas aspect ratio.
+    const screenshotHeight = this.backgroundImageCanvas().height;
+    const screenshotWidth = this.backgroundImageCanvas().width;
+
+    const doesScreenshotFillContainer =
+        Math.abs(
+            containerWidth - (screenshotWidth / window.devicePixelRatio)) <=
+            SCREENSHOT_RESIZE_TOLERANCE_PIXELS &&
+        Math.abs(
+            containerHeight - (screenshotHeight / window.devicePixelRatio)) <=
+            SCREENSHOT_RESIZE_TOLERANCE_PIXELS;
+    const shouldApplyMargins =
+        !doesScreenshotFillContainer || this.sidePanelOpened;
+
+    // Apply margins if the page is resized / side panel is opened and not
+    // closing.
+    const margins = shouldApplyMargins && !this.isClosing ?
+        SCREENSHOT_FULLSIZE_MARGIN_PIXEL * 2 :
+        0;
+    const newContainerWidth = containerWidth - margins;
+    const newContainerHeight = containerHeight - margins;
+
+    // Get the aspect ratio to force the image to conform to.
+    const aspectRatio = this.backgroundImageCanvas().width /
+        this.backgroundImageCanvas().height;
+
+    // Calculate potential dimensions based on width and height
+    const widthBasedHeight = Math.round(newContainerWidth / aspectRatio);
+    const heightBasedWidth = Math.round(newContainerHeight * aspectRatio);
+
+    // Choose dimensions that fit within the container while preserving aspect
+    // ratio
+    if (widthBasedHeight <= newContainerHeight) {
+      // Width-based dimensions fit
+      this.canvasHeight = widthBasedHeight;
+      this.canvasWidth = newContainerWidth;
+    } else {
+      // Height-based dimensions fit
+      this.canvasWidth = heightBasedWidth;
+      this.canvasHeight = newContainerHeight;
+    }
+
+    this.isResized = shouldApplyMargins;
+    if (this.isResized) {
+      this.isInitialSize = false;
+      // The flash animation is cut short but animationend is never called if
+      // the overlay is resized before animationend is called. This is because
+      // the flash scrim is hidden on resize.
+      this.onInitialFlashAnimationEnd();
+    }
+  }
+
+  private handleResize(entries: ResizeObserverEntry[]) {
+    // Cancel a pending event to prevent multiple updates per frame.
+    if (this.handleResizeRequestId) {
+      cancelAnimationFrame(this.handleResizeRequestId);
+    }
+
+    // Use requestAnimationFrame to only calculate the screenshot size once
+    // a frame instead of multiple times per frame.
+    this.handleResizeRequestId = requestAnimationFrame(() => {
+      assert(entries.length === 1);
+      const newRect = entries[0].contentRect;
+
+      // If the screenshot is not rendered yet, there is nothing to do yet.
+      if (!this.isScreenshotRendered ||
+          (newRect.width === 0 && newRect.height === 0)) {
+        this.handleResizeRequestId = undefined;
+        return;
+      }
+
+      this.updateCanvasSize(newRect.width, newRect.height);
+
+      // Update our cached selection overlay rect to the new bounds.
+      this.updateSelectionOverlayRect();
+
+      // TODO(b/361798599): Since we now pass selectionOverlayRect, we can use
+      // polymer events to allow each client to resize their canvas once
+      // selectionOverlayRect changes. We should remove this and do the
+      // resizing via polymer techniques.
+      this.resizeSelectionCanvases(
+          this.selectionOverlayRect.width, this.selectionOverlayRect.height);
+
+      this.handleResizeRequestId = undefined;
+    });
+  }
+
+  private updateSelectionOverlayRect(): void {
+    // We use offsetXXX instead of call this.getBoundingClientRect() because
+    // offsetXXX is a cached DOM property, while this.getBoundingClientRect()
+    // recalculates the layout every time it is called. Since we have no
+    // scrolling, these calls should be equivalent.
+    this.selectionOverlayRect = new DOMRect(
+        this.selectionOverlay().offsetLeft, this.selectionOverlay().offsetTop,
+        this.selectionOverlay().offsetWidth,
+        this.selectionOverlay().offsetHeight);
+  }
+
+  protected resizeSelectionCanvases(newWidth: number, newHeight: number) {
+    this.regionSelectionLayer().setCanvasSizeTo(newWidth, newHeight);
+    this.postSelectionRenderer().setCanvasSizeTo(newWidth, newHeight);
+    this.overlayShimmerCanvas().setCanvasSizeTo(newWidth, newHeight);
+  }
+
+  // Updates the currentGesture to correspond with the given PointerEvent.
+  private updateGestureCoordinates(event: PointerEvent) {
+    this.currentGesture.clientX = event.clientX;
+    this.currentGesture.clientY = event.clientY;
+  }
+
+  // Returns if the given PointerEvent should be ignored.
+  protected shouldIgnoreEvent(event: PointerEvent) {
+    if (this.isClosing) {
+      return true;
+    }
+    // Ignore multi touch events and non-left/right click events.
+    return !event.isPrimary || (event.button !== 0 && event.button !== 2);
+  }
+
+  // Returns whether the current gesture event is a drag.
+  private isDragging() {
+    if (this.currentGesture.state === GestureState.DRAGGING) {
+      return true;
+    }
+
+    // TODO(b/329514345): Revisit if pointer movement is enough of an indicator,
+    // or if we also need a timelimit on how long a tap can last before starting
+    // a drag.
+    const xMovement =
+        Math.abs(this.currentGesture.clientX - this.currentGesture.startX);
+    const yMovement =
+        Math.abs(this.currentGesture.clientY - this.currentGesture.startY);
+    return xMovement > DRAG_THRESHOLD || yMovement > DRAG_THRESHOLD;
+  }
+
+  private onInitialFlashAnimationEnd() {
+    if (this.hasInitialFlashAnimationEnded) {
+      return;
+    }
+    this.hasInitialFlashAnimationEnded = true;
+    this.eventTracker_.remove(this.initialFlashScrim(), 'animationend');
+
+    this.baseHandler.addBackgroundBlur();
+
+    // Let the parent know the initial flash image animation has finished.
+    this.dispatchEvent(new CustomEvent(
+        'initial-flash-animation-end', {bubbles: true, composed: true}));
+
+    // Don't start the shimmer animation until the initial flash animation is
+    // finished.
+    if (!this.disableShimmer && !this.enableBorderGlow) {
+      this.overlayShimmerCanvas().startAnimation();
+    }
+  }
+
+  private screenshotDataReceived(
+      screenshotBitmap: ImageBitmap, isSidePanelOpen: boolean) {
+    renderScreenshot(this.backgroundImageCanvas(), screenshotBitmap);
+    // Start the canvas as the same dimensions as the viewport, since we are
+    // assuming the screenshot takes up the viewport dimensions. Our resize
+    // handler will adjust as needed.
+    this.canvasWidth = window.innerWidth;
+    this.canvasHeight = window.innerHeight;
+
+    // This is the first time the screenshot has been rendered.
+    this.isScreenshotRendered = true;
+    if (isSidePanelOpen) {
+      this.setSidePanelOpened();
+    }
+    this.onImageRendered();
+  }
+
+  protected setSidePanelOpened() {
+    // Reset the state of the selection overlay to represent the overlay being
+    // opened with the side panel open.
+    this.sidePanelOpened = true;
+    this.isResized = true;
+    this.isInitialSize = false;
+
+    // In the case of an overlay being shown with an already open side panel,
+    // the region context menu should not be shown. Disable text highlights
+    // as the text is not actionable anymore.
+    this.enableRegionContextMenu = false;
+  }
+
+  private onOverlayReshown(screenshotBitmap: ImageBitmap) {
+    // Render the new screenshot.
+    renderScreenshot(this.backgroundImageCanvas(), screenshotBitmap);
+
+    // Reset the state of the selection overlay to represent the overlay being
+    // opened with the side panel open.
+    this.isClosing = false;
+    this.sidePanelOpened = true;
+    this.hideBackgroundImageCanvas = true;
+    this.enableRegionContextMenu = false;
+
+    this.updateCanvasSize(window.innerWidth, window.innerHeight);
+
+    // Update our cached selection overlay rect to the new bounds.
+    this.updateSelectionOverlayRect();
+    this.resizeSelectionCanvases(
+        this.selectionOverlayRect.width, this.selectionOverlayRect.height);
+
+    // Allow the new screenshot to render / allow any resizing that needs to
+    // happen before finishing the reshow overlay flow. This needs an extra
+    // animation frame after the next render to ensure the new screenshot is
+    // painted at least once.
+    afterNextRender(this.backgroundImageCanvas(), () => {
+      requestAnimationFrame(() => {
+        this.onFinishReshowOverlay();
+      });
+    });
+  }
+
+  private getOverlayBorderGlow(): OverlayBorderGlowElement {
+    if (this.overlayBorderGlow) {
+      return this.overlayBorderGlow;
+    }
+    this.overlayBorderGlow =
+        this.shadowRoot!.querySelector('overlay-border-glow')!;
+    return this.overlayBorderGlow;
+  }
+
+  protected onFinishReshowOverlay() {
+    this.hideBackgroundImageCanvas = false;
+    this.dispatchEvent(new CustomEvent(
+        'on-finish-reshow-overlay', {bubbles: true, composed: true}));
+  }
+
+  /**
+   * Returns the bounding rect of the selection overlay. This is preferred over
+   * using getBoundingClientRect() because it is a cached DOM property which
+   * doesn't need to be recalculated every time.
+   */
+  getBoundingRect() {
+    return this.selectionOverlayRect;
+  }
+
+  fetchNewScreenshotForTesting() {
+    ScreenshotBitmapBrowserProxyImpl.getInstance().fetchScreenshot(
+        this.screenshotDataReceived.bind(this));
+  }
+
+  getHideBackgroundImageCanvasForTesting() {
+    return this.hideBackgroundImageCanvas;
+  }
+}
diff --git a/chrome/browser/resources/lens/overlay/selection_overlay_base_handler.ts b/chrome/browser/resources/lens/overlay/selection_overlay_base_handler.ts
new file mode 100644
index 0000000..45b96af
--- /dev/null
+++ b/chrome/browser/resources/lens/overlay/selection_overlay_base_handler.ts
@@ -0,0 +1,56 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import type {BitmapMappedFromTrustedProcess} from '//resources/mojo/skia/public/mojom/bitmap.mojom-webui.js';
+import type {RectF} from '//resources/mojo/ui/gfx/geometry/mojom/geometry.mojom-webui.js';
+
+export enum RegionSource {
+  CLICK,
+  SELECTION,
+  KEYBOARD,
+  SELECTION_CHANGE,
+}
+
+/*
+ * Interface definition of the core functionality that the common selection
+ * controller requires from the embedding code.
+ */
+export interface SelectionOverlayBaseHandler {
+  addBackgroundBlur(): void;
+  addOnOverlayReshownListener(
+      callback: (screenshotData: BitmapMappedFromTrustedProcess) => void):
+      number;
+  addNotifyOverlayClosingListener(callback: () => void): number;
+  addScreenshotDataReceivedListener(
+      callback:
+          (screenshotData: BitmapMappedFromTrustedProcess,
+           isSidePanelOpen: boolean) => void,
+      ): number;
+  addClearRegionSelectionListener(callback: () => void): number;
+  addClearAllSelectionsListener(callback: () => void): number;
+  addNotifyResultsPanelOpenedListener(callback: () => void): number;
+  addSetPostRegionSelectionListener(callback: (region: RectF) => void): number;
+  adjustRegionSelected(rect: RectF, source: RegionSource): void;
+  closePreselectionBubble(): void;
+  notifyOverlayInitialized(): void;
+  removeListener(id: number): boolean;
+  setLiveBlur(enabled: boolean): void;
+}
+
+/*
+ * This class has a static member pointing to the implementation of the
+ * installed SelectionOverlayBaseHandler. setInstance should be called in each
+ * embedder.
+ */
+export class SelectionOverlayBaseHandler {
+  private static instance: SelectionOverlayBaseHandler|null = null;
+
+  static getInstance(): SelectionOverlayBaseHandler {
+    return SelectionOverlayBaseHandler.instance!;
+  }
+
+  static setInstance(obj: SelectionOverlayBaseHandler) {
+    SelectionOverlayBaseHandler.instance = obj;
+  }
+}
diff --git a/chrome/browser/resources/lens/overlay/text_layer.ts b/chrome/browser/resources/lens/overlay/text_layer.ts
index 6a65e13..75dda7b 100644
--- a/chrome/browser/resources/lens/overlay/text_layer.ts
+++ b/chrome/browser/resources/lens/overlay/text_layer.ts
@@ -22,7 +22,8 @@
 import {SemanticEvent, UserAction} from './lens.mojom-webui.js';
 import {INVOCATION_SOURCE} from './lens_overlay_app.js';
 import {recordLensOverlayInteraction, recordLensOverlaySemanticEvent} from './metrics_utils.js';
-import type {CursorData, SelectedRegionContextMenuData, SelectedTextContextMenuData} from './selection_overlay.js';
+import type {SelectedRegionContextMenuData, SelectedTextContextMenuData} from './selection_overlay.js';
+import type {CursorData} from './selection_overlay_base_element.js';
 import {CursorType} from './selection_utils.js';
 import type {GestureEvent} from './selection_utils.js';
 import type {BackgroundImageData, Line, Paragraph, Text, TranslatedLine, TranslatedParagraph, Word} from './text.mojom-webui.js';
diff --git a/chrome/browser/resources/updater/scope_icon.html.ts b/chrome/browser/resources/updater/scope_icon.html.ts
index 56e502b..1558556 100644
--- a/chrome/browser/resources/updater/scope_icon.html.ts
+++ b/chrome/browser/resources/updater/scope_icon.html.ts
@@ -11,7 +11,7 @@
   return html`
 <!--_html_template_start_-->
 ${this.scope !== undefined ? html`
-  <cr-icon icon="${this.icon}" title="${this.label}">
+  <cr-icon icon="${this.icon}" title="${this.label}" role="img">
   </cr-icon>
 ` : ''}
 <!--_html_template_end_-->`;
diff --git a/chrome/browser/sync/sessions/browser_list_router_helper_unittest.cc b/chrome/browser/sync/sessions/browser_list_router_helper_unittest.cc
index b978d5e9..032b8a2 100644
--- a/chrome/browser/sync/sessions/browser_list_router_helper_unittest.cc
+++ b/chrome/browser/sync/sessions/browser_list_router_helper_unittest.cc
@@ -17,6 +17,7 @@
 #include "chrome/test/base/browser_with_test_window_test.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chrome/test/base/testing_profile_manager.h"
+#include "chrome/test/base/ui_test_utils.h"
 #include "components/sync_sessions/synced_tab_delegate.h"
 
 namespace sync_sessions {
@@ -124,7 +125,7 @@
   AddTab(browser(), gurl_1);
 
   // Tab needs to have been active to be found when discarding.
-  BrowserList::GetInstance()->SetLastActive(browser());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser());
 
   EXPECT_EQ(gurl_1, *handler_1.seen_urls()->rbegin());
   SessionID old_id = *handler_1.seen_ids()->rbegin();
diff --git a/chrome/browser/sync/test/integration/web_apps/two_client_web_apps_sync_test.cc b/chrome/browser/sync/test/integration/web_apps/two_client_web_apps_sync_test.cc
index aeac726a..35eef99 100644
--- a/chrome/browser/sync/test/integration/web_apps/two_client_web_apps_sync_test.cc
+++ b/chrome/browser/sync/test/integration/web_apps/two_client_web_apps_sync_test.cc
@@ -171,9 +171,9 @@
   info->scope = GURL("http://www.chromium.org/");
   info->user_display_mode = mojom::UserDisplayMode::kStandalone;
 
-  web_app::proto::WebAppMigrationSource source;
-  source.set_manifest_id("http://migration.chromium.org/start.html");
-  info->migration_sources.push_back(std::move(source));
+  info->migration_sources.emplace_back(
+      webapps::ManifestId(GURL("http://migration.chromium.org/start.html")),
+      MigrationBehavior::kSuggest);
 
   // Install app on first profile, mark it suggested for migration.
   base::test::TestFuture<const webapps::AppId&, webapps::InstallResultCode>
diff --git a/chrome/browser/tab/BUILD.gn b/chrome/browser/tab/BUILD.gn
index 6dc8ef4..d97a2b9 100644
--- a/chrome/browser/tab/BUILD.gn
+++ b/chrome/browser/tab/BUILD.gn
@@ -218,6 +218,7 @@
     "collection_save_forwarder.h",
     "collection_storage_observer.h",
     "collection_storage_package.h",
+    "media_state.h",
     "payload.h",
     "payload_util.h",
     "restore_entity_tracker.h",
@@ -308,5 +309,8 @@
 }
 
 java_cpp_enum("enums_java") {
-  sources = [ "tab_storage_type.h" ]
+  sources = [
+    "media_state.h",
+    "tab_storage_type.h",
+  ]
 }
diff --git a/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/EmptyTabObserver.java b/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/EmptyTabObserver.java
index ec9f11c..88590ca3 100644
--- a/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/EmptyTabObserver.java
+++ b/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/EmptyTabObserver.java
@@ -12,7 +12,6 @@
 import org.chromium.cc.input.BrowserControlsState;
 import org.chromium.chrome.browser.browser_controls.BrowserControlsOffsetTagsInfo;
 import org.chromium.chrome.browser.tab.Tab.LoadUrlResult;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
 import org.chromium.components.find_in_page.FindMatchRectsDetails;
 import org.chromium.components.find_in_page.FindNotificationDetails;
 import org.chromium.content_public.browser.LoadUrlParams;
diff --git a/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/Tab.java b/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/Tab.java
index 37bcecd..d46bf27 100644
--- a/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/Tab.java
+++ b/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/Tab.java
@@ -27,10 +27,8 @@
 import org.chromium.ui.base.WindowAndroid;
 import org.chromium.url.GURL;
 
-import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
 
 /**
  * Tab is a visual/functional unit that encapsulates the content (not just web site content from
@@ -49,31 +47,6 @@
         int DEFAULT_PAGE_LOAD = 1;
     }
 
-    /** Tracks the media indicator state of the tab. */
-    // LINT.IfChange(AndroidTabMediaState)
-    @IntDef({
-        MediaState.NONE,
-        MediaState.MUTED,
-        MediaState.AUDIBLE,
-        MediaState.RECORDING,
-        MediaState.SHARING,
-        MediaState.MAX_VALUE,
-        MediaState.COUNT,
-    })
-    @Target(ElementType.TYPE_USE)
-    @Retention(RetentionPolicy.SOURCE)
-    @interface MediaState {
-        int NONE = 0;
-        int MUTED = 1;
-        int AUDIBLE = 2;
-        int RECORDING = 3;
-        int SHARING = 4;
-        int MAX_VALUE = SHARING;
-        int COUNT = MAX_VALUE + 1;
-    }
-
-    // LINT.ThenChange(//tools/metrics/histograms/metadata/tab/enums.xml:AndroidTabMediaState)
-
     /** The result of the loadUrl. */
     class LoadUrlResult {
         /** Tab load status. */
diff --git a/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/TabObserver.java b/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/TabObserver.java
index 84a2110..8508c91 100644
--- a/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/TabObserver.java
+++ b/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/TabObserver.java
@@ -12,7 +12,6 @@
 import org.chromium.cc.input.BrowserControlsState;
 import org.chromium.chrome.browser.browser_controls.BrowserControlsOffsetTagsInfo;
 import org.chromium.chrome.browser.tab.Tab.LoadUrlResult;
-import org.chromium.chrome.browser.tab.Tab.MediaState;
 import org.chromium.components.find_in_page.FindMatchRectsDetails;
 import org.chromium.components.find_in_page.FindNotificationDetails;
 import org.chromium.content_public.browser.LoadUrlParams;
diff --git a/chrome/browser/tab/media_state.h b/chrome/browser/tab/media_state.h
new file mode 100644
index 0000000..515f6b5
--- /dev/null
+++ b/chrome/browser/tab/media_state.h
@@ -0,0 +1,26 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_TAB_MEDIA_STATE_H_
+#define CHROME_BROWSER_TAB_MEDIA_STATE_H_
+
+namespace tabs {
+
+// LINT.IfChange(AndroidTabMediaState)
+// GENERATED_JAVA_ENUM_PACKAGE: org.chromium.chrome.browser.tab
+// GENERATED_JAVA_CLASS_NAME_OVERRIDE: MediaState
+// Tracks the media state of the tab.
+enum class MediaState {
+  kNone = 0,
+  kMuted = 1,
+  kAudible = 2,
+  kRecording = 3,
+  kSharing = 4,
+  kMaxValue = kSharing,
+};
+// LINT.ThenChange(//tools/metrics/histograms/metadata/tab/enums.xml:AndroidTabMediaState)
+
+}  // namespace tabs
+
+#endif  // CHROME_BROWSER_TAB_MEDIA_STATE_H_
diff --git a/chrome/browser/tab_ui/android/java/src/org/chromium/chrome/browser/tab_ui/TabThumbnailView.java b/chrome/browser/tab_ui/android/java/src/org/chromium/chrome/browser/tab_ui/TabThumbnailView.java
index fc029bf..5ce382f1 100644
--- a/chrome/browser/tab_ui/android/java/src/org/chromium/chrome/browser/tab_ui/TabThumbnailView.java
+++ b/chrome/browser/tab_ui/android/java/src/org/chromium/chrome/browser/tab_ui/TabThumbnailView.java
@@ -23,6 +23,7 @@
 import android.util.AttributeSet;
 import android.widget.ImageView;
 
+import androidx.annotation.IntDef;
 import androidx.appcompat.content.res.AppCompatResources;
 import androidx.core.view.ViewCompat;
 
@@ -32,6 +33,9 @@
 import org.chromium.build.annotations.Nullable;
 import org.chromium.components.tab_groups.TabGroupColorId;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * A specialized {@link ImageView} that clips a thumbnail to a card shape with varied corner radii.
  * Overlays a background drawable. The height is varied based on the width and the aspect ratio of
@@ -50,6 +54,21 @@
 
     private static @MonotonicNonNull Integer sVerticalOffsetPx;
 
+    /** States for the thumbnail view when fetching or displaying placeholders. */
+    @IntDef({
+        ThumbnailViewState.THUMBNAIL_LOADED,
+        ThumbnailViewState.PLACEHOLDER_LOADED,
+        ThumbnailViewState.LOADING
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ThumbnailViewState {
+        int THUMBNAIL_LOADED = 0;
+        int PLACEHOLDER_LOADED = 1;
+        int LOADING = 2;
+    }
+
+    private @ThumbnailViewState int mThumbnailViewState = ThumbnailViewState.PLACEHOLDER_LOADED;
+
     /** To prevent {@link TabThumbnailView#updateImage()} from running during inflation. */
     private final boolean mInitialized;
 
@@ -117,6 +136,11 @@
         updateImage();
     }
 
+    public void setThumbnailViewState(@ThumbnailViewState int state) {
+        mThumbnailViewState = state;
+        updateImage();
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
@@ -173,6 +197,17 @@
 
     private void updateImage() {
         if (!mInitialized) return;
+
+        if (isThumbnailLoading()) {
+            setBackground(mBackgroundDrawable);
+            clearColorFilter();
+            mIconDrawable = null;
+            // Clear the actual image drawable from the ImageView so only background
+            // remains.
+            super.setImageDrawable(null);
+            return;
+        }
+
         // If the drawable is empty, display a placeholder image.
         if (isPlaceholder()) {
             setBackground(mBackgroundDrawable);
@@ -286,6 +321,10 @@
         mBackgroundDrawable.setCornerRadii(mRadii);
     }
 
+    private boolean isThumbnailLoading() {
+        return mThumbnailViewState == ThumbnailViewState.LOADING;
+    }
+
     private void resizeIconDrawable() {
         if (mIconDrawable != null) {
             // Called in onMeasure() so getWidth() and getHeight() may not be ready yet.
diff --git a/chrome/browser/tracing/BUILD.gn b/chrome/browser/tracing/BUILD.gn
index 82e00a41..aee7063 100644
--- a/chrome/browser/tracing/BUILD.gn
+++ b/chrome/browser/tracing/BUILD.gn
@@ -62,6 +62,8 @@
   if (is_chromeos) {
     deps += [ "//ash/constants" ]
   }
+
+  public_deps = [ "//chrome/browser:browser_public_dependencies" ]
 }
 
 source_set("unit_tests") {
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 6cdec26..23d31a5 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -319,6 +319,7 @@
     "//chrome/browser/media/router/discovery/access_code:access_code_cast_feature",
     "//chrome/browser/new_tab_page/chrome_colors:generate_chrome_colors_info",
     "//chrome/browser/new_tab_page/chrome_colors:selected_colors_info",
+    "//chrome/browser/obsolete_system",
     "//chrome/browser/omnibox",
     "//chrome/browser/optimization_guide",
     "//chrome/browser/page_content_annotations",
@@ -4768,6 +4769,7 @@
       "//chrome/browser/collaboration",
       "//chrome/browser/collaboration/messaging",
       "//chrome/browser/desktop_to_mobile_promos:utils",
+      "//chrome/browser/digital_credentials",
       "//chrome/browser/infobars",
       "//chrome/browser/profiles/keep_alive",
       "//chrome/browser/ui/actions",
@@ -4888,6 +4890,9 @@
 
     # Any circular includes must depend on the target "//chrome/browser:browser_public_dependencies".
     allow_circular_includes_from += [
+      # Remove this circular dependency once tab_ui_helpers are modularized.
+      "//chrome/browser/ui/tabs/tab_strip_api/converters",
+
       "//chrome/browser/ui/qrcode_generator:impl",
       "//chrome/browser/ui/send_tab_to_self:impl",
       "//chrome/browser/ui/dialogs:impl",
@@ -4954,6 +4959,12 @@
       # c/b/ui/views/controls/hover_button.h
       # c/b/ui/views/frame/browser_view.h
       "//chrome/browser/ui/views/send_tab_to_self:impl",
+
+      # TODO(crbug.com/353332589): Remove this circular dependency when the following
+      # headers get componentized:
+      # - c/b/ui/views/digital_credentials/digital_identity_bluetooth_manual_dialog_controller.h
+      # - c/b/ui/views/digital_credentials/digital_identity_multi_step_dialog.h
+      "//chrome/browser/digital_credentials",
     ]
 
     if (is_linux) {
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java
index 268360b..e28f54e 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java
@@ -69,6 +69,7 @@
 import org.chromium.chrome.browser.multiwindow.MultiInstanceManager;
 import org.chromium.chrome.browser.multiwindow.MultiInstanceManager.PersistedInstanceType;
 import org.chromium.chrome.browser.omnibox.UrlBar.UrlBarDelegate;
+import org.chromium.chrome.browser.omnibox.fusebox.FuseboxAttachmentModelList;
 import org.chromium.chrome.browser.omnibox.fusebox.FuseboxAttachmentModelList.FuseboxAttachmentChangeListener;
 import org.chromium.chrome.browser.omnibox.fusebox.FuseboxCoordinator;
 import org.chromium.chrome.browser.omnibox.fusebox.FuseboxCoordinator.FuseboxState;
@@ -250,6 +251,7 @@
     private final Supplier<@Nullable ModalDialogManager> mModalDialogManagerSupplier;
     private final FuseboxCoordinator mFuseboxCoordinator;
     private @Nullable AutocompleteInput mCurrentInput;
+    private @Nullable FuseboxAttachmentModelList mFuseboxAttachmentModelList;
     private final Callback<@AutocompleteRequestType Integer> mAutocompleteRequestTypeObserver =
             this::onAutocompleteRequestTypeChanged;
     private @Nullable Callback<Boolean> mOnSpecializedFuseboxModeActivatedCallback;
@@ -354,7 +356,6 @@
                 .getFuseboxStateSupplier()
                 .addSyncObserverAndPostIfNonNull(
                         mCallbackController.makeCancelable(this::onFuseboxStateChanged));
-        mFuseboxCoordinator.addAttachmentChangeListener(this);
         mOmniboxChipManager = omniboxChipManager;
     }
 
@@ -395,7 +396,6 @@
     @SuppressWarnings("NullAway")
     /* package */ void destroy() {
         mCallbackController.destroy();
-        mFuseboxCoordinator.removeAttachmentChangeListener(this);
         TemplateUrlService templateUrlService = mTemplateUrlServiceSupplier.get();
         if (templateUrlService != null) {
             templateUrlService.removeObserver(this);
@@ -1056,6 +1056,8 @@
         if (mCurrentInput != null) {
             mCurrentInput.getRequestTypeSupplier().removeObserver(mAutocompleteRequestTypeObserver);
         }
+        // To avoid the asyc gap between now and on activate, null out here as well.
+        setAttachmentModelList(null);
 
         session.activate(
                 mProfileSupplier,
@@ -1063,6 +1065,7 @@
                     if (mAutocompleteCoordinator == null) return;
                     mAutocompleteCoordinator.beginInput(session);
                     mFuseboxCoordinator.beginInput(session);
+                    setAttachmentModelList(session.getFuseboxAttachmentModelList());
                 });
 
         mCurrentInput = session.getAutocompleteInput();
@@ -1079,9 +1082,10 @@
         mAutocompleteCoordinator.endInput();
         mFuseboxCoordinator.endInput();
         mCurrentInput.getRequestTypeSupplier().removeObserver(mAutocompleteRequestTypeObserver);
-        var state = FuseboxSessionState.from(mLocationBarDataProvider);
+        FuseboxSessionState state = FuseboxSessionState.from(mLocationBarDataProvider);
         if (state != null) state.deactivate();
         mCurrentInput = null;
+        setAttachmentModelList(null);
     }
 
     /**
@@ -2363,4 +2367,15 @@
     void setUrlActionContainerVisibility(boolean shouldShow) {
         mLocationBarLayout.setUrlActionContainerVisibility(shouldShow);
     }
+
+    private void setAttachmentModelList(
+            @Nullable FuseboxAttachmentModelList fuseboxAttachmentModelList) {
+        if (mFuseboxAttachmentModelList != null) {
+            mFuseboxAttachmentModelList.removeAttachmentChangeListener(this);
+        }
+        mFuseboxAttachmentModelList = fuseboxAttachmentModelList;
+        if (mFuseboxAttachmentModelList != null) {
+            mFuseboxAttachmentModelList.addAttachmentChangeListener(this);
+        }
+    }
 }
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/fusebox/FuseboxCoordinator.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/fusebox/FuseboxCoordinator.java
index 3af33598..969649f 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/fusebox/FuseboxCoordinator.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/fusebox/FuseboxCoordinator.java
@@ -29,7 +29,6 @@
 import org.chromium.build.annotations.Nullable;
 import org.chromium.chrome.browser.omnibox.FuseboxSessionState;
 import org.chromium.chrome.browser.omnibox.R;
-import org.chromium.chrome.browser.omnibox.fusebox.FuseboxAttachmentModelList.FuseboxAttachmentChangeListener;
 import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteController;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
@@ -71,7 +70,6 @@
     private final PropertyModel mModel;
     private final Context mContext;
     private final WindowAndroid mWindowAndroid;
-    private final FuseboxAttachmentModelList mModelList;
     private final MonotonicObservableSupplier<TabModelSelector> mTabModelSelectorSupplier;
     private @Nullable FuseboxMediator mMediator;
     private @Nullable AutocompleteInput mInput;
@@ -108,7 +106,6 @@
         mProfileSupplier = profileObservableSupplier;
         mTabModelSelectorSupplier = tabModelSelectorSupplier;
         mSnackbarManager = snackbarManager;
-        mModelList = new FuseboxAttachmentModelList();
 
         if (!OmniboxFeatures.sOmniboxMultimodalInput.isEnabled()
                 || parent.findViewById(R.id.fusebox_request_type) == null) {
@@ -145,13 +142,8 @@
 
         var popup = new FuseboxPopup(mContext, popupWindowBuilder.build(), popupView);
         mViewHolder = new FuseboxViewHolder(parent, popup);
-
-        var adapter = mModelList.getAdapter();
-        mViewHolder.attachmentsView.setAdapter(adapter);
-
         mModel =
                 new PropertyModel.Builder(FuseboxProperties.ALL_KEYS)
-                        .with(FuseboxProperties.ADAPTER, adapter)
                         .with(FuseboxProperties.ATTACHMENTS_TOOLBAR_VISIBLE, false)
                         .with(
                                 FuseboxProperties.AUTOCOMPLETE_REQUEST_TYPE,
@@ -200,7 +192,6 @@
         if (mTemplateUrlService != null) {
             mTemplateUrlService.removeObserver(this);
         }
-        mModelList.destroy();
         if (mViewportRectProvider != null) {
             mViewportRectProvider.destroy();
         }
@@ -335,21 +326,6 @@
         return mFuseboxStateSupplier;
     }
 
-    /** Registers the listener notified whenever attachments list is changed. */
-    public void addAttachmentChangeListener(FuseboxAttachmentChangeListener listener) {
-        mModelList.addAttachmentChangeListener(listener);
-    }
-
-    /** Unregisters the listener from being notified that attachments list has been changed. */
-    public void removeAttachmentChangeListener(FuseboxAttachmentChangeListener listener) {
-        mModelList.removeAttachmentChangeListener(listener);
-    }
-
-    /** Returns the number of attachments in the Fusebox Attachments list. */
-    public int getAttachmentsCount() {
-        return mModelList.size();
-    }
-
     /**
      * Provider of the viewport for the fusebox popup window. This implementation treats the entire
      * window as available, ignoring e.g. ime insets which can reduce the available height to a very
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediator.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediator.java
index f1a35ca..271f83c 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediator.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediator.java
@@ -37,6 +37,7 @@
 import org.chromium.chrome.browser.omnibox.OmniboxMetrics;
 import org.chromium.chrome.browser.omnibox.R;
 import org.chromium.chrome.browser.omnibox.UrlBarEditingTextStateProvider;
+import org.chromium.chrome.browser.omnibox.fusebox.FuseboxAttachmentModelList;
 import org.chromium.chrome.browser.omnibox.fusebox.FuseboxAttachmentModelList.FuseboxAttachmentChangeListener;
 import org.chromium.chrome.browser.omnibox.fusebox.FuseboxCoordinator;
 import org.chromium.chrome.browser.omnibox.fusebox.FuseboxCoordinator.FuseboxState;
@@ -119,6 +120,7 @@
     private final OmniboxSuggestionsDropdownEmbedder mEmbedder;
     private @Nullable AutocompleteInput mAutocompleteInput;
     private @Nullable FuseboxSessionState mSessionState;
+    private @Nullable FuseboxAttachmentModelList mFuseboxAttachmentModelList;
     private final boolean mForcePhoneStyleOmnibox;
     private final Callback<@ControlsPosition Integer> mToolbarPositionChangedCallback =
             this::onToolbarPositionChanged;
@@ -234,7 +236,6 @@
 
         mAnimationDriver = initializeAnimationDriver();
 
-        mFuseboxCoordinator.addAttachmentChangeListener(this);
         mFuseboxCoordinator.getFuseboxStateSupplier().addSyncObserver(mOnFuseboxStateChanged);
 
         mDataProvider
@@ -277,7 +278,6 @@
         if (mNativeInitialized) {
             OmniboxActionFactoryImpl.get().destroyNativeFactory();
         }
-        mFuseboxCoordinator.removeAttachmentChangeListener(this);
         mFuseboxCoordinator.getFuseboxStateSupplier().removeObserver(mOnFuseboxStateChanged);
         mHandler.removeCallbacksAndMessages(null);
         mDropdownViewInfoListBuilder.destroy();
@@ -444,6 +444,7 @@
         cancelAutocompleteRequests();
         setAutocompleteInput(session.getAutocompleteInput());
         mSessionState = session;
+        setFuseboxAttachmentModelList(mSessionState.getFuseboxAttachmentModelList());
 
         if (!alreadyInInput) {
             // Propagate the information about omnibox session state change to all the processors
@@ -518,7 +519,7 @@
         OmniboxMetrics.recordOmniboxFocusResultedInNavigation(
                 mAutocompleteInput.getRequestType(),
                 mOmniboxFocusResultedInNavigation,
-                mFuseboxCoordinator.getAttachmentsCount() > 0);
+                hasAttachments());
         OmniboxMetrics.recordRefineActionUsage(mAutocompleteInput.getRefineActionUsage());
 
         OmniboxMetrics.recordSuggestionsListScrolled(
@@ -538,7 +539,9 @@
         // a consequence the omnibox is unfocused).
         clearSuggestions();
         setAutocompleteInput(null);
+
         mSessionState = null;
+        setFuseboxAttachmentModelList(null);
     }
 
     private void setAutocompleteInput(@Nullable AutocompleteInput input) {
@@ -1663,8 +1666,7 @@
     @Override
     public void onAttachmentListChanged() {
         if (!isInInputSession()) return;
-
-        mAutocompleteInput.setHasAttachments(mFuseboxCoordinator.getAttachmentsCount() > 0);
+        mAutocompleteInput.setHasAttachments(hasAttachments());
         // Re-request ZPS in the event of attachments being removed/replaced.
         onTextChanged(mAutocompleteInput.getUserText(), /* isOnFocusContext= */ false);
     }
@@ -1793,4 +1795,21 @@
 
         mHandler.postDelayed(mRecordZpsSuppressionRunnable, ZPS_SUPPRESSION_METRIC_DEBOUNCE_MS);
     }
+
+    private void setFuseboxAttachmentModelList(
+            @Nullable FuseboxAttachmentModelList fuseboxAttachmentModelList) {
+        if (mFuseboxAttachmentModelList != null) {
+            mFuseboxAttachmentModelList.removeAttachmentChangeListener(this);
+        }
+        mFuseboxAttachmentModelList = fuseboxAttachmentModelList;
+        if (mFuseboxAttachmentModelList != null) {
+            mFuseboxAttachmentModelList.addAttachmentChangeListener(this);
+        }
+    }
+
+    private boolean hasAttachments() {
+        if (!isInInputSession()) return false;
+        FuseboxAttachmentModelList attachments = mSessionState.getFuseboxAttachmentModelList();
+        return attachments != null && !attachments.isEmpty();
+    }
 }
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediatorUnitTest.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediatorUnitTest.java
index da91c90..9949965a 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediatorUnitTest.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediatorUnitTest.java
@@ -188,8 +188,6 @@
                 .when(mFuseboxCoordinator)
                 .getFuseboxStateSupplier();
 
-        lenient().doReturn(0).when(mFuseboxCoordinator).getAttachmentsCount();
-
         mMediator =
                 new AutocompleteMediator(
                         mContext,
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
index 7049d73b..61e954e 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -254,6 +254,10 @@
       <message name="IDS_NOTIFICATION_CATEGORY_BROWSER" desc="Label for browser-related notifications, within a list of notification categories. [CHAR_LIMIT=32]">
         Browser
       </message>
+      <!-- TODO(crbug.com/489124317): Update translation. -->
+      <message name="IDS_NOTIFICATION_CATEGORY_ACTOR" desc="Label for actor-related notifications, within a list of notification categories. [CHAR_LIMIT=32]" translateable="false">
+        Actor
+      </message>
       <message name="IDS_NOTIFICATION_CATEGORY_SCREEN_CAPTURE" desc="Label for notifications shown when screen is sharing or recording, within a list of notification categories. [CHAR_LIMIT=32]">
         Screen Capture
       </message>
diff --git a/chrome/browser/ui/ash/clipboard/clipboard_history_browsertest.cc b/chrome/browser/ui/ash/clipboard/clipboard_history_browsertest.cc
index 2d258ea..c6b1d2b 100644
--- a/chrome/browser/ui/ash/clipboard/clipboard_history_browsertest.cc
+++ b/chrome/browser/ui/ash/clipboard/clipboard_history_browsertest.cc
@@ -1168,7 +1168,7 @@
 
     // Create a widget containing a single, focusable textfield.
     widget_ =
-        CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+        CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
     textfield_ = widget_->SetContentsView(std::make_unique<views::Textfield>());
     textfield_->GetViewAccessibility().SetName(u"Textfield");
     textfield_->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
diff --git a/chrome/browser/ui/ash/device_scheduled_reboot/scheduled_reboot_dialog_unittest.cc b/chrome/browser/ui/ash/device_scheduled_reboot/scheduled_reboot_dialog_unittest.cc
index 575f762..2e82fc67 100644
--- a/chrome/browser/ui/ash/device_scheduled_reboot/scheduled_reboot_dialog_unittest.cc
+++ b/chrome/browser/ui/ash/device_scheduled_reboot/scheduled_reboot_dialog_unittest.cc
@@ -26,7 +26,7 @@
     views::ViewsTestBase::SetUp();
     SetConstrainedWindowViewsClient(CreateChromeConstrainedWindowViewsClient());
     views::Widget::InitParams params =
-        CreateParams(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
+        CreateParams(views::Widget::InitParams::CLIENT_OWNS_WIDGET,
                      views::Widget::InitParams::TYPE_WINDOW);
     parent_widget_.Init(std::move(params));
     parent_widget_.Show();
diff --git a/chrome/browser/ui/ash/multi_user/multi_user_window_manager_browser_adaptor_unittest.cc b/chrome/browser/ui/ash/multi_user/multi_user_window_manager_browser_adaptor_unittest.cc
index fdfa8ecb..cc8a35a 100644
--- a/chrome/browser/ui/ash/multi_user/multi_user_window_manager_browser_adaptor_unittest.cc
+++ b/chrome/browser/ui/ash/multi_user/multi_user_window_manager_browser_adaptor_unittest.cc
@@ -56,6 +56,7 @@
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chrome/test/base/testing_profile_manager.h"
+#include "chrome/test/base/ui_test_utils.h"
 #include "chromeos/ash/components/browser_context_helper/annotated_account_id.h"
 #include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
 #include "chromeos/ash/components/install_attributes/stub_install_attributes.h"
@@ -411,7 +412,7 @@
   const int kActiveDeskIndex = 0;
   for (int i = 0; i < desks_controller->GetNumberOfDesks(); i++) {
     widgets.push_back(
-        CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
+        CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET,
                          nullptr, container_ids[i], gfx::Rect(700, 0, 50, 50)));
     aura::Window* win = widgets[i]->GetNativeWindow();
     windows_.push_back(win);
@@ -1668,7 +1669,7 @@
       CreateTestWindowInShell({.window_id = 0}), {16, 32, 640, 320}, &params));
   browser->window()->Activate();
   // Manually set last active browser in BrowserList for testing.
-  BrowserList::GetInstance()->SetLastActive(browser.get());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser.get());
   EXPECT_EQ(browser.get(), GetLastActiveBrowserWindowInterfaceWithAnyProfile());
   EXPECT_TRUE(browser->window()->IsActive());
   EXPECT_EQ(browser.get(), chrome::FindBrowserWithActiveWindow());
diff --git a/chrome/browser/ui/ash/shelf/chrome_shelf_controller_browsertest.cc b/chrome/browser/ui/ash/shelf/chrome_shelf_controller_browsertest.cc
index 0a2baebb..f81d3ffd 100644
--- a/chrome/browser/ui/ash/shelf/chrome_shelf_controller_browsertest.cc
+++ b/chrome/browser/ui/ash/shelf/chrome_shelf_controller_browsertest.cc
@@ -1068,7 +1068,6 @@
 
   // Ensures that display 0 has one browser with focus and display 1 has two
   // browsers. Each browser only has one tab.
-  BrowserList* browser_list = BrowserList::GetInstance();
   BrowserWindowInterface* const browser0 = browser();
   BrowserWindowInterface* const browser1 = CreateBrowser(browser()->profile());
   BrowserWindowInterface* const browser2 = CreateBrowser(browser()->profile());
@@ -1076,8 +1075,8 @@
   browser1->GetWindow()->SetBounds(displays[1].work_area());
   browser2->GetWindow()->SetBounds(displays[1].work_area());
   // Ensures browser 2 is above browser 1 in display 1.
-  browser_list->SetLastActive(browser2->GetBrowserForMigrationOnly());
-  browser_list->SetLastActive(browser0->GetBrowserForMigrationOnly());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser2);
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser0);
   EXPECT_EQ(chrome::GetTotalBrowserCount(), 3U);
   EXPECT_EQ(displays[0].id(),
             GetDisplayIdForBrowserWindow(browser0->GetWindow()));
diff --git a/chrome/browser/ui/ash/test/accelerator_commands_browsertest.cc b/chrome/browser/ui/ash/test/accelerator_commands_browsertest.cc
index dad26bd..a761b5e7 100644
--- a/chrome/browser/ui/ash/test/accelerator_commands_browsertest.cc
+++ b/chrome/browser/ui/ash/test/accelerator_commands_browsertest.cc
@@ -147,7 +147,7 @@
 
   // 5) Miscellaneous windows (e.g. task manager).
   views::Widget::InitParams params(
-      views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+      views::Widget::InitParams::CLIENT_OWNS_WIDGET);
   params.delegate =
       new views::WidgetDelegateView(views::WidgetDelegateView::CreatePassKey());
   params.delegate->SetCanMaximize(true);
diff --git a/chrome/browser/ui/ash/web_view/ash_web_view_impl_browsertest.cc b/chrome/browser/ui/ash/web_view/ash_web_view_impl_browsertest.cc
index 8252be7..1c8cd446 100644
--- a/chrome/browser/ui/ash/web_view/ash_web_view_impl_browsertest.cc
+++ b/chrome/browser/ui/ash/web_view/ash_web_view_impl_browsertest.cc
@@ -111,7 +111,7 @@
   auto widget = std::make_unique<views::Widget>();
 
   views::Widget::InitParams params(
-      views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
+      views::Widget::InitParams::CLIENT_OWNS_WIDGET,
       views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
   params.activatable = activatable
                            ? views::Widget::InitParams::Activatable::kDefault
diff --git a/chrome/browser/ui/browser_browsertest.cc b/chrome/browser/ui/browser_browsertest.cc
index b90bad8a..21510c5 100644
--- a/chrome/browser/ui/browser_browsertest.cc
+++ b/chrome/browser/ui/browser_browsertest.cc
@@ -2965,7 +2965,7 @@
 
 IN_PROC_BROWSER_TEST_F(BrowserTest, TestActiveBrowserChangedUserAction) {
   base::UserActionTester user_action_tester;
-  BrowserList::SetLastActive(browser());
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser());
   EXPECT_EQ(user_action_tester.GetActionCount("ActiveBrowserChanged"), 1);
 }
 
diff --git a/chrome/browser/ui/browser_commands.cc b/chrome/browser/ui/browser_commands.cc
index b96a520..a239247f 100644
--- a/chrome/browser/ui/browser_commands.cc
+++ b/chrome/browser/ui/browser_commands.cc
@@ -2192,8 +2192,7 @@
 
 void ShowTabSearch(BrowserWindowInterface* bwi) {
   bwi->GetBrowserForMigrationOnly()->window()->CreateTabSearchBubble(
-      tab_search::mojom::TabSearchSection::kSearch,
-      tab_search::mojom::TabOrganizationFeature::kNone);
+      tab_search::mojom::TabSearchSection::kSearch);
 }
 
 void CloseTabSearch(Browser* browser) {
@@ -2222,8 +2221,7 @@
 
 void ShowTabDeclutter(Browser* browser) {
   browser->window()->CreateTabSearchBubble(
-      tab_search::mojom::TabSearchSection::kOrganize,
-      tab_search::mojom::TabOrganizationFeature::kDeclutter);
+      tab_search::mojom::TabSearchSection::kOrganize);
 }
 
 bool CanCloseFind(Browser* browser) {
diff --git a/chrome/browser/ui/browser_element_identifiers.cc b/chrome/browser/ui/browser_element_identifiers.cc
index 37b53df..782fe32 100644
--- a/chrome/browser/ui/browser_element_identifiers.cc
+++ b/chrome/browser/ui/browser_element_identifiers.cc
@@ -72,6 +72,7 @@
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kInactiveTabSettingElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kInstallPwaElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kIntentChipElementId);
+DEFINE_ELEMENT_IDENTIFIER_VALUE(kIOSLensPromoAnchorElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kJsOptimizationsIconElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kLeftAlignedSidePanelSeparatorViewElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kLensOverlayHomeworkPageActionIconElementId);
diff --git a/chrome/browser/ui/browser_element_identifiers.h b/chrome/browser/ui/browser_element_identifiers.h
index 85738f5c..69a682e0 100644
--- a/chrome/browser/ui/browser_element_identifiers.h
+++ b/chrome/browser/ui/browser_element_identifiers.h
@@ -111,6 +111,7 @@
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kInactiveTabSettingElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kInstallPwaElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kIntentChipElementId);
+DECLARE_ELEMENT_IDENTIFIER_VALUE(kIOSLensPromoAnchorElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kJsOptimizationsIconElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kLeftAlignedSidePanelSeparatorViewElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kLensOverlayHomeworkPageActionIconElementId);
diff --git a/chrome/browser/ui/browser_list.h b/chrome/browser/ui/browser_list.h
index 7965efc..52cf009 100644
--- a/chrome/browser/ui/browser_list.h
+++ b/chrome/browser/ui/browser_list.h
@@ -54,6 +54,14 @@
   static void AddObserver(BrowserListObserver* observer);
   static void RemoveObserver(BrowserListObserver* observer);
 
+ private:
+  friend class Browser;
+  friend class BrowserObserverChild;
+  FRIEND_TEST_ALL_PREFIXES(BrowserListBrowserTest, ObserverAddedInFlight);
+
+  BrowserList();
+  ~BrowserList();
+
   // Called by Browser objects when their window is activated (focused).  This
   // allows us to determine what the last active Browser was on each desktop.
   static void SetLastActive(Browser* browser);
@@ -61,13 +69,6 @@
   // Notifies the observers when the current active browser becomes not active.
   static void NotifyBrowserNoLongerActive(Browser* browser);
 
- private:
-  friend class BrowserObserverChild;
-  FRIEND_TEST_ALL_PREFIXES(BrowserListBrowserTest, ObserverAddedInFlight);
-
-  BrowserList();
-  ~BrowserList();
-
   const_iterator deprecated_begin() const { return browsers_.begin(); }
   const_iterator deprecated_end() const { return browsers_.end(); }
 
diff --git a/chrome/browser/ui/browser_tabrestore.cc b/chrome/browser/ui/browser_tabrestore.cc
index 7f5531b..dc253df 100644
--- a/chrome/browser/ui/browser_tabrestore.cc
+++ b/chrome/browser/ui/browser_tabrestore.cc
@@ -195,7 +195,7 @@
     // the throbber when a background restored tab is loading.
     tabs::TabInterface* const tab_interface =
         tabs::TabInterface::GetFromContents(raw_web_contents);
-    TabUIHelper::From(tab_interface)->set_created_by_session_restore(true);
+    TabUIHelper::From(tab_interface)->SetCreatedBySessionRestore(true);
   }
 
   // We set the size of the view here, before Blink does its initial layout.
@@ -320,7 +320,7 @@
     // the throbber when a background restored tab is loading.
     tabs::TabInterface* const tab_interface =
         tabs::TabInterface::GetFromContents(raw_web_contents);
-    TabUIHelper::From(tab_interface)->set_created_by_session_restore(true);
+    TabUIHelper::From(tab_interface)->SetCreatedBySessionRestore(true);
   }
 
   tab_strip->CloseWebContentsAt(insertion_index, TabCloseTypes::CLOSE_NONE);
diff --git a/chrome/browser/ui/browser_window.h b/chrome/browser/ui/browser_window.h
index 6314437d..c7a5719 100644
--- a/chrome/browser/ui/browser_window.h
+++ b/chrome/browser/ui/browser_window.h
@@ -592,15 +592,10 @@
   virtual void ShowCaretBrowsingDialog() = 0;
 
   // Create and open the tab search bubble. Optionally force it to open to the
-  // given section and organization feature.
+  // given section.
   virtual void CreateTabSearchBubble(
-      tab_search::mojom::TabSearchSection section,
-      tab_search::mojom::TabOrganizationFeature organization_feature) = 0;
-  void CreateTabSearchBubble(tab_search::mojom::TabSearchSection section =
-                                 tab_search::mojom::TabSearchSection::kSearch) {
-    CreateTabSearchBubble(section,
-                          tab_search::mojom::TabOrganizationFeature::kNone);
-  }
+      tab_search::mojom::TabSearchSection section =
+          tab_search::mojom::TabSearchSection::kSearch) = 0;
 
   // Closes the tab search bubble if open for the given browser instance.
   virtual void CloseTabSearchBubble() = 0;
diff --git a/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBaseWindow.java b/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBaseWindow.java
index 82d6b9f..454e84a 100644
--- a/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBaseWindow.java
+++ b/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBaseWindow.java
@@ -17,16 +17,13 @@
 /** Java class for communicating with the native {@code AndroidBaseWindow}. */
 @NullMarked
 final class AndroidBaseWindow {
-
-    /** Supports windowing functionalities of the native {@code AndroidBaseWindow}. */
-    @SuppressWarnings("UnusedVariable")
-    private final ChromeAndroidTask mChromeAndroidTask;
+    private final AndroidBrowserWindow mAndroidBrowserWindow;
 
     /** Address of the native {@code AndroidBaseWindow}. */
     private long mNativeAndroidBaseWindow;
 
-    AndroidBaseWindow(ChromeAndroidTask chromeAndroidTask) {
-        mChromeAndroidTask = chromeAndroidTask;
+    AndroidBaseWindow(AndroidBrowserWindow androidBrowserWindow) {
+        mAndroidBrowserWindow = androidBrowserWindow;
     }
 
     /**
@@ -51,86 +48,86 @@
 
     @CalledByNative
     private boolean isActive() {
-        return mChromeAndroidTask.isActive();
+        return mAndroidBrowserWindow.getTask().isActive();
     }
 
     @CalledByNative
     private boolean isMaximized() {
-        return mChromeAndroidTask.isMaximized();
+        return mAndroidBrowserWindow.getTask().isMaximized();
     }
 
     @CalledByNative
     private boolean isMinimized() {
-        return mChromeAndroidTask.isMinimized();
+        return mAndroidBrowserWindow.getTask().isMinimized();
     }
 
     @CalledByNative
     private boolean isFullscreen() {
-        return mChromeAndroidTask.isFullscreen();
+        return mAndroidBrowserWindow.getTask().isFullscreen();
     }
 
     @CalledByNative
     @JniType("std::vector<int>")
     private int[] getRestoredBounds() {
-        Rect bounds = mChromeAndroidTask.getRestoredBoundsInDp();
+        Rect bounds = mAndroidBrowserWindow.getTask().getRestoredBoundsInDp();
         return new int[] {bounds.left, bounds.top, bounds.width(), bounds.height()};
     }
 
     @CalledByNative
     @JniType("std::vector<int>")
     private int[] getBounds() {
-        Rect bounds = mChromeAndroidTask.getBoundsInDp();
+        Rect bounds = mAndroidBrowserWindow.getTask().getBoundsInDp();
         return new int[] {bounds.left, bounds.top, bounds.width(), bounds.height()};
     }
 
     @CalledByNative
     private void show() {
-        mChromeAndroidTask.show();
+        mAndroidBrowserWindow.getTask().show();
     }
 
     @CalledByNative
     private boolean isVisible() {
-        return mChromeAndroidTask.isVisible();
+        return mAndroidBrowserWindow.getTask().isVisible();
     }
 
     @CalledByNative
     private void showInactive() {
-        mChromeAndroidTask.showInactive();
+        mAndroidBrowserWindow.getTask().showInactive();
     }
 
     @CalledByNative
     private void close() {
-        mChromeAndroidTask.close();
+        mAndroidBrowserWindow.getTask().close();
     }
 
     @CalledByNative
     private void activate() {
-        mChromeAndroidTask.activate();
+        mAndroidBrowserWindow.getTask().activate();
     }
 
     @CalledByNative
     private void deactivate() {
-        mChromeAndroidTask.deactivate();
+        mAndroidBrowserWindow.getTask().deactivate();
     }
 
     @CalledByNative
     private void maximize() {
-        mChromeAndroidTask.maximize();
+        mAndroidBrowserWindow.getTask().maximize();
     }
 
     @CalledByNative
     private void minimize() {
-        mChromeAndroidTask.minimize();
+        mAndroidBrowserWindow.getTask().minimize();
     }
 
     @CalledByNative
     private void restore() {
-        mChromeAndroidTask.restore();
+        mAndroidBrowserWindow.getTask().restore();
     }
 
     @CalledByNative
     private void setBounds(int left, int top, int right, int bottom) {
-        mChromeAndroidTask.setBoundsInDp(new Rect(left, top, right, bottom));
+        mAndroidBrowserWindow.getTask().setBoundsInDp(new Rect(left, top, right, bottom));
     }
 
     @CalledByNative
@@ -140,7 +137,7 @@
 
     @CalledByNative
     private @Nullable WindowAndroid getWindowAndroid() {
-        return mChromeAndroidTask.getTopActivityWindowAndroid();
+        return mAndroidBrowserWindow.getActivityWindowAndroid();
     }
 
     long getNativePtrForTesting() {
diff --git a/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBrowserWindow.java b/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBrowserWindow.java
index 7917193..d8dd9d4 100644
--- a/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBrowserWindow.java
+++ b/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBrowserWindow.java
@@ -15,6 +15,7 @@
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.build.annotations.Nullable;
 import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.ui.base.ActivityWindowAndroid;
 
 /** Java class for communicating with the native {@code AndroidBrowserWindow}. */
 @NullMarked
@@ -23,6 +24,7 @@
     private final ChromeAndroidTask mChromeAndroidTask;
     private final Profile mProfile;
     private final AndroidBaseWindow mAndroidBaseWindow;
+    private @Nullable ActivityWindowAndroid mActivityWindowAndroid;
 
     /** Address of the native {@code AndroidBrowserWindow}. */
     private long mNativeAndroidBrowserWindow;
@@ -33,10 +35,19 @@
      */
     private boolean mIsDeleteScheduled;
 
-    AndroidBrowserWindow(ChromeAndroidTask chromeAndroidTask, Profile profile) {
+    AndroidBrowserWindow(
+            ChromeAndroidTask chromeAndroidTask,
+            Profile profile,
+            @Nullable ActivityWindowAndroid activityWindowAndroid) {
         mChromeAndroidTask = chromeAndroidTask;
         mProfile = profile;
-        mAndroidBaseWindow = new AndroidBaseWindow(chromeAndroidTask);
+        mAndroidBaseWindow = new AndroidBaseWindow(this);
+        mActivityWindowAndroid = activityWindowAndroid;
+    }
+
+    /** Returns the {@link ChromeAndroidTask} that owns this window. */
+    ChromeAndroidTask getTask() {
+        return mChromeAndroidTask;
     }
 
     /**
@@ -96,15 +107,28 @@
         return AndroidBrowserWindowJni.get().getSessionIdForTesting(mNativeAndroidBrowserWindow);
     }
 
-    @CalledByNative
-    private void clearNativePtr() {
-        mNativeAndroidBrowserWindow = 0;
+    Profile getProfile() {
+        return mProfile;
+    }
+
+    void setActivityWindowAndroid(ActivityWindowAndroid activityWindowAndroid) {
+        assert mActivityWindowAndroid == null
+                : "An Activity is already associated with this AndroidBrowserWindow";
+        mActivityWindowAndroid = activityWindowAndroid;
+    }
+
+    @Nullable ActivityWindowAndroid getActivityWindowAndroid() {
+        return mActivityWindowAndroid;
     }
 
     @CalledByNative
     @Nullable Activity getActivity() {
-        var activityWindowAndroid = mChromeAndroidTask.getTopActivityWindowAndroid();
-        return activityWindowAndroid == null ? null : activityWindowAndroid.getActivity().get();
+        return mActivityWindowAndroid == null ? null : mActivityWindowAndroid.getActivity().get();
+    }
+
+    @CalledByNative
+    private void clearNativePtr() {
+        mNativeAndroidBrowserWindow = 0;
     }
 
     @NativeMethods
diff --git a/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskImpl.java b/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskImpl.java
index 1e746dd..1bcb917 100644
--- a/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskImpl.java
+++ b/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskImpl.java
@@ -128,6 +128,62 @@
         int DESTROYED = 5;
     }
 
+    /** Includes the public {@link ActivityScopedObjects} and internal Activity-scoped objects. */
+    private static final class InternalActivityScopedObjects {
+        final ActivityScopedObjects mActivityScopedObjects;
+
+        /**
+         * Contains {@link AndroidBrowserWindow}s for one Activity.
+         *
+         * <p>{@link AndroidBrowserWindow} is the Android counterpart of the native {@code
+         * BrowserWindowInterface} (BWI).
+         *
+         * <p>BWI assumes it will only be associated with 1 profile, and a lot of native code is
+         * based on this assumption. However, on Android, a {@code ChromeActivity} can have more
+         * than one Profile, e.g., the {@code ChromeActivity} on mobile Android allows the user to
+         * switch between a regular tab and an incognito tab without creating a new Activity/Task.
+         *
+         * <p>Therefore, to keep the aforementioned assumption valid, and to avoid auditing all
+         * native code, one {@code ChromeActivity} is allowed to have more than one BWI, each for a
+         * different Profile.
+         */
+        final Map<Profile, AndroidBrowserWindow> mAndroidBrowserWindows = new ArrayMap<>();
+
+        InternalActivityScopedObjects(
+                ActivityScopedObjects activityScopedObjects, AndroidBrowserWindow browserWindow) {
+            assert activityScopedObjects.mActivityWindowAndroid
+                            == browserWindow.getActivityWindowAndroid()
+                    : "AndroidBrowserWindow does not match ActivityScopedObjects.";
+            var currentProfile =
+                    activityScopedObjects.mTabModelSelector.getCurrentModel().getProfile();
+            assert currentProfile == browserWindow.getProfile()
+                    : "browserWindow profile does not match activityScopedObjects. Did TabModel"
+                            + " change its current Profile?";
+
+            mActivityScopedObjects = activityScopedObjects;
+            mAndroidBrowserWindows.put(currentProfile, browserWindow);
+        }
+
+        AndroidBrowserWindow getBrowserWindowForCurrentProfile() {
+            var profile = mActivityScopedObjects.mTabModelSelector.getCurrentModel().getProfile();
+            assert profile != null
+                    : "getBrowserWindowForCurrentProfile() called with a TabModel with no Profile.";
+            var browserWindow = mAndroidBrowserWindows.get(profile);
+            assert browserWindow != null
+                    : "getBrowserWindowForCurrentProfile() called but no AndroidBrowserWindow for"
+                            + " Profile!";
+            return browserWindow;
+        }
+
+        void addBrowserWindow(AndroidBrowserWindow browserWindow) {
+            var profile = browserWindow.getProfile();
+            assert !mAndroidBrowserWindows.containsKey(profile)
+                    : "Within one Activity, a Profile can only be associated with one"
+                            + " AndroidBrowserWindow";
+            mAndroidBrowserWindows.put(profile, browserWindow);
+        }
+    }
+
     /**
      * Contains objects whose lifecycle is in sync with the top {@code Activity} tracked by this
      * {@link ChromeAndroidTask}.
@@ -151,7 +207,16 @@
         final @Nullable DesktopWindowStateManager mDesktopWindowStateManager;
 
         static @Nullable TopActivityScopedObjects obtain(ChromeAndroidTaskImpl chromeAndroidTask) {
-            var activityScopedObjects = chromeAndroidTask.mActivityScopedObjectsDeque.peekFirst();
+            var internalActivityScopedObjects =
+                    chromeAndroidTask.mActivityScopedObjectsDeque.peekFirst();
+            var activityScopedObjects =
+                    internalActivityScopedObjects == null
+                            ? null
+                            : internalActivityScopedObjects.mActivityScopedObjects;
+            var desktopWindowStateManager =
+                    activityScopedObjects == null
+                            ? null
+                            : activityScopedObjects.mDesktopWindowStateManager;
             var activityWindowAndroid =
                     activityScopedObjects == null
                             ? null
@@ -163,9 +228,7 @@
             return activityWindowAndroid == null || activity == null
                     ? null
                     : new TopActivityScopedObjects(
-                            activity,
-                            activityWindowAndroid,
-                            activityScopedObjects.mDesktopWindowStateManager);
+                            activity, activityWindowAndroid, desktopWindowStateManager);
         }
 
         private TopActivityScopedObjects(
@@ -196,14 +259,6 @@
     // TabModelSelector to determine the profile.
     private final Profile mInitialProfile;
 
-    /**
-     * Each {@link AndroidBrowserWindow} is associated with a {@link Profile}. A task tracks the
-     * state of a particular OS level window, which may contain multiple virtual {@link
-     * AndroidBrowserWindow}s if the current activity supports multiple profiles i.e. {@code
-     * mSupportedProfileType} is {@link SupportedProfileType#MIXED}.
-     */
-    private final Map<Profile, AndroidBrowserWindow> mAndroidBrowserWindows = new ArrayMap<>();
-
     private final ObserverList<AndroidBrowserWindowObserver> mAndroidBrowserWindowObservers =
             new ObserverList<>();
 
@@ -217,16 +272,24 @@
             new ArrayMap<>();
 
     /**
-     * All {@link ActivityScopedObjects} instances associated with this Task.
+     * When the Task is PENDING, this variable is used to store the associated {@link
+     * AndroidBrowserWindow}.
+     */
+    @Nullable private AndroidBrowserWindow mPendingBrowserWindow;
+
+    /**
+     * All {@link InternalActivityScopedObjects} instances associated with this Task.
      *
      * <p>As a {@link ChromeAndroidTask} is meant to track an Android Task, but {@link
-     * ActivityScopedObjects} is associated with a {@code ChromeActivity}, {@link
-     * ActivityScopedObjects} should be added/removed per the {@code ChromeActivity} lifecycle.
+     * InternalActivityScopedObjects} is associated with a {@code ChromeActivity}, {@link
+     * InternalActivityScopedObjects} should be added/removed per the {@code ChromeActivity}
+     * lifecycle.
      *
      * @see #addActivityScopedObjects
      * @see #removeActivityScopedObjects
      */
-    private final Deque<ActivityScopedObjects> mActivityScopedObjectsDeque = new ArrayDeque<>();
+    private final Deque<InternalActivityScopedObjects> mActivityScopedObjectsDeque =
+            new ArrayDeque<>();
 
     /**
      * Observer for profile removal. This is attached to the {@link ProfileManager} in the
@@ -239,15 +302,7 @@
 
                 @Override
                 public void onProfileDestroyed(Profile profile) {
-                    var iterator = mFeatures.entrySet().iterator();
-                    while (iterator.hasNext()) {
-                        var entry = iterator.next();
-                        var key = entry.getKey();
-                        if (profile.equals(key.mProfile)) {
-                            entry.getValue().onFeatureRemoved();
-                            iterator.remove();
-                        }
-                    }
+                    destroyFeaturesForProfile(profile);
 
                     // TODO(crbug.com/479566813): Several objects for desktop Android related to
                     // extensions do not handle the BrowserWindow destruction happening when the
@@ -255,9 +310,28 @@
                     // destruction until the activity is destroyed since there should never be more
                     // than one profile/window on desktop Android.
                     if (!BuildConfig.IS_DESKTOP_ANDROID) {
-                        var browserWindow = mAndroidBrowserWindows.remove(profile);
-                        if (browserWindow != null) {
-                            destroyBrowserWindow(profile, browserWindow);
+                        if (mPendingBrowserWindow != null
+                                && mPendingBrowserWindow.getProfile() == profile) {
+                            assert mActivityScopedObjectsDeque.isEmpty();
+
+                            destroyBrowserWindow(
+                                    mPendingBrowserWindow, null, mAndroidBrowserWindowObservers);
+                            mPendingBrowserWindow = null;
+                            return;
+                        }
+
+                        var iterator = mActivityScopedObjectsDeque.iterator();
+                        while (iterator.hasNext()) {
+                            var internalActivityScopedObjects = iterator.next();
+                            var browserWindow =
+                                    internalActivityScopedObjects.mAndroidBrowserWindows.get(
+                                            profile);
+                            if (browserWindow != null) {
+                                destroyBrowserWindow(
+                                        browserWindow,
+                                        internalActivityScopedObjects,
+                                        mAndroidBrowserWindowObservers);
+                            }
                         }
                     }
                 }
@@ -265,24 +339,41 @@
 
     private final Callback<TabModel> mOnTabModelSelectedCallback = this::onTabModelSelected;
 
+    // TODO(crbug.com/486858979): Attach this listener to all Activities.
+    // See the comments in cl/7560711 for more details.
     private final IncognitoTabModelObserver mIncognitoTabModelObserver =
             new IncognitoTabModelObserver() {
                 @Override
                 public void onIncognitoModelCreated() {
-                    var activityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
-                    assert activityScopedObjects != null
-                            : "ActivityScopedObjects should not be null if the"
+                    var internalActivityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
+                    assert internalActivityScopedObjects != null
+                            : "InternalActivityScopedObjects should not be null since the"
                                     + " mIncognitoTabModelObserver is registered.";
-                    var tabModelSelector = activityScopedObjects.mTabModelSelector;
-                    var incognitoModel = tabModelSelector.getModel(/* incognito= */ true);
+                    var incognitoModel =
+                            internalActivityScopedObjects.mActivityScopedObjects.mTabModelSelector
+                                    .getModel(/* incognito= */ true);
 
                     var incognitoProfile = incognitoModel.getProfile();
                     assert incognitoProfile != null : "Incognito profile should not be null.";
-                    assert !mAndroidBrowserWindows.containsKey(incognitoProfile)
-                            : "AndroidBrowserWindow should not be associated with the incognito"
-                                    + " profile yet.";
+                    assert internalActivityScopedObjects.mAndroidBrowserWindows.get(
+                                            incognitoProfile)
+                                    == null
+                            : "Incognito TabModel created, but its Activity already has the"
+                                    + " incognito Profile";
 
-                    associateTabModelWithBrowserWindow(incognitoModel);
+                    var browserWindow =
+                            new AndroidBrowserWindow(
+                                    ChromeAndroidTaskImpl.this,
+                                    incognitoProfile,
+                                    internalActivityScopedObjects
+                                            .mActivityScopedObjects
+                                            .mActivityWindowAndroid);
+                    internalActivityScopedObjects.addBrowserWindow(browserWindow);
+                    long ptr = browserWindow.getOrCreateNativePtr();
+                    incognitoModel.associateWithBrowserWindow(browserWindow.getOrCreateNativePtr());
+                    for (var observer : mAndroidBrowserWindowObservers) {
+                        observer.onBrowserWindowAdded(ptr);
+                    }
                 }
             };
 
@@ -421,11 +512,6 @@
                 : "ChromeAndroidTask must be initialized with a non-null profile";
         mInitialProfile = initialProfile;
 
-        // The AndroidBrowserWindowObserver list will be empty at this point, so it's safe to not
-        // notify the observers.
-        mAndroidBrowserWindows.put(
-                mInitialProfile,
-                new AndroidBrowserWindow(/* chromeAndroidTask= */ this, mInitialProfile));
         ProfileManager.addObserver(mProfileObserver);
 
         mState = State.IDLE;
@@ -440,11 +526,13 @@
         assert mInitialProfile != null
                 : "PendingTaskInfo must be initialized with a non-null profile";
 
-        // The AndroidBrowserWindowObserver list will be empty at this point, so it's safe to not
-        // notify the observers.
-        mAndroidBrowserWindows.put(
-                mInitialProfile,
-                new AndroidBrowserWindow(/* chromeAndroidTask= */ this, mInitialProfile));
+        // ActivityWindowAndroid does not exist yet, since Task is pending. So we pass a null
+        // ActivityWindowAndroid.
+        mPendingBrowserWindow =
+                new AndroidBrowserWindow(
+                        /* chromeAndroidTask= */ this,
+                        mInitialProfile,
+                        /* activityWindowAndroid= */ null);
 
         ProfileManager.addObserver(mProfileObserver);
 
@@ -496,8 +584,11 @@
         JniOnceCallback<Long> taskCreationCallbackForNative =
                 mPendingTaskInfo.mTaskCreationCallbackForNative;
         if (taskCreationCallbackForNative != null) {
-            var browserWindow = mAndroidBrowserWindows.get(mInitialProfile);
-            assert browserWindow != null;
+            assert mActivityScopedObjectsDeque.size() == 1
+                    : "#completePendingCreate() called in an invalid state";
+            var internalActivityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
+            var browserWindow = internalActivityScopedObjects.getBrowserWindowForCurrentProfile();
+
             taskCreationCallbackForNative.onResult(browserWindow.getOrCreateNativePtr());
         }
         mPendingTaskInfo = null;
@@ -518,10 +609,12 @@
         ThreadUtils.assertOnUiThread();
 
         // (1) Check whether the Activity to remove is the top Activity.
-        var topActivityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
-        if (topActivityScopedObjects == null) {
+        var internalActivityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
+        if (internalActivityScopedObjects == null) {
             return;
         }
+        var topActivityScopedObjects = internalActivityScopedObjects.mActivityScopedObjects;
+
         boolean isActivityToRemoveAtTop =
                 (activityWindowAndroid == topActivityScopedObjects.mActivityWindowAndroid);
 
@@ -550,7 +643,11 @@
             return;
         }
 
-        var topActivityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
+        var internalActivityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
+        var topActivityScopedObjects =
+                internalActivityScopedObjects == null
+                        ? null
+                        : internalActivityScopedObjects.mActivityScopedObjects;
         var tabModelSelector =
                 topActivityScopedObjects == null
                         ? null
@@ -575,10 +672,11 @@
     @Override
     public @Nullable Intent createIntentForNormalBrowserWindow(boolean isIncognito) {
         ThreadUtils.assertOnUiThread();
-        var topActivityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
-        if (topActivityScopedObjects == null) {
+        var internalActivityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
+        if (internalActivityScopedObjects == null) {
             return null;
         }
+        var topActivityScopedObjects = internalActivityScopedObjects.mActivityScopedObjects;
 
         var multiInstanceManager = topActivityScopedObjects.mMultiInstanceManager;
         if (multiInstanceManager == null) {
@@ -589,6 +687,7 @@
                 isIncognito, NewWindowAppSource.BROWSER_WINDOW_CREATOR);
     }
 
+    // TODO(crbug.com/486858979): Mark this as deprecated and add Activity as a parameter.
     @Override
     public long getOrCreateNativeBrowserWindowPtr(Profile profile) {
         ThreadUtils.assertOnUiThread();
@@ -596,8 +695,8 @@
                         || mState == State.IDLE
                         || mState == State.PENDING_UPDATE
                 : "This Task is not pending or alive.";
-        var browserWindow = mAndroidBrowserWindows.get(profile);
-        assert browserWindow != null : "Profile not found in AndroidBrowserWindows map.";
+        var browserWindow = getTopmostWindowWithProfile(profile);
+        assert browserWindow != null : "No AndroidBrowserWindow found for given Profile.";
         return browserWindow.getOrCreateNativePtr();
     }
 
@@ -625,30 +724,62 @@
             mPendingTaskInfo = null;
         }
 
-        removeAllActivityScopedObjects();
         destroyFeatures();
         ProfileManager.removeObserver(mProfileObserver);
 
-        for (var profileAndbrowserWindow : mAndroidBrowserWindows.entrySet()) {
-            destroyBrowserWindow(
-                    profileAndbrowserWindow.getKey(), profileAndbrowserWindow.getValue());
-        }
-        mAndroidBrowserWindows.clear();
+        removeAllActivityScopedObjects();
+
         mState = State.DESTROYED;
     }
 
-    private void destroyBrowserWindow(Profile profile, AndroidBrowserWindow browserWindow) {
-        long ptr = browserWindow.getOrCreateNativePtr();
-        for (var observer : mAndroidBrowserWindowObservers) {
-            observer.onBrowserWindowRemoved(ptr);
+    /**
+     * Destroys an {@link AndroidBrowserWindow} with guaranteed correctness of the order of method
+     * calls and object destruction.
+     *
+     * <p>Do not destroy an {@link AndroidBrowserWindow} in any other way; always use this method.
+     *
+     * <p>Do not make this method non-static; being stateless helps guarantee its correctness.
+     *
+     * @param browserWindow The {@link AndroidBrowserWindow} to destroy.
+     * @param InternalActivityScopedObjects The {@link InternalActivityScopedObjects} the given
+     *     {@code browserWindow} is associated with; failure to provide the correct {@link
+     *     InternalActivityScopedObjects} will result in a crash.
+     * @param browserWindowObservers Observers to be notified of the {@link AndroidBrowserWindow}
+     *     destruction.
+     */
+    private static void destroyBrowserWindow(
+            AndroidBrowserWindow browserWindow,
+            @Nullable InternalActivityScopedObjects internalActivityScopedObjects,
+            ObserverList<AndroidBrowserWindowObserver> browserWindowObservers) {
+
+        // Check if the given browserWindow matches internalActivityScopedObjects.
+        if (internalActivityScopedObjects == null) {
+            assert browserWindow.getActivityWindowAndroid() == null;
+        } else {
+            assert browserWindow.getActivityWindowAndroid()
+                    == internalActivityScopedObjects.mActivityScopedObjects.mActivityWindowAndroid;
         }
-        var activityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
-        if (activityScopedObjects != null) {
-            activityScopedObjects
+
+        // TODO(crbug.com/486858979): Replace with AndroidBrowserWindow#getNativePtr() which
+        // guarantees to return a non-null ptr and not create a new pointer.
+        long ptr = browserWindow.getOrCreateNativePtr();
+
+        if (internalActivityScopedObjects != null) {
+            var profile = browserWindow.getProfile();
+            internalActivityScopedObjects
+                    .mActivityScopedObjects
                     .mTabModelSelector
                     .getModel(profile.isOffTheRecord())
                     .dissociateWithBrowserWindow();
+            internalActivityScopedObjects.mAndroidBrowserWindows.remove(profile);
         }
+
+        // Note: Notify observers immediately before browserWindow.destroy(), and after everything
+        // else.
+        for (var observer : browserWindowObservers) {
+            observer.onBrowserWindowRemoved(ptr);
+        }
+
         browserWindow.destroy();
     }
 
@@ -1030,7 +1161,14 @@
 
     List<ActivityScopedObjects> getActivityScopedObjectsListForTesting() {
         ThreadUtils.assertOnUiThread();
-        return new ArrayList<>(mActivityScopedObjectsDeque);
+
+        List<ActivityScopedObjects> resultList =
+                new ArrayList<>(mActivityScopedObjectsDeque.size());
+        for (InternalActivityScopedObjects internalObj : mActivityScopedObjectsDeque) {
+            resultList.add(internalObj.mActivityScopedObjects);
+        }
+
+        return resultList;
     }
 
     @Override
@@ -1048,7 +1186,7 @@
 
     @Override
     public @Nullable Integer getSessionIdForTesting(Profile profile) {
-        var browserWindow = mAndroidBrowserWindows.get(profile);
+        var browserWindow = getTopmostWindowWithProfile(profile);
         return browserWindow == null ? null : browserWindow.getNativeSessionIdForTesting();
     }
 
@@ -1056,8 +1194,17 @@
     public List<Long> getAllNativeBrowserWindowPtrs() {
         ThreadUtils.assertOnUiThread();
         List<Long> allNativeBrowserWindowPtrs = new ArrayList<>();
-        for (var browserWindow : mAndroidBrowserWindows.values()) {
-            allNativeBrowserWindowPtrs.add(browserWindow.getOrCreateNativePtr());
+        for (var internalActivityScopedObjects : mActivityScopedObjectsDeque) {
+
+            Iterator<Map.Entry<Profile, AndroidBrowserWindow>> window_iterator =
+                    internalActivityScopedObjects.mAndroidBrowserWindows.entrySet().iterator();
+            while (window_iterator.hasNext()) {
+                var entry = window_iterator.next();
+                allNativeBrowserWindowPtrs.add(entry.getValue().getOrCreateNativePtr());
+            }
+        }
+        if (mPendingBrowserWindow != null) {
+            allNativeBrowserWindowPtrs.add(mPendingBrowserWindow.getOrCreateNativePtr());
         }
         return allNativeBrowserWindowPtrs;
     }
@@ -1083,16 +1230,49 @@
         return mState;
     }
 
+    List<AndroidBrowserWindow> getBrowserWindowsForTesting(Profile profile) {
+        List<AndroidBrowserWindow> windows = new ArrayList<>();
+        for (var internalActivityScopedObjects : mActivityScopedObjectsDeque) {
+            var browserWindow = internalActivityScopedObjects.mAndroidBrowserWindows.get(profile);
+            if (browserWindow != null) {
+                windows.add(browserWindow);
+            }
+        }
+        if (mPendingBrowserWindow != null && mPendingBrowserWindow.getProfile() == profile) {
+            windows.add(mPendingBrowserWindow);
+        }
+        return windows;
+    }
+
+    /**
+     * Returns the first {@link AndroidBrowserWindow} from the top of {@link
+     * mActivityScopedObjectsDeque} that matches the given {@link Profile}, or null if no such
+     * {@link AndroidBrowserWindow} exists.
+     */
+    @Nullable
+    private AndroidBrowserWindow getTopmostWindowWithProfile(Profile profile) {
+        for (var internalActivityScopedObjects : mActivityScopedObjectsDeque) {
+            var browserWindow = internalActivityScopedObjects.mAndroidBrowserWindows.get(profile);
+            if (browserWindow != null) {
+                return browserWindow;
+            }
+        }
+        if (mPendingBrowserWindow != null && mPendingBrowserWindow.getProfile() == profile) {
+            return mPendingBrowserWindow;
+        }
+        return null;
+    }
+
     private void addActivityScopedObjectsInternal(ActivityScopedObjects activityScopedObjects) {
         assertPendingCreateOrIdle();
 
-        // Unregister all listeners for the current top Activity.
-        unregisterListenersForTopActivity();
-
-        // If the ActivityScopedObjects to be added already exists, remove it first.
-        removeActivityScopedObjectsInternal(activityScopedObjects.mActivityWindowAndroid);
-
+        // Get everything we need from the ActivityScopedObjects to be added.
         var activityWindowAndroid = activityScopedObjects.mActivityWindowAndroid;
+        var tabModel = activityScopedObjects.mTabModelSelector.getCurrentModel();
+        var profile = tabModel.getProfile();
+
+        // Precondition checks.
+        assert profile != null;
         if (mState == State.IDLE) {
             assert mId != null;
             assert mId == getActivity(activityWindowAndroid).getTaskId()
@@ -1101,8 +1281,53 @@
             assert mId == null;
         }
 
-        // Add the ActivityScopedObjects and register listeners.
-        mActivityScopedObjectsDeque.addFirst(activityScopedObjects);
+        // Unregister all listeners for the current top Activity.
+        // This must be done before changing mActivityScopedObjectsDeque.
+        unregisterListenersForTopActivity();
+
+        // See if the ActivityScopedObjects to be added already exists.
+        var existingInternalActivityScopedObjects =
+                findInternalActivityScopedObjects(activityWindowAndroid);
+
+        if (existingInternalActivityScopedObjects != null) {
+            // If the ActivityScopedObjects to be added already exists,
+            // move it to the top of the deque.
+            mActivityScopedObjectsDeque.remove(existingInternalActivityScopedObjects);
+            mActivityScopedObjectsDeque.addFirst(existingInternalActivityScopedObjects);
+        } else {
+            // Create a new AndroidBrowserWindow.
+            AndroidBrowserWindow newBrowserWindow;
+            if (mState == State.PENDING_CREATE) {
+                assert mActivityScopedObjectsDeque.isEmpty() && mPendingBrowserWindow != null
+                        : "addActivityScopedObjects() called in an invalid state.";
+                assert mPendingBrowserWindow.getProfile() == profile;
+                newBrowserWindow = mPendingBrowserWindow;
+                newBrowserWindow.setActivityWindowAndroid(activityWindowAndroid);
+                mPendingBrowserWindow = null;
+            } else {
+                newBrowserWindow =
+                        new AndroidBrowserWindow(
+                                /* chromeAndroidTask= */ this, profile, activityWindowAndroid);
+            }
+
+            // Associate the new AndroidBrowserWindow with TabModel.
+            tabModel.associateWithBrowserWindow(newBrowserWindow.getOrCreateNativePtr());
+
+            // Create a new InternalActivityScopedObjects instance, and
+            // add it to the top of the deque.
+            var internalActivityScopedObjects =
+                    new InternalActivityScopedObjects(activityScopedObjects, newBrowserWindow);
+            mActivityScopedObjectsDeque.addFirst(internalActivityScopedObjects);
+
+            // Notify observers of new window creation.
+            long ptr = newBrowserWindow.getOrCreateNativePtr();
+            for (var observer : mAndroidBrowserWindowObservers) {
+                observer.onBrowserWindowAdded(ptr);
+            }
+        }
+
+        // By this point, mActivityScopedObjectsDeque has been correctly
+        // updated. Register listeners for its current top Activity.
         registerListenersForTopActivity();
 
         // Cache the maximize bound.
@@ -1118,11 +1343,11 @@
     }
 
     private void registerListenersForTopActivity() {
-        var topActivityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
-        if (topActivityScopedObjects == null) {
+        var internalActivityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
+        if (internalActivityScopedObjects == null) {
             return;
         }
-
+        var topActivityScopedObjects = internalActivityScopedObjects.mActivityScopedObjects;
         var topActivityWindowAndroid = topActivityScopedObjects.mActivityWindowAndroid;
 
         // Register Activity LifecycleObservers
@@ -1151,20 +1376,9 @@
 
         TabModelSelector tabModelSelector = topActivityScopedObjects.mTabModelSelector;
         if (topActivityScopedObjects.mSupportedProfileType == SupportedProfileType.MIXED) {
-            // Associate regular model.
-            var regularModel = tabModelSelector.getModel(/* incognito= */ false);
-            associateTabModelWithBrowserWindow(regularModel);
-
-            // Associate incognito model if it exists, otherwise observe.
             var incognitoModel =
                     (IncognitoTabModel) tabModelSelector.getModel(/* incognito= */ true);
-            var incognitoProfile = incognitoModel.getProfile();
-            if (incognitoProfile != null) {
-                associateTabModelWithBrowserWindow(incognitoModel);
-            }
             incognitoModel.addIncognitoObserver(mIncognitoTabModelObserver);
-        } else {
-            associateTabModelWithBrowserWindow(tabModelSelector.getCurrentModel());
         }
 
         tabModelSelector
@@ -1180,33 +1394,12 @@
         mWindowStateManager.update(getActivity(topActivityWindowAndroid));
     }
 
-    /**
-     * Associates the given {@link TabModel} with the {@link AndroidBrowserWindow} for its {@link
-     * Profile} creating the browser window if it does not exist. *
-     *
-     * @param tabModel The {@link TabModel} to associate.
-     */
-    private void associateTabModelWithBrowserWindow(TabModel tabModel) {
-        var profile = tabModel.getProfile();
-        assert profile != null;
-        var browserWindow = mAndroidBrowserWindows.get(profile);
-        if (browserWindow == null) {
-            browserWindow = new AndroidBrowserWindow(this, profile);
-            mAndroidBrowserWindows.put(profile, browserWindow);
-            long ptr = browserWindow.getOrCreateNativePtr();
-            for (var observer : mAndroidBrowserWindowObservers) {
-                observer.onBrowserWindowAdded(ptr);
-            }
-        }
-        tabModel.associateWithBrowserWindow(browserWindow.getOrCreateNativePtr());
-    }
-
     private void unregisterListenersForTopActivity() {
-        var topActivityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
-        if (topActivityScopedObjects == null) {
+        var internalActivityScopedObjects = mActivityScopedObjectsDeque.peekFirst();
+        if (internalActivityScopedObjects == null) {
             return;
         }
-
+        var topActivityScopedObjects = internalActivityScopedObjects.mActivityScopedObjects;
         var topActivityWindowAndroid = topActivityScopedObjects.mActivityWindowAndroid;
 
         // Unregister Activity LifecycleObservers.
@@ -1234,10 +1427,6 @@
             tabModelSelector
                     .getCurrentTabModelSupplier()
                     .removeObserver(mOnTabModelSelectedCallback);
-
-            for (var tabModel : tabModelSelector.getModels()) {
-                tabModel.dissociateWithBrowserWindow();
-            }
         }
         getActivity(topActivityWindowAndroid)
                 .findViewById(android.R.id.content)
@@ -1306,23 +1495,38 @@
         }
     }
 
+    @Nullable
+    private InternalActivityScopedObjects findInternalActivityScopedObjects(
+            ActivityWindowAndroid activityWindowAndroid) {
+        InternalActivityScopedObjects result = null;
+        for (var internalActivityScopedObjects : mActivityScopedObjectsDeque) {
+            if (internalActivityScopedObjects.mActivityScopedObjects.mActivityWindowAndroid
+                    == activityWindowAndroid) {
+                assert result == null
+                        : "the same instance of ActivityScopedObjects was added more than once";
+                result = internalActivityScopedObjects;
+            }
+        }
+        return result;
+    }
+
     private void removeActivityScopedObjectsInternal(ActivityWindowAndroid activityWindowAndroid) {
-        if (mActivityScopedObjectsDeque.isEmpty()) {
+        var activityScopedObjectsToRemove =
+                findInternalActivityScopedObjects(activityWindowAndroid);
+        if (activityScopedObjectsToRemove == null) {
             return;
         }
 
-        ActivityScopedObjects activityScopedObjectsToRemove = null;
-        for (var activityScopedObjects : mActivityScopedObjectsDeque) {
-            if (activityScopedObjects.mActivityWindowAndroid == activityWindowAndroid) {
-                assert activityScopedObjectsToRemove == null
-                        : "the same instance of ActivityScopedObjects was added more than once";
-                activityScopedObjectsToRemove = activityScopedObjects;
-            }
-        }
-
-        if (activityScopedObjectsToRemove != null) {
-            mActivityScopedObjectsDeque.remove(activityScopedObjectsToRemove);
-            removeAllFeaturesForActivity(activityWindowAndroid);
+        // Remove from Deque.
+        mActivityScopedObjectsDeque.remove(activityScopedObjectsToRemove);
+        // Remove task features.
+        removeAllFeaturesForActivityInternal(activityWindowAndroid);
+        // Destroy associated windows.
+        var windows =
+                new ArrayList<>(activityScopedObjectsToRemove.mAndroidBrowserWindows.values());
+        for (var window : windows) {
+            destroyBrowserWindow(
+                    window, activityScopedObjectsToRemove, mAndroidBrowserWindowObservers);
         }
     }
 
@@ -1341,7 +1545,22 @@
 
     private void removeAllActivityScopedObjects() {
         unregisterListenersForTopActivity();
-        mActivityScopedObjectsDeque.clear();
+
+        while (!mActivityScopedObjectsDeque.isEmpty()) {
+            var internalActivityScopedObjects = mActivityScopedObjectsDeque.pollFirst();
+            removeAllFeaturesForActivityInternal(
+                    internalActivityScopedObjects.mActivityScopedObjects.mActivityWindowAndroid);
+            var windows =
+                    new ArrayList<>(internalActivityScopedObjects.mAndroidBrowserWindows.values());
+            for (var window : windows) {
+                destroyBrowserWindow(
+                        window, internalActivityScopedObjects, mAndroidBrowserWindowObservers);
+            }
+        }
+        if (mPendingBrowserWindow != null) {
+            destroyBrowserWindow(mPendingBrowserWindow, null, mAndroidBrowserWindowObservers);
+            mPendingBrowserWindow = null;
+        }
     }
 
     private void useActivity(ActivityUpdater updater) {
@@ -1365,6 +1584,18 @@
         mFeatures.clear();
     }
 
+    private void destroyFeaturesForProfile(Profile profile) {
+        var iterator = mFeatures.entrySet().iterator();
+        while (iterator.hasNext()) {
+            var entry = iterator.next();
+            ChromeAndroidTaskFeatureKey key = entry.getKey();
+            if (profile.equals(key.mProfile)) {
+                entry.getValue().onFeatureRemoved();
+                iterator.remove();
+            }
+        }
+    }
+
     private Rect getCurrentBoundsInDp(TopActivityScopedObjects topActivityScopedObjects) {
         Rect boundsInPx = getCurrentBoundsInPx(topActivityScopedObjects);
         return convertBoundsInPxToDp(
@@ -1390,7 +1621,7 @@
 
     private void assertPendingCreateOrIdle() {
         assert mState == State.IDLE || mState == State.PENDING_CREATE
-                : "This Task is neither pending create nor idle.";
+                : "This Task is neither pending create nor idle. Current state: " + mState;
     }
 
     private static boolean isActiveInternal(TopActivityScopedObjects topActivityScopedObjects) {
diff --git a/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskImplUnitTest.java b/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskImplUnitTest.java
index 6c4a98a..6dffdfd 100644
--- a/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskImplUnitTest.java
+++ b/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskImplUnitTest.java
@@ -12,6 +12,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -117,6 +118,11 @@
 
     @SuppressLint("NewApi" /* @Config already specifies the required SDK */)
     private static ActivityScopedObjects createActivityScopedObjects(int taskId) {
+        return createActivityScopedObjects(taskId, mock(Profile.class));
+    }
+
+    @SuppressLint("NewApi" /* @Config already specifies the required SDK */)
+    private static ActivityScopedObjects createActivityScopedObjects(int taskId, Profile profile) {
         var activityWindowAndroidMocks =
                 ChromeAndroidTaskUnitTestSupport.createActivityWindowAndroidMocks(taskId);
         ChromeAndroidTaskUnitTestSupport.mockDesktopWindowingMode(activityWindowAndroidMocks);
@@ -126,7 +132,7 @@
         ApplicationStatus.onStateChangeForTesting(mockActivity, ActivityState.CREATED);
         ApplicationStatus.onStateChangeForTesting(mockActivity, ActivityState.RESUMED);
         return ChromeAndroidTaskUnitTestSupport.createMockActivityScopedObjects(
-                activityWindowAndroidMocks.mMockActivityWindowAndroid, mock(Profile.class));
+                activityWindowAndroidMocks.mMockActivityWindowAndroid, profile);
     }
 
     private static void assertListenersRegisteredForActivity(
@@ -327,7 +333,6 @@
         chromeAndroidTask.addActivityScopedObjects(activityScopedObjects2);
 
         // Assert.
-        verify(tabModel1).dissociateWithBrowserWindow();
         assertListenersUnregisteredForActivity(chromeAndroidTask, activityScopedObjects1);
         assertListenersRegisteredForActivity(chromeAndroidTask, activityScopedObjects2);
     }
@@ -356,6 +361,44 @@
 
     @Test
     public void
+            addActivityScopedObjects_sameActivityScopedObjectsExists_doesNotRecreateBrowserWindow() {
+        // Arrange: Create a task and its initial ActivityScopedObjects.
+        int taskId = 1;
+        var chromeAndroidTaskWithMockDeps = createChromeAndroidTaskWithMockDeps(taskId);
+        var chromeAndroidTask =
+                (ChromeAndroidTaskImpl) chromeAndroidTaskWithMockDeps.mChromeAndroidTask;
+        var activityScopedObjects = chromeAndroidTaskWithMockDeps.mActivityScopedObjects;
+
+        // Arrange: Add an observer to track window lifecycle events.
+        var observer = mock(AndroidBrowserWindowObserver.class);
+        chromeAndroidTask.addAndroidBrowserWindowObserver(observer);
+
+        // Act: Add the SAME ActivityScopedObjects instance again.
+        // This is a common occurrence when an Activity is brought to the foreground
+        // and its references are refreshed in the Task tracker.
+        chromeAndroidTask.addActivityScopedObjects(activityScopedObjects);
+
+        // Assert:
+        verify(
+                        observer,
+                        never().description(
+                                        "Window should not be removed when re-adding the same"
+                                                + " activity"))
+                .onBrowserWindowRemoved(any(Long.class));
+        verify(
+                        observer,
+                        never().description(
+                                        "A new window should not be added when re-adding the same"
+                                                + " activity"))
+                .onBrowserWindowAdded(any(Long.class));
+
+        // Final check: Ensure the task still has exactly 1 window and 1 activity.
+        assertEquals(1, chromeAndroidTask.getActivityScopedObjectsListForTesting().size());
+        assertEquals(1, chromeAndroidTask.getAllNativeBrowserWindowPtrs().size());
+    }
+
+    @Test
+    public void
             addActivityScopedObjects_sameActivityScopedObjectsExists_movesListenersToNewTopActivity() {
         // Arrange: Add the 1st instance of ActivityScopedObjects.
         int taskId = 1;
@@ -617,21 +660,19 @@
         var activityScopedObjects2 = createActivityScopedObjects(taskId);
         chromeAndroidTask.addActivityScopedObjects(activityScopedObjects2);
         assertListenersUnregisteredForActivity(
-                chromeAndroidTask,
-                activityScopedObjects1,
-                /* expectedNumberOfInvocations= */ 1);
+                chromeAndroidTask, activityScopedObjects1, /* expectedNumberOfInvocations= */ 1);
         assertListenersRegisteredForActivity(
-                chromeAndroidTask,
-                activityScopedObjects2,
-                /* expectedNumberOfInvocations= */ 1);
+                chromeAndroidTask, activityScopedObjects2, /* expectedNumberOfInvocations= */ 1);
+        List<ActivityScopedObjects> activityScopedObjectsList =
+                chromeAndroidTask.getActivityScopedObjectsListForTesting();
+        assertEquals(2, activityScopedObjectsList.size());
 
         // Act: Remove activityScopedObjects1, which doesn't represent the top Activity.
         chromeAndroidTask.removeActivityScopedObjects(
                 activityScopedObjects1.mActivityWindowAndroid);
 
         // Assert: activityScopedObjects1 is removed.
-        List<ActivityScopedObjects> activityScopedObjectsList =
-                chromeAndroidTask.getActivityScopedObjectsListForTesting();
+        activityScopedObjectsList = chromeAndroidTask.getActivityScopedObjectsListForTesting();
         assertEquals(1, activityScopedObjectsList.size());
         assertEquals(activityScopedObjects2, activityScopedObjectsList.get(0));
 
@@ -659,6 +700,132 @@
     }
 
     @Test
+    public void removeActivityScopedObjects_mixedProfile_destroysBothRegularAndIncognitoWindows() {
+        // Arrange: Create Task with Mixed Profile support.
+        var chromeAndroidTaskWithMockDeps =
+                ChromeAndroidTaskUnitTestSupport.createChromeAndroidTaskWithMockDeps(
+                        /* taskId= */ 1,
+                        /* mockNatives= */ true,
+                        /* isPendingTask= */ false,
+                        /* isDesktopMode= */ true,
+                        SupportedProfileType.MIXED);
+        var chromeAndroidTask =
+                (ChromeAndroidTaskImpl) chromeAndroidTaskWithMockDeps.mChromeAndroidTask;
+        var activityScopedObjects = chromeAndroidTaskWithMockDeps.mActivityScopedObjects;
+        var tabModelSelector = activityScopedObjects.mTabModelSelector;
+        var activityWindowAndroid = activityScopedObjects.mActivityWindowAndroid;
+
+        // Simulate Incognito creation to force a second window/deque entry.
+        var incognitoModel = (IncognitoTabModel) tabModelSelector.getModel(true);
+        var incognitoProfile = mock(Profile.class);
+        when(incognitoProfile.isOffTheRecord()).thenReturn(true);
+        when(incognitoModel.getProfile()).thenReturn(incognitoProfile);
+
+        // Trigger the observer to create the incognito window
+        ArgumentCaptor<IncognitoTabModelObserver> captor =
+                ArgumentCaptor.forClass(IncognitoTabModelObserver.class);
+        verify(incognitoModel).addIncognitoObserver(captor.capture());
+        captor.getValue().onIncognitoModelCreated();
+
+        // Pre-assertion: We should have 2 native pointers (Regular + Incognito).
+        assertEquals(2, chromeAndroidTask.getAllNativeBrowserWindowPtrs().size());
+
+        // Act: Remove the SINGLE ActivityScopedObjects.
+        chromeAndroidTask.removeActivityScopedObjects(activityWindowAndroid);
+
+        // Assert: Both windows should be destroyed.
+        assertEquals(0, chromeAndroidTask.getAllNativeBrowserWindowPtrs().size());
+
+        // Verify native destroy was called for both.
+        verify(chromeAndroidTaskWithMockDeps.mMockAndroidBrowserWindowNatives, times(1))
+                .destroy(ChromeAndroidTaskUnitTestSupport.FAKE_NATIVE_ANDROID_BROWSER_WINDOW_PTR);
+        verify(chromeAndroidTaskWithMockDeps.mMockAndroidBrowserWindowNatives, times(1))
+                .destroy(
+                        ChromeAndroidTaskUnitTestSupport
+                                .FAKE_INCOGNITO_NATIVE_ANDROID_BROWSER_WINDOW_PTR);
+    }
+
+    @Test
+    public void removeActivityScopedObjects_multipleActivities_destroysOnlyRemovedActivityWindow() {
+        // Arrange: Add Activity 1
+        int taskId = 1;
+        var chromeAndroidTaskWithMockDeps = createChromeAndroidTaskWithMockDeps(taskId);
+        var chromeAndroidTask =
+                (ChromeAndroidTaskImpl) chromeAndroidTaskWithMockDeps.mChromeAndroidTask;
+        var profile = chromeAndroidTaskWithMockDeps.mMockProfile;
+        var mockNatives = chromeAndroidTaskWithMockDeps.mMockAndroidBrowserWindowNatives;
+
+        // Arrange: Add Activity 2 to the same task using the same profile
+        var activityScopedObjects2 = createActivityScopedObjects(taskId, profile);
+        chromeAndroidTask.addActivityScopedObjects(activityScopedObjects2);
+
+        // We now have 2 ActivityScopedObjects wrappers, each with its own AndroidBrowserWindow
+        assertEquals(2, chromeAndroidTask.getAllNativeBrowserWindowPtrs().size());
+
+        // Grab the specific Java window objects so we can verify which one survives
+        var window1 =
+                chromeAndroidTask.getBrowserWindowsForTesting(profile).get(1); // older (Activity 1)
+
+        // Act: Remove Activity 2
+        chromeAndroidTask.removeActivityScopedObjects(
+                activityScopedObjects2.mActivityWindowAndroid);
+
+        // Assert:
+        // 1. Exactly one window was destroyed natively. (Both windows share the same mock native
+        //    pointer, so we verify the destroy method was invoked exactly 1 time in total).
+        verify(mockNatives, times(1))
+                .destroy(ChromeAndroidTaskUnitTestSupport.FAKE_NATIVE_ANDROID_BROWSER_WINDOW_PTR);
+
+        // 2. Only Activity 1's window remains tracked in the Java layer.
+        var remainingWindows = chromeAndroidTask.getBrowserWindowsForTesting(profile);
+        assertEquals(1, remainingWindows.size());
+        assertEquals(
+                "Activity 1's window should be the surviving window",
+                window1,
+                remainingWindows.get(0));
+    }
+
+    @Test
+    public void removeActivityScopedObjects_destroysBrowserWindow() {
+        // Arrange.
+        var chromeAndroidTaskWithMockDeps = createChromeAndroidTaskWithMockDeps(/* taskId= */ 1);
+        var chromeAndroidTask =
+                (ChromeAndroidTaskImpl) chromeAndroidTaskWithMockDeps.mChromeAndroidTask;
+        var profile = chromeAndroidTaskWithMockDeps.mMockProfile;
+        long nativePtr = chromeAndroidTask.getOrCreateNativeBrowserWindowPtr(profile);
+        var observer = mock(AndroidBrowserWindowObserver.class);
+        chromeAndroidTask.addAndroidBrowserWindowObserver(observer);
+        var activityScopedObjects = chromeAndroidTaskWithMockDeps.mActivityScopedObjects;
+
+        // Act.
+        chromeAndroidTask.removeActivityScopedObjects(activityScopedObjects.mActivityWindowAndroid);
+
+        // Assert.
+        verify(observer, times(1)).onBrowserWindowRemoved(nativePtr);
+        verify(chromeAndroidTaskWithMockDeps.mMockAndroidBrowserWindowNatives, times(1))
+                .destroy(nativePtr);
+    }
+
+    @Test
+    public void removeActivityScopedObjects_dissociatesTabModelBeforeDestroyingNativeWindow() {
+        // Arrange
+        var chromeAndroidTaskWithMockDeps = createChromeAndroidTaskWithMockDeps(1);
+        var chromeAndroidTask =
+                (ChromeAndroidTaskImpl) chromeAndroidTaskWithMockDeps.mChromeAndroidTask;
+        var activityScopedObjects = chromeAndroidTaskWithMockDeps.mActivityScopedObjects;
+        var mockTabModel = activityScopedObjects.mTabModelSelector.getCurrentModel();
+        var mockNatives = chromeAndroidTaskWithMockDeps.mMockAndroidBrowserWindowNatives;
+
+        // Act
+        chromeAndroidTask.removeActivityScopedObjects(activityScopedObjects.mActivityWindowAndroid);
+
+        // Assert: Strict ordering
+        InOrder inOrder = inOrder(mockTabModel, mockNatives);
+        inOrder.verify(mockTabModel).dissociateWithBrowserWindow();
+        inOrder.verify(mockNatives).destroy(any(Long.class));
+    }
+
+    @Test
     public void addFeature_addsFeatureToInternalFeatureMap() {
         // Arrange.
         var chromeAndroidTaskWithMockDeps = createChromeAndroidTaskWithMockDeps(/* taskId= */ 1);
@@ -2741,7 +2908,8 @@
         chromeAndroidTask.minimize();
         // Arrange: Setup ActivityScopedObjects.
         int taskId = 2;
-        var activityScopedObjects = createActivityScopedObjects(taskId);
+        var profile = chromeAndroidTaskWithMockDeps.mMockProfile;
+        var activityScopedObjects = createActivityScopedObjects(taskId, profile);
         var mockActivity = activityScopedObjects.mActivityWindowAndroid.getActivity().get();
 
         // Act.
@@ -2864,7 +3032,8 @@
 
         // Arrange: Setup ActivityScopedObjects.
         int taskId = 2;
-        var activityScopedObjects = createActivityScopedObjects(taskId);
+        var profile = pendingTaskInfo.mCreateParams.getProfile();
+        var activityScopedObjects = createActivityScopedObjects(taskId, profile);
 
         // Act.
         task.addActivityScopedObjects(activityScopedObjects);
@@ -3102,6 +3271,105 @@
     }
 
     @Test
+    public void onProfileDestroyed_mixedProfile_removesOnlyDestroyedProfileWindow() {
+        assumeFalse(BuildConfig.IS_DESKTOP_ANDROID);
+
+        // Arrange: Create Task with Mixed Profile support.
+        var chromeAndroidTaskWithMockDeps =
+                ChromeAndroidTaskUnitTestSupport.createChromeAndroidTaskWithMockDeps(
+                        /* taskId= */ 1,
+                        /* mockNatives= */ true,
+                        /* isPendingTask= */ false,
+                        /* isDesktopMode= */ true,
+                        SupportedProfileType.MIXED);
+        var chromeAndroidTask =
+                (ChromeAndroidTaskImpl) chromeAndroidTaskWithMockDeps.mChromeAndroidTask;
+        var tabModelSelector =
+                chromeAndroidTaskWithMockDeps.mActivityScopedObjects.mTabModelSelector;
+        var mockNatives = chromeAndroidTaskWithMockDeps.mMockAndroidBrowserWindowNatives;
+
+        // Simulate Incognito creation.
+        var incognitoModel = (IncognitoTabModel) tabModelSelector.getModel(true);
+        var incognitoProfile = mock(Profile.class, "IncognitoProfile");
+        when(incognitoProfile.isOffTheRecord()).thenReturn(true);
+        when(incognitoModel.getProfile()).thenReturn(incognitoProfile);
+
+        ArgumentCaptor<IncognitoTabModelObserver> captor =
+                ArgumentCaptor.forClass(IncognitoTabModelObserver.class);
+        verify(incognitoModel).addIncognitoObserver(captor.capture());
+        captor.getValue().onIncognitoModelCreated();
+
+        assertEquals(
+                "Both regular and incognito windows should exist",
+                2,
+                chromeAndroidTask.getAllNativeBrowserWindowPtrs().size());
+        assertEquals(
+                "There should be 1 ActivityScopedObjects wrapping both windows",
+                1,
+                chromeAndroidTask.getActivityScopedObjectsListForTesting().size());
+
+        // Act: Destroy ONLY the Incognito profile.
+        ProfileManager.onProfileDestroyed(incognitoProfile);
+
+        // Assert:
+        // 1. Incognito window was destroyed.
+        verify(mockNatives, times(1))
+                .destroy(
+                        ChromeAndroidTaskUnitTestSupport
+                                .FAKE_INCOGNITO_NATIVE_ANDROID_BROWSER_WINDOW_PTR);
+
+        // 2. Regular window was NOT destroyed.
+        verify(mockNatives, never())
+                .destroy(ChromeAndroidTaskUnitTestSupport.FAKE_NATIVE_ANDROID_BROWSER_WINDOW_PTR);
+
+        // 3. The ActivityScopedObjects wrapper should STILL exist because the regular window is
+        // still alive.
+        assertEquals(
+                "The Activity wrapper should not have been removed",
+                1,
+                chromeAndroidTask.getActivityScopedObjectsListForTesting().size());
+
+        // 4. Only 1 native pointer (the regular one) should remain tracked.
+        assertEquals(1, chromeAndroidTask.getAllNativeBrowserWindowPtrs().size());
+    }
+
+    @Test
+    public void onProfileDestroyed_whenTaskIsPendingCreate_destroysPendingBrowserWindow() {
+        // TODO(crbug.com/479566813): Re-enable for Desktop Android when fixed.
+        assumeFalse(BuildConfig.IS_DESKTOP_ANDROID);
+
+        // Arrange: Creating a pending task requires an existing task to generate the Intent.
+        createChromeAndroidTaskWithMockDeps(/* taskId= */ 1);
+
+        // Arrange: Create the pending task.
+        var pendingTaskWithDeps =
+                createChromeAndroidTaskWithMockDeps(/* taskId= */ 2, /* isPendingTask= */ true);
+        var pendingTask = (ChromeAndroidTaskImpl) pendingTaskWithDeps.mChromeAndroidTask;
+        var profile = pendingTaskWithDeps.mMockProfile;
+        var mockNatives = pendingTaskWithDeps.mMockAndroidBrowserWindowNatives;
+
+        assertEquals(
+                "Pending task should track exactly 1 native window pointer",
+                1,
+                pendingTask.getAllNativeBrowserWindowPtrs().size());
+
+        // Extract the created native pointer to verify it gets destroyed.
+        long pendingWindowPtr = pendingTask.getAllNativeBrowserWindowPtrs().get(0);
+
+        // Act: Destroy the profile before the task attaches to an Activity.
+        ProfileManager.onProfileDestroyed(profile);
+
+        // Assert: The pending window should be destroyed and cleared.
+        assertEquals(
+                "Pending window should be cleared",
+                0,
+                pendingTask.getAllNativeBrowserWindowPtrs().size());
+
+        // Verify native destroy was actually called.
+        verify(mockNatives, times(1)).destroy(pendingWindowPtr);
+    }
+
+    @Test
     public void addActivityScopedObjects_invokesOnTabModelSelectedOnFeatures() throws Exception {
         // Arrange.
         int taskId = 1;
diff --git a/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskTrackerImpl.java b/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskTrackerImpl.java
index 5d3ca0c8..9c1c15e 100644
--- a/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskTrackerImpl.java
+++ b/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskTrackerImpl.java
@@ -247,15 +247,16 @@
      * <p>This method must be called on the UI thread.
      */
     void removeAllForTesting() {
-        ThreadUtils.assertOnUiThread();
-        for (var task : mTasks.values()) {
-            task.destroy();
-        }
+        List<ChromeAndroidTask> tasks = new ArrayList<>(mTasks.values());
         mTasks.clear();
-        for (var task : mPendingTasks.values()) {
+        for (var task : tasks) {
             task.destroy();
         }
+        List<ChromeAndroidTask> pendingTasks = new ArrayList<>(mPendingTasks.values());
         mPendingTasks.clear();
+        for (var task : pendingTasks) {
+            task.destroy();
+        }
     }
 
     boolean hasObserverForTesting(ChromeAndroidTaskTrackerObserver observer) {
diff --git a/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskTrackerImplUnitTest.java b/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskTrackerImplUnitTest.java
index a88d80d2..fdc0737e 100644
--- a/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskTrackerImplUnitTest.java
+++ b/chrome/browser/ui/browser_window/internal/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskTrackerImplUnitTest.java
@@ -347,7 +347,8 @@
 
         int taskId = IdSequencer.next();
         var activityScopedObjects =
-                ChromeAndroidTaskUnitTestSupport.createMockActivityScopedObjects(taskId);
+                ChromeAndroidTaskUnitTestSupport.createMockActivityScopedObjects(
+                        taskId, mockParams.getProfile());
 
         // Act.
         var task =
@@ -835,7 +836,8 @@
         mFakeTime.advanceMillis(100);
 
         var newActivityScopedObjects =
-                ChromeAndroidTaskUnitTestSupport.createMockActivityScopedObjects(/* taskId= */ 2);
+                ChromeAndroidTaskUnitTestSupport.createMockActivityScopedObjects(
+                        /* taskId= */ 2, mockParams.getProfile());
         var newTask =
                 (ChromeAndroidTaskImpl)
                         mChromeAndroidTaskTracker.obtainTask(
diff --git a/chrome/browser/ui/browser_window/test/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBaseWindowNativeUnitTestSupport.java b/chrome/browser/ui/browser_window/test/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBaseWindowNativeUnitTestSupport.java
index 49a3e390..044eb4e 100644
--- a/chrome/browser/ui/browser_window/test/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBaseWindowNativeUnitTestSupport.java
+++ b/chrome/browser/ui/browser_window/test/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBaseWindowNativeUnitTestSupport.java
@@ -14,6 +14,8 @@
 import org.jni_zero.CalledByNative;
 
 import org.chromium.build.annotations.NullMarked;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.ui.base.ActivityWindowAndroid;
 
 /**
  * Supports {@code android_base_window_unittest.cc}.
@@ -28,12 +30,16 @@
 @NullMarked
 final class AndroidBaseWindowNativeUnitTestSupport {
     private final AndroidBaseWindow mAndroidBaseWindow;
+    private final AndroidBrowserWindow mAndroidBrowserWindow;
     private final ChromeAndroidTask mChromeAndroidTask;
 
     @CalledByNative
     private AndroidBaseWindowNativeUnitTestSupport() {
         mChromeAndroidTask = mock(ChromeAndroidTask.class);
-        mAndroidBaseWindow = new AndroidBaseWindow(mChromeAndroidTask);
+        mAndroidBrowserWindow =
+                new AndroidBrowserWindow(
+                        mChromeAndroidTask, mock(Profile.class), mock(ActivityWindowAndroid.class));
+        mAndroidBaseWindow = new AndroidBaseWindow(mAndroidBrowserWindow);
     }
 
     @CalledByNative
diff --git a/chrome/browser/ui/browser_window/test/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBrowserWindowNativeUnitTestSupport.java b/chrome/browser/ui/browser_window/test/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBrowserWindowNativeUnitTestSupport.java
index f5e507d..ed142b37 100644
--- a/chrome/browser/ui/browser_window/test/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBrowserWindowNativeUnitTestSupport.java
+++ b/chrome/browser/ui/browser_window/test/android/java/src/org/chromium/chrome/browser/ui/browser_window/AndroidBrowserWindowNativeUnitTestSupport.java
@@ -12,6 +12,7 @@
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.profiles.ProfileManager;
+import org.chromium.ui.base.ActivityWindowAndroid;
 
 /**
  * Supports {@code android_browser_window_unittest.cc}.
@@ -38,7 +39,9 @@
             @BrowserWindowType int browserWindowType, Profile profile) {
         mMockChromeAndroidTask = mock(ChromeAndroidTask.class);
         when(mMockChromeAndroidTask.getBrowserWindowType()).thenReturn(browserWindowType);
-        mAndroidBrowserWindow = new AndroidBrowserWindow(mMockChromeAndroidTask, profile);
+        mAndroidBrowserWindow =
+                new AndroidBrowserWindow(
+                        mMockChromeAndroidTask, profile, mock(ActivityWindowAndroid.class));
 
         ProfileManager.setLastUsedProfileForTesting(profile);
     }
diff --git a/chrome/browser/ui/browser_window/test/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskUnitTestSupport.java b/chrome/browser/ui/browser_window/test/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskUnitTestSupport.java
index 4b82802..8cdba95 100644
--- a/chrome/browser/ui/browser_window/test/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskUnitTestSupport.java
+++ b/chrome/browser/ui/browser_window/test/android/java/src/org/chromium/chrome/browser/ui/browser_window/ChromeAndroidTaskUnitTestSupport.java
@@ -262,7 +262,12 @@
         var chromeAndroidTask =
                 isPendingTask
                         ? chromeAndroidTaskTracker.createPendingTask(
-                                createPendingTaskInfo().mCreateParams, null)
+                                createMockAndroidBrowserWindowCreateParams(
+                                        BrowserWindowType.NORMAL,
+                                        new Rect(),
+                                        WindowShowState.DEFAULT,
+                                        profile),
+                                null)
                         : chromeAndroidTaskTracker.obtainTask(
                                 BrowserWindowType.NORMAL,
                                 activityScopedObjects,
@@ -486,7 +491,7 @@
     }
 
     /**
-     * @see #createMockAndroidBrowserWindowCreateParams(int, Rect, int)
+     * @see #createMockAndroidBrowserWindowCreateParams(int, Rect, int, Profile)
      */
     static AndroidBrowserWindowCreateParams createMockAndroidBrowserWindowCreateParams() {
         return createMockAndroidBrowserWindowCreateParams(
@@ -494,7 +499,7 @@
     }
 
     /**
-     * @see #createMockAndroidBrowserWindowCreateParams(int, Rect, int)
+     * @see #createMockAndroidBrowserWindowCreateParams(int, Rect, int, Profile)
      */
     static AndroidBrowserWindowCreateParams createMockAndroidBrowserWindowCreateParams(
             @BrowserWindowType int windowType) {
@@ -503,21 +508,33 @@
     }
 
     /**
-     * Creates an {@link AndroidBrowserWindowCreateParams} mock.
-     *
-     * @param windowType The mock {@link BrowserWindowType} to set in the create params.
-     * @param launchBounds The launch bounds to set in the create params.
-     * @param showState The mock {@link WindowShowState} to set in the create params.
-     * @return The {@link AndroidBrowserWindowCreateParams} mock.
+     * @see #createMockAndroidBrowserWindowCreateParams(int, Rect, int, Profile)
      */
     static AndroidBrowserWindowCreateParams createMockAndroidBrowserWindowCreateParams(
             @BrowserWindowType int windowType,
             Rect launchBounds,
             @WindowShowState.EnumType int showState) {
+        return createMockAndroidBrowserWindowCreateParams(
+                windowType, launchBounds, showState, mock(Profile.class));
+    }
+
+    /**
+     * Creates an {@link AndroidBrowserWindowCreateParams} mock.
+     *
+     * @param windowType The mock {@link BrowserWindowType} to set in the create params.
+     * @param launchBounds The launch bounds to set in the create params.
+     * @param showState The mock {@link WindowShowState} to set in the create params.
+     * @param profile The {@link Profile} to set in the create params.
+     * @return The {@link AndroidBrowserWindowCreateParams} mock.
+     */
+    static AndroidBrowserWindowCreateParams createMockAndroidBrowserWindowCreateParams(
+            @BrowserWindowType int windowType,
+            Rect launchBounds,
+            @WindowShowState.EnumType int showState,
+            Profile profile) {
         var mockParams = mock(AndroidBrowserWindowCreateParams.class);
         when(mockParams.getWindowType()).thenReturn(windowType);
-        Profile mockProfile = mock(Profile.class);
-        when(mockParams.getProfile()).thenReturn(mockProfile);
+        when(mockParams.getProfile()).thenReturn(profile);
         when(mockParams.getInitialBoundsInDp()).thenReturn(launchBounds);
         when(mockParams.getInitialShowState()).thenReturn(showState);
         mockPopupIntentCreator();
diff --git a/chrome/browser/ui/extensions/BUILD.gn b/chrome/browser/ui/extensions/BUILD.gn
index 8885611..9ed9419 100644
--- a/chrome/browser/ui/extensions/BUILD.gn
+++ b/chrome/browser/ui/extensions/BUILD.gn
@@ -337,7 +337,6 @@
       "//chrome/browser/search_engines",
       "//chrome/browser/themes",
       "//chrome/browser/ui/color:color_headers",
-      "//chrome/browser/ui/tabs:tab_strip",
       "//chrome/browser/ui/views/toolbar",
       "//chrome/browser/ui/web_applications",
       "//chrome/browser/ui/web_applications:launch_utils",
diff --git a/chrome/browser/ui/lens/lens_overlay_controller.cc b/chrome/browser/ui/lens/lens_overlay_controller.cc
index 065a295..e0d37b9 100644
--- a/chrome/browser/ui/lens/lens_overlay_controller.cc
+++ b/chrome/browser/ui/lens/lens_overlay_controller.cc
@@ -1490,6 +1490,11 @@
 
 void LensOverlayController::NotifyTabWillEnterBackground() {
   UpdateEntryPointsState();
+  if (auto* interface = BrowserUserEducationInterface::From(
+          tab_->GetBrowserWindowInterface())) {
+    interface->AbortFeaturePromo(
+        feature_engagement::kIPHiOSLensPromoDesktopFeature);
+  }
 }
 
 bool LensOverlayController::IsOverlayViewShared() const {
diff --git a/chrome/browser/ui/lens/overlay_base_controller.cc b/chrome/browser/ui/lens/overlay_base_controller.cc
index b35bf147..95b9592 100644
--- a/chrome/browser/ui/lens/overlay_base_controller.cc
+++ b/chrome/browser/ui/lens/overlay_base_controller.cc
@@ -128,6 +128,10 @@
     overlay_blur_layer_delegate_->layer()->SetBounds(
         overlay_view_->GetLocalBounds());
   }
+
+  if (promo_anchor_) {
+    promo_anchor_->SetBounds(0, 0, 1, 1);
+  }
 }
 
 #if BUILDFLAG(IS_MAC)
@@ -227,6 +231,14 @@
   anchor_view->SetFocusBehavior(views::View::FocusBehavior::NEVER);
   preselection_widget_anchor_ = host_view->AddChildView(std::move(anchor_view));
 
+  // Create top left anchor.
+  auto promo_anchor = std::make_unique<views::View>();
+  promo_anchor->SetProperty(views::kElementIdentifierKey,
+                            kIOSLensPromoAnchorElementId);
+  promo_anchor->SetCanProcessEventsWithinSubtree(false);
+  promo_anchor->SetProperty(views::kViewIgnoredByLayoutKey, true);
+  promo_anchor_ = host_view->AddChildView(std::move(promo_anchor));
+
   // Create the web view.
   std::unique_ptr<views::WebView> web_view = std::make_unique<views::WebView>(
       tab_->GetContents()->GetBrowserContext());
@@ -566,6 +578,7 @@
     overlay_view_->SetVisible(true);
     preselection_widget_anchor_->SetVisible(true);
     overlay_web_view_->SetVisible(true);
+    promo_anchor_->SetVisible(true);
     SetOverlayRoundedCorner();
 
     // Restart the live blur since the view is visible again.
@@ -643,6 +656,9 @@
   if (overlay_web_view_) {
     overlay_web_view_->SetVisible(false);
   }
+  if (promo_anchor_) {
+    promo_anchor_->SetVisible(false);
+  }
   MaybeHideSharedOverlayView();
 
   // Save the current value of whether live blur is enabled so that it can be
@@ -718,6 +734,7 @@
   if (overlay_view_) {
     overlay_view_->RemoveChildViewT(
         std::exchange(preselection_widget_anchor_, nullptr));
+    overlay_view_->RemoveChildViewT(std::exchange(promo_anchor_, nullptr));
     overlay_view_->RemoveChildViewT(std::exchange(overlay_web_view_, nullptr));
     MaybeHideSharedOverlayView();
     overlay_view_ = nullptr;
diff --git a/chrome/browser/ui/lens/overlay_base_controller.h b/chrome/browser/ui/lens/overlay_base_controller.h
index c1b984b9..232ebf7 100644
--- a/chrome/browser/ui/lens/overlay_base_controller.h
+++ b/chrome/browser/ui/lens/overlay_base_controller.h
@@ -390,6 +390,10 @@
   // Pointer to the web view within the overlay view if it exists.
   raw_ptr<views::WebView> overlay_web_view_;
 
+  // Pointer to the anchor view for the top left corner. Used to anchor the
+  // mobile Lens promo bubble.
+  raw_ptr<views::View> promo_anchor_ = nullptr;
+
   // Preselection toast bubble. Weak; owns itself. NULL when closed.
   raw_ptr<views::Widget> preselection_widget_ = nullptr;
 
diff --git a/chrome/browser/ui/read_anything/read_anything_entry_point_controller.cc b/chrome/browser/ui/read_anything/read_anything_entry_point_controller.cc
index 9e1921e..bdc06a8 100644
--- a/chrome/browser/ui/read_anything/read_anything_entry_point_controller.cc
+++ b/chrome/browser/ui/read_anything/read_anything_entry_point_controller.cc
@@ -28,8 +28,13 @@
 #include "components/prefs/pref_filter.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/common/content_switches.h"
+#include "pdf/buildflags.h"
 #include "ui/accessibility/accessibility_features.h"
 
+#if BUILDFLAG(ENABLE_PDF)
+#include "components/pdf/browser/pdf_document_helper.h"
+#endif  // BUILDFLAG(ENABLE_PDF)
+
 namespace {
 
 static const int kMaxChipIgnoredCount = 5;
@@ -66,10 +71,44 @@
          base::FeatureList::IsEnabled(features::kPageActionsMigration);
 }
 
+#if BUILDFLAG(ENABLE_PDF)
+size_t g_min_pdf_text_length_for_omnibox = 1100;
+constexpr float kMaxNonAlphaFraction = 0.33;
+
+bool IsMostlyAlphaChars(const std::u16string& text) {
+  long non_alpha_chars =
+      std::count_if(text.begin(), text.end(), [](char16_t c) {
+        // Check specifically for certain non-alphabetic characters rather than
+        // for alphabetic characters. IsAsciiAlpha is only true for certain
+        // scripts, so this avoids excluding other languages.
+        return base::IsAsciiPunctuation(c) || base::IsAsciiDigit(c) ||
+               base::IsWhitespace(c) || base::IsUnicodeControl(c);
+      });
+
+  return (static_cast<float>(non_alpha_chars) / text.size()) <
+         kMaxNonAlphaFraction;
+}
+
+void OnPdfTextReceived(base::OnceCallback<void(bool)> result_callback,
+                       const std::u16string& text) {
+  // Show the omnibox on PDFs above a certain length, with a high percentage of
+  // alphabetic characters. In this case, it is likely going to distill well in
+  // Reading mode.
+  std::move(result_callback)
+      .Run((text.length() > g_min_pdf_text_length_for_omnibox) &&
+           IsMostlyAlphaChars(text));
+}
+#endif
+
 }  // namespace
 
 namespace read_anything {
 
+base::AutoReset<size_t>
+ReadAnythingEntryPointController::SetMinPdfTextLengthForTesting(size_t length) {
+  return {&g_min_pdf_text_length_for_omnibox, length};
+}
+
 // static
 void ReadAnythingEntryPointController::InvokePageAction(
     BrowserWindowInterface* bwi,
@@ -265,9 +304,25 @@
     return;
   }
 
+  content::WebContents* contents = bwi->GetActiveTabInterface()->GetContents();
+
+#if BUILDFLAG(ENABLE_PDF)
+  // If this contents is a PDF, then Readability will always return false. But
+  // since PDFs are distilled via Screen2x, use our own heuristic to determine
+  // if the PDF will distill well with RM.
+  auto* pdf_helper = pdf::PDFDocumentHelper::MaybeGetForWebContents(contents);
+  if (pdf_helper) {
+    // Use the text on the first page of the document to estimate if this could
+    // be a distillable PDF.
+    pdf_helper->GetPageText(
+        /*page_index=*/0,
+        base::BindOnce(&OnPdfTextReceived, std::move(result_callback)));
+    return;
+  }
+#endif  // BUILDFLAG(ENABLE_PDF)
+
   // Readability will callback with whether or not the current contents are a
   // good candidate for distillation.
-  content::WebContents* contents = bwi->GetActiveTabInterface()->GetContents();
   RunReadabilityHeuristicsOnWebContents(contents, std::move(result_callback));
 }
 
diff --git a/chrome/browser/ui/read_anything/read_anything_entry_point_controller.h b/chrome/browser/ui/read_anything/read_anything_entry_point_controller.h
index c460828..0f8e49f 100644
--- a/chrome/browser/ui/read_anything/read_anything_entry_point_controller.h
+++ b/chrome/browser/ui/read_anything/read_anything_entry_point_controller.h
@@ -60,6 +60,8 @@
       BrowserWindowInterface* bwi,
       base::OnceCallback<void(bool)> result_callback);
 
+  static base::AutoReset<size_t> SetMinPdfTextLengthForTesting(size_t length);
+
  private:
   static void ToggleUI(BrowserWindowInterface* bwi,
                        ReadAnythingOpenTrigger open_trigger);
diff --git a/chrome/browser/ui/read_anything/read_anything_entry_point_controller_browsertest.cc b/chrome/browser/ui/read_anything/read_anything_entry_point_controller_browsertest.cc
index fde3f6f..1aeb65ef 100644
--- a/chrome/browser/ui/read_anything/read_anything_entry_point_controller_browsertest.cc
+++ b/chrome/browser/ui/read_anything/read_anything_entry_point_controller_browsertest.cc
@@ -9,7 +9,9 @@
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/run_until.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/test/test_future.h"
 #include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/pdf/pdf_extension_test_util.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_commands.h"
@@ -22,12 +24,14 @@
 #include "chrome/browser/ui/tabs/public/tab_features.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/ui_features.h"
+#include "chrome/browser/ui/views/frame/browser_view.h"
 #include "chrome/browser/ui/views/page_action/page_action_triggers.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/ui_test_utils.h"
 #include "components/prefs/pref_service.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
 #include "ui/accessibility/accessibility_features.h"
 #include "url/url_constants.h"
 
@@ -195,7 +199,10 @@
 class ReadAnythingEntryPointControllerOmniboxBrowserTest
     : public InProcessBrowserTest {
  public:
-  ReadAnythingEntryPointControllerOmniboxBrowserTest() = default;
+  ReadAnythingEntryPointControllerOmniboxBrowserTest()
+      : test_min_pdf_text_length_for_omnibox_(
+            read_anything::ReadAnythingEntryPointController::
+                SetMinPdfTextLengthForTesting(500)) {}
 
   void SetUp() override {
     scoped_feature_list_.InitWithFeatures(
@@ -206,6 +213,7 @@
 
  private:
   base::test::ScopedFeatureList scoped_feature_list_;
+  base::AutoReset<size_t> test_min_pdf_text_length_for_omnibox_;
 };
 
 IN_PROC_BROWSER_TEST_F(ReadAnythingEntryPointControllerOmniboxBrowserTest,
@@ -260,14 +268,69 @@
       browser(), GURL("https://www.google.com"),
       WindowOpenDisposition::NEW_FOREGROUND_TAB,
       ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
-  static bool called_back = false;
-  auto result_callback =
-      base::BindOnce([](bool is_good_candidate) { called_back = true; });
+  base::test::TestFuture<bool> future;
 
   read_anything::ReadAnythingEntryPointController::
-      CheckIfShouldSuggestReadingMode(browser(), std::move(result_callback));
+      CheckIfShouldSuggestReadingMode(browser(), future.GetCallback());
 
-  ASSERT_TRUE(base::test::RunUntil([&]() { return called_back; }));
+  EXPECT_TRUE(future.Wait());
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingEntryPointControllerOmniboxBrowserTest,
+                       CheckIfShouldSuggestReadingMode_LongerPdfIsCandidate) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(),
+      embedded_test_server()->GetURL(
+          "/pdf/accessibility/paragraphs-and-heading-untagged.pdf")));
+  ASSERT_TRUE(pdf_extension_test_util::EnsurePDFHasLoaded(web_contents));
+  base::test::TestFuture<bool> future;
+
+  read_anything::ReadAnythingEntryPointController::
+      CheckIfShouldSuggestReadingMode(browser(), future.GetCallback());
+
+  EXPECT_TRUE(future.Wait());
+  EXPECT_TRUE(future.Get());
+}
+
+IN_PROC_BROWSER_TEST_F(
+    ReadAnythingEntryPointControllerOmniboxBrowserTest,
+    CheckIfShouldSuggestReadingMode_ShorterPdfIsNotCandidate) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), embedded_test_server()->GetURL("/pdf/test.pdf")));
+  ASSERT_TRUE(pdf_extension_test_util::EnsurePDFHasLoaded(web_contents));
+  base::test::TestFuture<bool> future;
+
+  read_anything::ReadAnythingEntryPointController::
+      CheckIfShouldSuggestReadingMode(browser(), future.GetCallback());
+
+  EXPECT_TRUE(future.Wait());
+  EXPECT_FALSE(future.Get());
+}
+
+IN_PROC_BROWSER_TEST_F(
+    ReadAnythingEntryPointControllerOmniboxBrowserTest,
+    CheckIfShouldSuggestReadingMode_LongerPdfWithLotsOfSymbolsIsNotCandidate) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(),
+      embedded_test_server()->GetURL(
+          "/pdf/accessibility/paragraphs-and-heading-untagged-nonsense.pdf")));
+  ASSERT_TRUE(pdf_extension_test_util::EnsurePDFHasLoaded(web_contents));
+  base::test::TestFuture<bool> future;
+
+  read_anything::ReadAnythingEntryPointController::
+      CheckIfShouldSuggestReadingMode(browser(), future.GetCallback());
+
+  EXPECT_TRUE(future.Wait());
+  EXPECT_FALSE(future.Get());
 }
 
 IN_PROC_BROWSER_TEST_F(ReadAnythingEntryPointControllerOmniboxBrowserTest,
@@ -276,14 +339,13 @@
       browser(), GURL(url::kAboutBlankURL),
       WindowOpenDisposition::NEW_FOREGROUND_TAB,
       ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
-  static bool is_good_candidate_ = true;
-  auto result_callback = base::BindOnce(
-      [](bool is_good_candidate) { is_good_candidate_ = is_good_candidate; });
+  base::test::TestFuture<bool> future;
 
   read_anything::ReadAnythingEntryPointController::
-      CheckIfShouldSuggestReadingMode(browser(), std::move(result_callback));
+      CheckIfShouldSuggestReadingMode(browser(), future.GetCallback());
 
-  ASSERT_TRUE(base::test::RunUntil([&]() { return !is_good_candidate_; }));
+  EXPECT_TRUE(future.Wait());
+  EXPECT_FALSE(future.Get());
 }
 
 IN_PROC_BROWSER_TEST_F(
@@ -293,14 +355,13 @@
       browser(), GURL("https://www.docs.google.com"),
       WindowOpenDisposition::NEW_FOREGROUND_TAB,
       ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
-  static bool is_good_candidate_ = true;
-  auto result_callback = base::BindOnce(
-      [](bool is_good_candidate) { is_good_candidate_ = is_good_candidate; });
+  base::test::TestFuture<bool> future;
 
   read_anything::ReadAnythingEntryPointController::
-      CheckIfShouldSuggestReadingMode(browser(), std::move(result_callback));
+      CheckIfShouldSuggestReadingMode(browser(), future.GetCallback());
 
-  ASSERT_TRUE(base::test::RunUntil([&]() { return !is_good_candidate_; }));
+  EXPECT_TRUE(future.Wait());
+  EXPECT_FALSE(future.Get());
 }
 
 IN_PROC_BROWSER_TEST_F(
diff --git a/chrome/browser/ui/read_anything/read_anything_omnibox_controller.cc b/chrome/browser/ui/read_anything/read_anything_omnibox_controller.cc
index d698565..b63b916 100644
--- a/chrome/browser/ui/read_anything/read_anything_omnibox_controller.cc
+++ b/chrome/browser/ui/read_anything/read_anything_omnibox_controller.cc
@@ -124,6 +124,10 @@
 }
 
 void ReadAnythingOmniboxController::PrimaryPageChanged(content::Page& page) {
+  if (IsIrrelevant()) {
+    return;
+  }
+
   UpdateIgnored(GetCurrentPageActionState().showing);
   if (!read_anything::ReadAnythingEntryPointController::
           CheckIfShouldSuggestReadingModeNaive(
@@ -139,6 +143,11 @@
 }
 
 void ReadAnythingOmniboxController::DebounceCheckSuggestion() {
+  if (IsIrrelevant()) {
+    return;
+  }
+
+  candidate_check_triggered_time_ms_ = base::TimeTicks::Now();
   if (!check_suggestion_debouncer_) {
     check_suggestion_debouncer_ = std::make_unique<base::OneShotTimer>();
   }
@@ -150,11 +159,10 @@
 }
 
 void ReadAnythingOmniboxController::CheckIfShouldSuggestReadingMode() {
-  if (!tab_->IsActivated()) {
+  if (IsIrrelevant()) {
     return;
   }
 
-  candidate_check_triggered_time_ms_ = base::TimeTicks::Now();
   read_anything::ReadAnythingEntryPointController::
       CheckIfShouldSuggestReadingMode(
           tab_->GetBrowserWindowInterface(),
@@ -169,7 +177,7 @@
   // entry point is hidden when the tab is closed, but a closed tab should
   // count as "ignored".
   was_last_checked_page_distillable_ = should_show;
-  if (!tab_->IsActivated()) {
+  if (IsIrrelevant()) {
     return;
   }
 
@@ -178,7 +186,7 @@
 
 void ReadAnythingOmniboxController::UpdateVisibility(bool should_show) {
   // Don't show the entrypoint if the tab is no longer active.
-  if (!tab_->IsActivated()) {
+  if (IsIrrelevant()) {
     return;
   }
 
@@ -236,3 +244,9 @@
     check_suggestion_debouncer_->Stop();
   }
 }
+
+bool ReadAnythingOmniboxController::IsIrrelevant() {
+  return !tab_->IsActivated() ||
+         read_anything::ReadAnythingEntryPointController::IsUIShowing(
+             tab_->GetBrowserWindowInterface());
+}
diff --git a/chrome/browser/ui/read_anything/read_anything_omnibox_controller.h b/chrome/browser/ui/read_anything/read_anything_omnibox_controller.h
index 719d8bc..58f4136 100644
--- a/chrome/browser/ui/read_anything/read_anything_omnibox_controller.h
+++ b/chrome/browser/ui/read_anything/read_anything_omnibox_controller.h
@@ -87,6 +87,10 @@
   // Stops any running timers.
   void StopTimers();
 
+  // If the omnibox chip is irrelevant now. e.g. because the tab is no longer
+  // active or RM is already open.
+  bool IsIrrelevant();
+
   // The time when CheckIfShouldSuggestReadingMode was triggered.
   base::TimeTicks candidate_check_triggered_time_ms_;
 
diff --git a/chrome/browser/ui/read_anything/read_anything_omnibox_controller_browsertest.cc b/chrome/browser/ui/read_anything/read_anything_omnibox_controller_browsertest.cc
index d8695a8..93033989 100644
--- a/chrome/browser/ui/read_anything/read_anything_omnibox_controller_browsertest.cc
+++ b/chrome/browser/ui/read_anything/read_anything_omnibox_controller_browsertest.cc
@@ -10,6 +10,7 @@
 #include "base/test/run_until.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/scoped_mock_time_message_loop_task_runner.h"
+#include "base/time/time.h"
 #include "chrome/app/chrome_command_ids.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
@@ -17,6 +18,7 @@
 #include "chrome/browser/ui/browser_window/public/browser_window_features.h"
 #include "chrome/browser/ui/read_anything/read_anything_controller.h"
 #include "chrome/browser/ui/read_anything/read_anything_entry_point_controller.h"
+#include "chrome/browser/ui/read_anything/read_anything_enums.h"
 #include "chrome/browser/ui/read_anything/read_anything_prefs.h"
 #include "chrome/browser/ui/side_panel/side_panel_action_callback.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry_id.h"
@@ -29,6 +31,7 @@
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/ui_test_utils.h"
 #include "components/prefs/pref_service.h"
+#include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
 #include "ui/accessibility/accessibility_features.h"
 #include "ui/actions/action_id.h"
@@ -79,6 +82,73 @@
 };
 
 IN_PROC_BROWSER_TEST_F(ReadAnythingOmniboxControllerBrowserTest,
+                       PrimaryPageChanged_UpdatesIgnoredCount) {
+  controller_ = CreateController();
+  tabs::TabInterface* tab = browser()->tab_strip_model()->GetActiveTab();
+  PrefService* prefs = browser()->GetProfile()->GetPrefs();
+
+  // When the page changes with no previous page, ignored count stay at 0.
+  controller_->PrimaryPageChanged(tab->GetContents()->GetPrimaryPage());
+  EXPECT_EQ(prefs->GetInteger(
+                prefs::kAccessibilityReadAnythingOmniboxChipIgnoredCount),
+            0);
+
+  // Show the omnibox chip on this page and dwell on it for long enough. The
+  // ignored count is still 0.
+  tab->GetTabFeatures()->page_action_controller()->Show(
+      kActionSidePanelShowReadAnything);
+  controller_->SetDwellTimeForTesting(base::TimeTicks::Now() -
+                                      base::Seconds(5));
+  EXPECT_EQ(prefs->GetInteger(
+                prefs::kAccessibilityReadAnythingOmniboxChipIgnoredCount),
+            0);
+
+  // After changing pages again, the ignored count should increment because the
+  // omnibox entrypoint was showing on the previous page and was dwelled on for
+  // a non-trivial amount of time.
+  controller_->PrimaryPageChanged(tab->GetContents()->GetPrimaryPage());
+  EXPECT_EQ(prefs->GetInteger(
+                prefs::kAccessibilityReadAnythingOmniboxChipIgnoredCount),
+            1);
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingOmniboxControllerBrowserTest,
+                       PrimaryPageChanged_DoesNotUpdateIgnoredCountIfRMOpened) {
+  controller_ = CreateController();
+  tabs::TabInterface* tab = browser()->tab_strip_model()->GetActiveTab();
+  PrefService* prefs = browser()->GetProfile()->GetPrefs();
+
+  // When the page changes with no previous page, ignored count stay at 0.
+  controller_->PrimaryPageChanged(tab->GetContents()->GetPrimaryPage());
+  EXPECT_EQ(prefs->GetInteger(
+                prefs::kAccessibilityReadAnythingOmniboxChipIgnoredCount),
+            0);
+
+  // Show the omnibox chip on this page and dwell on it for long enough. The
+  // ignored count is still 0.
+  tab->GetTabFeatures()->page_action_controller()->Show(
+      kActionSidePanelShowReadAnything);
+  controller_->SetDwellTimeForTesting(base::TimeTicks::Now() -
+                                      base::Seconds(5));
+  EXPECT_EQ(prefs->GetInteger(
+                prefs::kAccessibilityReadAnythingOmniboxChipIgnoredCount),
+            0);
+
+  // Now when the page changes after RM is opened, the chip is not considered
+  // ignored.
+  read_anything::ReadAnythingEntryPointController::ShowUI(
+      browser(), ReadAnythingOpenTrigger::kReadAnythingContextMenu);
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return read_anything::ReadAnythingEntryPointController::IsUIShowing(
+        browser());
+  }));
+  controller_->PrimaryPageChanged(tab->GetContents()->GetPrimaryPage());
+  EXPECT_EQ(prefs->GetInteger(
+                prefs::kAccessibilityReadAnythingOmniboxChipIgnoredCount),
+            0);
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingOmniboxControllerBrowserTest,
                        DidStopLoadingIsDebounced) {
   base::ScopedMockTimeMessageLoopTaskRunner mocked_task_runner;
   controller_ = CreateController();
@@ -102,6 +172,23 @@
 }
 
 IN_PROC_BROWSER_TEST_F(ReadAnythingOmniboxControllerBrowserTest,
+                       DidStopLoadingDoesNotCheckIfRMOpened) {
+  controller_ = CreateController();
+
+  read_anything::ReadAnythingEntryPointController::ShowUI(
+      browser(), ReadAnythingOpenTrigger::kReadAnythingContextMenu);
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return read_anything::ReadAnythingEntryPointController::IsUIShowing(
+        browser());
+  }));
+
+  base::ScopedMockTimeMessageLoopTaskRunner mocked_task_runner;
+  controller_->DidStopLoading();
+  mocked_task_runner->FastForwardBy(base::Seconds(1));
+  EXPECT_EQ(controller_->CheckCount(), 0);
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingOmniboxControllerBrowserTest,
                        TabForegroundedIsDebounced) {
   base::ScopedMockTimeMessageLoopTaskRunner mocked_task_runner;
   controller_ = CreateController();
@@ -130,6 +217,41 @@
 }
 
 IN_PROC_BROWSER_TEST_F(ReadAnythingOmniboxControllerBrowserTest,
+                       TabForegroundedDoesNotCheckIfRMOpened) {
+  controller_ = CreateController();
+  chrome::NewTab(browser());
+  ASSERT_EQ(browser()->tab_strip_model()->count(), 2);
+
+  // Show RM on tab 0.
+  browser()->tab_strip_model()->ActivateTabAt(0);
+  read_anything::ReadAnythingEntryPointController::ShowUI(
+      browser(), ReadAnythingOpenTrigger::kReadAnythingContextMenu);
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return read_anything::ReadAnythingEntryPointController::IsUIShowing(
+        browser());
+  }));
+  // Switch to tab 1 which has no RM.
+  browser()->tab_strip_model()->ActivateTabAt(1);
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return !read_anything::ReadAnythingEntryPointController::IsUIShowing(
+        browser());
+  }));
+
+  // Switch back to tab 0, where RM should still be showing.
+  controller_->ResetCheckCount();
+  browser()->tab_strip_model()->ActivateTabAt(0);
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return read_anything::ReadAnythingEntryPointController::IsUIShowing(
+        browser());
+  }));
+
+  // Tab 0 was foregrounded but should not run CheckSuggestion.
+  base::ScopedMockTimeMessageLoopTaskRunner mocked_task_runner;
+  mocked_task_runner->FastForwardBy(base::Seconds(1));
+  EXPECT_EQ(controller_->CheckCount(), 0);
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingOmniboxControllerBrowserTest,
                        Activate_LogsOmniboxEntrypointAfterOmniboxClicked) {
   base::HistogramTester histogram_tester;
   controller_ = CreateController();
diff --git a/chrome/browser/ui/side_panel/BUILD.gn b/chrome/browser/ui/side_panel/BUILD.gn
index d964723b..271d8039 100644
--- a/chrome/browser/ui/side_panel/BUILD.gn
+++ b/chrome/browser/ui/side_panel/BUILD.gn
@@ -43,6 +43,8 @@
     "side_panel_entry.h",
     "side_panel_entry_waiter.cc",
     "side_panel_entry_waiter.h",
+    "side_panel_metrics.cc",
+    "side_panel_metrics.h",
     "side_panel_registry.cc",
     "side_panel_registry.h",
     "side_panel_ui.h",
diff --git a/chrome/browser/ui/side_panel/side_panel_entry.cc b/chrome/browser/ui/side_panel/side_panel_entry.cc
index b065645..dcd13e9 100644
--- a/chrome/browser/ui/side_panel/side_panel_entry.cc
+++ b/chrome/browser/ui/side_panel/side_panel_entry.cc
@@ -9,7 +9,7 @@
 #include "base/time/time.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry_observer.h"
 #include "chrome/browser/ui/side_panel/side_panel_enums.h"
-#include "chrome/browser/ui/views/side_panel/side_panel_util.h"
+#include "chrome/browser/ui/side_panel/side_panel_metrics.h"
 
 DEFINE_UI_CLASS_PROPERTY_KEY(bool, kShouldShowTitleInSidePanelHeaderKey, true)
 
@@ -73,8 +73,8 @@
 
 void SidePanelEntry::OnEntryShown() {
   entry_shown_timestamp_ = base::TimeTicks::Now();
-  SidePanelUtil::RecordEntryShownMetrics(type(), key_.id(),
-                                         entry_show_triggered_timestamp_);
+  SidePanelMetrics::RecordEntryShownMetrics(type(), key_.id(),
+                                            entry_show_triggered_timestamp_);
   // After the initial load time is recorded, we need to reset the triggered
   // timestamp so we don't keep recording this entry after its selected from the
   // combobox.
@@ -91,8 +91,8 @@
 }
 
 void SidePanelEntry::OnEntryHidden() {
-  SidePanelUtil::RecordEntryHiddenMetrics(type(), key_.id(),
-                                          entry_shown_timestamp_);
+  SidePanelMetrics::RecordEntryHiddenMetrics(type(), key_.id(),
+                                             entry_shown_timestamp_);
   observers_.Notify(&SidePanelEntryObserver::OnEntryHidden, this);
 }
 
diff --git a/chrome/browser/ui/side_panel/side_panel_enums.h b/chrome/browser/ui/side_panel/side_panel_enums.h
index 869fc04..418c414 100644
--- a/chrome/browser/ui/side_panel/side_panel_enums.h
+++ b/chrome/browser/ui/side_panel/side_panel_enums.h
@@ -64,4 +64,12 @@
   kBackgrounded = 2,
 };
 
+// LINT.IfChange(SidePanelAnimationType)
+enum class SidePanelAnimationType {
+  kOpen = 0,
+  kOpenWithContentTransition = 1,
+  kClose = 2,
+};
+// LINT.ThenChange(//tools/metrics/histograms/metadata/browser/enums.xml:SidePanelAnimationType)
+
 #endif  // CHROME_BROWSER_UI_SIDE_PANEL_SIDE_PANEL_ENUMS_H_
diff --git a/chrome/browser/ui/side_panel/side_panel_metrics.cc b/chrome/browser/ui/side_panel/side_panel_metrics.cc
new file mode 100644
index 0000000..84ba9007
--- /dev/null
+++ b/chrome/browser/ui/side_panel/side_panel_metrics.cc
@@ -0,0 +1,206 @@
+// Copyright 2022 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/side_panel/side_panel_metrics.h"
+
+#include <string_view>
+
+#include "base/metrics/histogram_functions.h"
+#include "base/metrics/user_metrics.h"
+#include "base/metrics/user_metrics_action.h"
+#include "base/notreached.h"
+#include "base/strings/strcat.h"
+
+namespace {
+
+std::string_view GetSidePanelNameFor(SidePanelEntry::PanelType panel_type) {
+  switch (panel_type) {
+    case SidePanelEntry::PanelType::kContent:
+      return "SidePanel";
+    case SidePanelEntry::PanelType::kToolbar:
+      return "SidePanelToolbarHeight";
+  }
+
+  NOTREACHED() << "Invalid PanelType " << static_cast<int>(panel_type);
+}
+
+std::string_view GetAnimationNameFor(SidePanelAnimationType animation_type) {
+  switch (animation_type) {
+    case SidePanelAnimationType::kOpen:
+      return "Open";
+    case SidePanelAnimationType::kOpenWithContentTransition:
+      return "OpenWithContentTransition";
+    case SidePanelAnimationType::kClose:
+      return "Close";
+  }
+
+  NOTREACHED() << "Invalid AnimationType " << static_cast<int>(animation_type);
+}
+
+}  // namespace
+
+void SidePanelMetrics::RecordSidePanelOpen(
+    SidePanelEntry::PanelType type,
+    std::optional<SidePanelOpenTrigger> trigger) {
+  base::RecordAction(base::UserMetricsAction(
+      base::StrCat({GetSidePanelNameFor(type), ".Show"}).c_str()));
+
+  if (trigger.has_value()) {
+    base::UmaHistogramEnumeration(
+        base::StrCat({GetSidePanelNameFor(type), ".OpenTrigger"}),
+        trigger.value());
+  }
+}
+
+void SidePanelMetrics::RecordSidePanelShowOrChangeEntryTrigger(
+    SidePanelEntry::PanelType type,
+    std::optional<SidePanelOpenTrigger> trigger) {
+  if (trigger.has_value()) {
+    base::UmaHistogramEnumeration(
+        base::StrCat({GetSidePanelNameFor(type), ".OpenOrChangeEntryTrigger"}),
+        trigger.value());
+  }
+}
+
+void SidePanelMetrics::RecordSidePanelClosed(SidePanelEntry::PanelType type,
+                                             base::TimeTicks opened_timestamp) {
+  base::RecordAction(base::UserMetricsAction(
+      base::StrCat({GetSidePanelNameFor(type), ".Hide"}).c_str()));
+
+  base::UmaHistogramLongTimes(
+      base::StrCat({GetSidePanelNameFor(type), ".OpenDuration"}),
+      base::TimeTicks::Now() - opened_timestamp);
+}
+
+void SidePanelMetrics::RecordSidePanelResizeMetrics(
+    SidePanelEntry::PanelType type,
+    SidePanelEntry::Id id,
+    int side_panel_contents_width,
+    int browser_window_width) {
+  std::string_view entry_name = SidePanelEntryIdToHistogramName(id);
+
+  // Metrics per-id and overall for side panel width after resize.
+  base::UmaHistogramCounts10000(base::StrCat({GetSidePanelNameFor(type), ".",
+                                              entry_name, ".ResizedWidth"}),
+                                side_panel_contents_width);
+  base::UmaHistogramCounts10000(
+      base::StrCat({GetSidePanelNameFor(type), ".ResizedWidth"}),
+      side_panel_contents_width);
+
+  // Metrics per-id and overall for side panel width after resize as a
+  // percentage of browser width.
+  int width_percentage = side_panel_contents_width * 100 / browser_window_width;
+  base::UmaHistogramPercentage(
+      base::StrCat({GetSidePanelNameFor(type), ".", entry_name,
+                    ".ResizedWidthPercentage"}),
+      width_percentage);
+  base::UmaHistogramPercentage(
+      base::StrCat({GetSidePanelNameFor(type), ".ResizedWidthPercentage"}),
+      width_percentage);
+}
+
+void SidePanelMetrics::RecordNewTabButtonClicked(SidePanelEntry::Id id) {
+  base::RecordComputedAction(
+      base::StrCat({"SidePanel.", SidePanelEntryIdToHistogramName(id),
+                    ".NewTabButtonClicked"}));
+}
+
+void SidePanelMetrics::RecordEntryShownMetrics(
+    SidePanelEntry::PanelType type,
+    SidePanelEntry::Id id,
+    base::TimeTicks load_started_timestamp) {
+  base::RecordComputedAction(
+      base::StrCat({GetSidePanelNameFor(type), ".",
+                    SidePanelEntryIdToHistogramName(id), ".Shown"}));
+  if (load_started_timestamp != base::TimeTicks()) {
+    base::UmaHistogramLongTimes(
+        base::StrCat({GetSidePanelNameFor(type), ".",
+                      SidePanelEntryIdToHistogramName(id),
+                      ".TimeFromEntryTriggerToShown"}),
+        base::TimeTicks::Now() - load_started_timestamp);
+  }
+}
+
+void SidePanelMetrics::RecordEntryHiddenMetrics(
+    SidePanelEntry::PanelType type,
+    SidePanelEntry::Id id,
+    base::TimeTicks shown_timestamp) {
+  base::UmaHistogramLongTimes(
+      base::StrCat({GetSidePanelNameFor(type), ".",
+                    SidePanelEntryIdToHistogramName(id), ".ShownDuration"}),
+      base::TimeTicks::Now() - shown_timestamp);
+  // To measure extended usage times, Read Anything also needs a higher maximum
+  // than what's supported by the standard ShownDuration histogram.
+  if (type == SidePanelEntry::PanelType::kContent &&
+      id == SidePanelEntryId::kReadAnything) {
+    // TODO(crbug.com/456824119): Consider removing the standard ShownDuration
+    // histogram for Read Anything after this one has gathered enough data.
+    base::UmaHistogramCustomTimes("SidePanel.ReadAnything.ShownDurationMax1Day",
+                                  base::TimeTicks::Now() - shown_timestamp,
+                                  /*min=*/base::Seconds(1),
+                                  /*max=*/base::Hours(24),
+                                  /*buckets=*/100);
+  }
+}
+
+void SidePanelMetrics::RecordEntryShowTriggeredMetrics(
+    SidePanelEntry::PanelType type,
+    SidePanelEntry::Id id,
+    std::optional<SidePanelOpenTrigger> trigger) {
+  if (trigger.has_value()) {
+    base::UmaHistogramEnumeration(
+        base::StrCat({GetSidePanelNameFor(type), ".",
+                      SidePanelEntryIdToHistogramName(id), ".ShowTriggered"}),
+        trigger.value());
+  }
+}
+
+void SidePanelMetrics::RecordPinnedButtonClicked(SidePanelEntry::Id id,
+                                                 bool is_pinned) {
+  base::RecordComputedAction(base::StrCat(
+      {"SidePanel.", SidePanelEntryIdToHistogramName(id), ".",
+       is_pinned ? "Pinned" : "Unpinned", ".BySidePanelHeaderButton"}));
+}
+
+void SidePanelMetrics::RecordSidePanelAnimationMetrics(
+    SidePanelEntry::PanelType panel_type,
+    SidePanelAnimationType animation_type,
+    base::TimeDelta largest_step_time,
+    int frames_per_second) {
+  if (!largest_step_time.is_zero()) {
+    base::UmaHistogramTimes(base::StrCat({GetSidePanelNameFor(panel_type),
+                                          ".TimeOfLongestAnimationStep"}),
+                            largest_step_time);
+  }
+
+  if (frames_per_second > 0) {
+    base::UmaHistogramCounts100(
+        base::StrCat({GetSidePanelNameFor(panel_type), ".",
+                      GetAnimationNameFor(animation_type), ".AnimationFPS"}),
+        frames_per_second);
+  }
+}
+
+void SidePanelMetrics::RecordPanelClosedForOtherPanelTypeMetrics(
+    SidePanelEntry::PanelType closing_panel_type,
+    SidePanelEntry::PanelType opening_panel_type,
+    SidePanelEntryId closing_panel_id,
+    SidePanelEntryId opening_panel_id) {
+  base::RecordComputedAction(
+      base::StrCat({GetSidePanelNameFor(closing_panel_type), ".ClosedToOpen.",
+                    GetSidePanelNameFor(opening_panel_type)}));
+  if (closing_panel_id == SidePanelEntryId::kContextualTasks) {
+    base::RecordComputedAction(base::StrCat(
+        {GetSidePanelNameFor(closing_panel_type), ".",
+         SidePanelEntryIdToHistogramName(closing_panel_id), ".ClosedToOpen.",
+         GetSidePanelNameFor(opening_panel_type)}));
+    if (opening_panel_id == SidePanelEntryId::kGlic) {
+      base::RecordComputedAction(
+          base::StrCat({GetSidePanelNameFor(closing_panel_type), ".",
+                        SidePanelEntryIdToHistogramName(closing_panel_id),
+                        ".EntryClosedToOpen.",
+                        SidePanelEntryIdToHistogramName(opening_panel_id)}));
+    }
+  }
+}
diff --git a/chrome/browser/ui/side_panel/side_panel_metrics.h b/chrome/browser/ui/side_panel/side_panel_metrics.h
new file mode 100644
index 0000000..f3e2e0d
--- /dev/null
+++ b/chrome/browser/ui/side_panel/side_panel_metrics.h
@@ -0,0 +1,52 @@
+// Copyright 2022 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_SIDE_PANEL_SIDE_PANEL_METRICS_H_
+#define CHROME_BROWSER_UI_SIDE_PANEL_SIDE_PANEL_METRICS_H_
+
+#include <optional>
+
+#include "base/time/time.h"
+#include "chrome/browser/ui/side_panel/side_panel_entry.h"
+#include "chrome/browser/ui/side_panel/side_panel_entry_id.h"
+#include "chrome/browser/ui/side_panel/side_panel_enums.h"
+
+class SidePanelMetrics {
+ public:
+  static void RecordNewTabButtonClicked(SidePanelEntry::Id id);
+  static void RecordSidePanelOpen(SidePanelEntry::PanelType type,
+                                  std::optional<SidePanelOpenTrigger> trigger);
+  static void RecordSidePanelShowOrChangeEntryTrigger(
+      SidePanelEntry::PanelType type,
+      std::optional<SidePanelOpenTrigger> trigger);
+  static void RecordSidePanelClosed(SidePanelEntry::PanelType type,
+                                    base::TimeTicks opened_timestamp);
+  static void RecordSidePanelResizeMetrics(SidePanelEntry::PanelType type,
+                                           SidePanelEntry::Id id,
+                                           int side_panel_contents_width,
+                                           int browser_window_width);
+  static void RecordEntryShownMetrics(SidePanelEntry::PanelType type,
+                                      SidePanelEntry::Id id,
+                                      base::TimeTicks load_started_timestamp);
+  static void RecordEntryHiddenMetrics(SidePanelEntry::PanelType type,
+                                       SidePanelEntry::Id id,
+                                       base::TimeTicks shown_timestamp);
+  static void RecordEntryShowTriggeredMetrics(
+      SidePanelEntry::PanelType type,
+      SidePanelEntry::Id id,
+      std::optional<SidePanelOpenTrigger> trigger);
+  static void RecordPinnedButtonClicked(SidePanelEntry::Id id, bool is_pinned);
+  static void RecordSidePanelAnimationMetrics(
+      SidePanelEntry::PanelType panel_type,
+      SidePanelAnimationType animation_type,
+      base::TimeDelta largest_step_time,
+      int frames_per_second);
+  static void RecordPanelClosedForOtherPanelTypeMetrics(
+      SidePanelEntry::PanelType closing_panel_type,
+      SidePanelEntry::PanelType opening_panel_type,
+      SidePanelEntryId closing_panel_id,
+      SidePanelEntryId opening_panel_id);
+};
+
+#endif  // CHROME_BROWSER_UI_SIDE_PANEL_SIDE_PANEL_METRICS_H_
diff --git a/chrome/browser/ui/views/side_panel/side_panel_util_unittest.cc b/chrome/browser/ui/side_panel/side_panel_metrics_unittest.cc
similarity index 65%
rename from chrome/browser/ui/views/side_panel/side_panel_util_unittest.cc
rename to chrome/browser/ui/side_panel/side_panel_metrics_unittest.cc
index 7f3cefb..fa456d04 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_util_unittest.cc
+++ b/chrome/browser/ui/side_panel/side_panel_metrics_unittest.cc
@@ -2,14 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/ui/views/side_panel/side_panel_util.h"
+#include "chrome/browser/ui/side_panel/side_panel_metrics.h"
 
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/task_environment.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry_id.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
-class SidePanelUtilTest : public testing::Test {
+class SidePanelMetricsTest : public testing::Test {
  public:
   void SetUp() override {
     task_environment_ = std::make_unique<base::test::TaskEnvironment>(
@@ -21,14 +21,14 @@
   std::unique_ptr<base::test::TaskEnvironment> task_environment_;
 };
 
-TEST_F(SidePanelUtilTest, RecordDuration_ForReadAnythingEntry) {
+TEST_F(SidePanelMetricsTest, RecordDuration_ForReadAnythingEntry) {
   const base::TimeTicks shown_timestamp = base::TimeTicks::Now();
   const base::TimeDelta duration = base::Hours(1);
   task_environment_->FastForwardBy(duration);
 
-  SidePanelUtil::RecordEntryHiddenMetrics(SidePanelEntry::PanelType::kContent,
-                                          SidePanelEntryId::kReadAnything,
-                                          shown_timestamp);
+  SidePanelMetrics::RecordEntryHiddenMetrics(
+      SidePanelEntry::PanelType::kContent, SidePanelEntryId::kReadAnything,
+      shown_timestamp);
 
   histogram_tester_.ExpectTimeBucketCount(
       "SidePanel.ReadAnything.ShownDuration", duration, 1);
@@ -36,14 +36,14 @@
       "SidePanel.ReadAnything.ShownDurationMax1Day", duration, 1);
 }
 
-TEST_F(SidePanelUtilTest, RecordsMaxCap_WhenDurationExceedsOneDay) {
+TEST_F(SidePanelMetricsTest, RecordsMaxCap_WhenDurationExceedsOneDay) {
   const base::TimeTicks shown_timestamp = base::TimeTicks::Now();
   const base::TimeDelta duration = base::Hours(25);
   task_environment_->FastForwardBy(duration);
 
-  SidePanelUtil::RecordEntryHiddenMetrics(SidePanelEntry::PanelType::kContent,
-                                          SidePanelEntryId::kReadAnything,
-                                          shown_timestamp);
+  SidePanelMetrics::RecordEntryHiddenMetrics(
+      SidePanelEntry::PanelType::kContent, SidePanelEntryId::kReadAnything,
+      shown_timestamp);
 
   histogram_tester_.ExpectTimeBucketCount(
       "SidePanel.ReadAnything.ShownDurationMax1Day", base::Hours(24), 1);
diff --git a/chrome/browser/ui/signin/promos/bubble_sign_in_promo_delegate_browsertest.cc b/chrome/browser/ui/signin/promos/bubble_sign_in_promo_delegate_browsertest.cc
index 42ec42d..a8cd8298 100644
--- a/chrome/browser/ui/signin/promos/bubble_sign_in_promo_delegate_browsertest.cc
+++ b/chrome/browser/ui/signin/promos/bubble_sign_in_promo_delegate_browsertest.cc
@@ -18,6 +18,7 @@
 #include "chrome/common/chrome_switches.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/ui_test_utils.h"
 #include "components/signin/public/base/account_consistency_method.h"
 #include "components/signin/public/base/signin_metrics.h"
 #include "components/signin/public/identity_manager/account_info.h"
@@ -130,7 +131,7 @@
           signin_metrics::AccessPoint::kBookmarkBubble,
           syncer::LocalDataItemModel::DataId());
 
-  BrowserList::SetLastActive(extra_browser);
+  ui_test_utils::DeprecatedFakeActivateBrowser(extra_browser);
 
   // Close all tabs in the original browser.  Run all pending messages
   // to make sure the browser window closes before continuing.
diff --git a/chrome/browser/ui/startup/BUILD.gn b/chrome/browser/ui/startup/BUILD.gn
index a87059e6..ecc76089 100644
--- a/chrome/browser/ui/startup/BUILD.gn
+++ b/chrome/browser/ui/startup/BUILD.gn
@@ -199,6 +199,7 @@
       "//chrome/browser/extensions",
       "//chrome/browser/headless",
       "//chrome/browser/headless:command_processor",
+      "//chrome/browser/obsolete_system",
       "//chrome/browser/prefs",
       "//chrome/browser/prefs:util",
       "//chrome/browser/privacy_sandbox:headers",
diff --git a/chrome/browser/ui/tab_ui_helper.cc b/chrome/browser/ui/tab_ui_helper.cc
index 5bf2782..b5a5551 100644
--- a/chrome/browser/ui/tab_ui_helper.cc
+++ b/chrome/browser/ui/tab_ui_helper.cc
@@ -300,6 +300,14 @@
   tab_ui_change_callbacks_.Notify();
 }
 
+void TabUIHelper::PrimaryMainFrameRenderProcessGone(
+    base::TerminationStatus status) {
+  // The tab's main frame was crashed so observers should be notified.
+  if (IsCrashed()) {
+    tab_ui_change_callbacks_.Notify();
+  }
+}
+
 #if !BUILDFLAG(IS_ANDROID)
 void TabUIHelper::PrimaryPageChanged(content::Page& page) {
   if (tab().IsSplit()) {
@@ -310,6 +318,14 @@
 }
 #endif
 
+void TabUIHelper::SetCreatedBySessionRestore(bool created_by_session_restore) {
+  const bool was_hiding_throbber = ShouldHideThrobber();
+  created_by_session_restore_ = created_by_session_restore;
+  if (was_hiding_throbber != ShouldHideThrobber()) {
+    tab_ui_change_callbacks_.Notify();
+  }
+}
+
 void TabUIHelper::SetNeedsAttention(bool needs_attention) {
   if (needs_attention == needs_attention_) {
     return;
diff --git a/chrome/browser/ui/tab_ui_helper.h b/chrome/browser/ui/tab_ui_helper.h
index dd9164c38..f568d878 100644
--- a/chrome/browser/ui/tab_ui_helper.h
+++ b/chrome/browser/ui/tab_ui_helper.h
@@ -14,6 +14,7 @@
 #include "base/functional/callback_forward.h"
 #include "base/types/pass_key.h"
 #include "chrome/browser/ui/tabs/contents_observing_tab_feature.h"
+#include "content/public/browser/web_contents_observer.h"
 #include "ui/base/unowned_user_data/scoped_unowned_user_data.h"
 
 #if !BUILDFLAG(IS_ANDROID)
@@ -88,13 +89,13 @@
   void WasDiscarded() override;
   void DidFinishNavigation(
       content::NavigationHandle* navigation_handle) override;
+  void PrimaryMainFrameRenderProcessGone(
+      base::TerminationStatus status) override;
 #if !BUILDFLAG(IS_ANDROID)
   void PrimaryPageChanged(content::Page& page) override;
 #endif
 
-  void set_created_by_session_restore(bool created_by_session_restore) {
-    created_by_session_restore_ = created_by_session_restore;
-  }
+  void SetCreatedBySessionRestore(bool created_by_session_restore);
   bool is_created_by_session_restore_for_testing() {
     return created_by_session_restore_;
   }
diff --git a/chrome/browser/ui/tab_ui_helper_browsertest.cc b/chrome/browser/ui/tab_ui_helper_browsertest.cc
index 6d23f37..4aca2201 100644
--- a/chrome/browser/ui/tab_ui_helper_browsertest.cc
+++ b/chrome/browser/ui/tab_ui_helper_browsertest.cc
@@ -157,6 +157,37 @@
   EXPECT_EQ(tab_ui_helper->GetTabNetworkState(), TabNetworkState::kNone);
 }
 
+IN_PROC_BROWSER_TEST_F(TabUIHelperBrowserTest, TabCrashStateChangeIsNotified) {
+  tabs::TabInterface* const tab_interface =
+      browser()->tab_strip_model()->GetActiveTab();
+  TabUIHelper* const tab_ui_helper = TabUIHelper::From(tab_interface);
+  ASSERT_FALSE(tab_ui_helper->IsCrashed());
+
+  auto tab_ui_change_waiter =
+      std::make_unique<MockTabUIHelperSubscriber>(tab_ui_helper);
+  EXPECT_CALL(*tab_ui_change_waiter, OnTabUIChange())
+      .Times(testing::AnyNumber());
+  content::CrashTab(tab_interface->GetContents());
+  EXPECT_TRUE(tab_ui_helper->IsCrashed());
+}
+
+IN_PROC_BROWSER_TEST_F(TabUIHelperBrowserTest, ShouldHideThrobberIsNotified) {
+  ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
+      browser(), GURL(url::kAboutBlankURL),
+      WindowOpenDisposition::NEW_BACKGROUND_TAB,
+      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
+  tabs::TabInterface* const tab_interface =
+      browser()->GetTabStripModel()->GetTabAtIndex(1);
+  TabUIHelper* const tab_ui_helper = TabUIHelper::From(tab_interface);
+  ASSERT_FALSE(tab_ui_helper->ShouldHideThrobber());
+
+  auto tab_ui_change_waiter =
+      std::make_unique<MockTabUIHelperSubscriber>(tab_ui_helper);
+  EXPECT_CALL(*tab_ui_change_waiter, OnTabUIChange());
+  tab_ui_helper->SetCreatedBySessionRestore(true);
+  EXPECT_TRUE(tab_ui_helper->ShouldHideThrobber());
+}
+
 class TabUIHelperWithPrerenderingTest : public InProcessBrowserTest {
  public:
   TabUIHelperWithPrerenderingTest()
@@ -207,7 +238,7 @@
   // Set |create_by_session_restore_| to true to check if the value is changed
   // after prerendering. It should not be changed because DidStopLoading is not
   // called during the prerendering.
-  tab_ui_helper->set_created_by_session_restore(true);
+  tab_ui_helper->SetCreatedBySessionRestore(true);
 
   // Prerender to another site.
   prerender_test_helper().AddPrerender(prerender_url);
diff --git a/chrome/browser/ui/tabs/tab_list_bridge_browsertest.cc b/chrome/browser/ui/tabs/tab_list_bridge_browsertest.cc
index c8df365..4afa981 100644
--- a/chrome/browser/ui/tabs/tab_list_bridge_browsertest.cc
+++ b/chrome/browser/ui/tabs/tab_list_bridge_browsertest.cc
@@ -623,7 +623,7 @@
       /*user_gesture=*/true);
   // params.window = window2.release();
   Browser* browser2 = Browser::Create(params);
-  BrowserList::SetLastActive(browser2);
+  ui_test_utils::DeprecatedFakeActivateBrowser(browser2);
 
   ASSERT_FALSE(browser2->tab_strip_model()->SupportsTabGroups());
 
diff --git a/chrome/browser/ui/tabs/tab_renderer_data_browsertest.cc b/chrome/browser/ui/tabs/tab_renderer_data_browsertest.cc
index f2e50bb..b2069722 100644
--- a/chrome/browser/ui/tabs/tab_renderer_data_browsertest.cc
+++ b/chrome/browser/ui/tabs/tab_renderer_data_browsertest.cc
@@ -350,7 +350,7 @@
       browser()->GetTabStripModel()->GetTabAtIndex(1);
   TabUIHelper* const helper = TabUIHelper::From(tab_interface);
   ASSERT_NE(nullptr, helper);
-  helper->set_created_by_session_restore(true);
+  helper->SetCreatedBySessionRestore(true);
   TabRendererData data = TabRendererData::FromTabInterface(tab_interface);
   EXPECT_TRUE(helper->ShouldHideThrobber());
   EXPECT_TRUE(data.should_hide_throbber);
diff --git a/chrome/browser/ui/tabs/tab_strip_api/BUILD.gn b/chrome/browser/ui/tabs/tab_strip_api/BUILD.gn
index e6f8b7cc..7224d1e 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/BUILD.gn
+++ b/chrome/browser/ui/tabs/tab_strip_api/BUILD.gn
@@ -63,16 +63,12 @@
 source_set("unit_tests") {
   testonly = true
 
-  sources = [
-    "tab_strip_experiment_service_unittest.cc",
-    "tab_strip_service_impl_unittest.cc",
-  ]
+  sources = [ "tab_strip_experiment_service_unittest.cc" ]
 
   deps = [
     ":impl",
     ":tab_strip_api",
     "adapters:unit_tests",
-    "converters:unit_tests",
     "events:unit_tests",
     "observation:unit_tests",
     "testing",
@@ -98,12 +94,14 @@
     ":impl",
     ":tab_strip_api",
     "adapters/tree_builder:browser_tests",
+    "converters:browser_tests",
     "observation",
     "//base",
     "//base/test:test_support",
     "//chrome/browser/ui:ui_features",
     "//chrome/browser/ui/browser_window",
     "//chrome/browser/ui/tabs:tab_strip",
+    "//chrome/browser/ui/tabs/tab_strip_api/adapters:impl",
     "//chrome/test:test_support",
     "//components/browser_apis/tab_strip:mojom_experiment",
     "//content/test:test_support",
diff --git a/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter.h b/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter.h
index ebf6f812..9f154244 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter.h
+++ b/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter.h
@@ -38,7 +38,6 @@
   virtual void RemoveCollectionObserver(
       tabs::TabCollectionObserver* collection_observer) = 0;
   virtual std::vector<tabs::TabHandle> GetTabs() const = 0;
-  virtual TabRendererData GetTabRendererData(int index) const = 0;
   virtual converters::TabStates GetTabStates(tabs::TabHandle) const = 0;
   virtual const ui::ColorProvider& GetColorProvider() const = 0;
   virtual void CloseTab(size_t tab_index) = 0;
diff --git a/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.cc b/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.cc
index b238d90..8192c37 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.cc
+++ b/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.cc
@@ -53,11 +53,6 @@
   return tabs;
 }
 
-TabRendererData TabStripModelAdapterImpl::GetTabRendererData(int index) const {
-  return TabRendererData::FromTabInterface(
-      tab_strip_model_->GetTabAtIndex(index));
-}
-
 converters::TabStates TabStripModelAdapterImpl::GetTabStates(
     tabs::TabHandle handle) const {
   CHECK(handle.Get() != nullptr);
diff --git a/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.h b/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.h
index c27d7c2eb..1fb4044 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.h
+++ b/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.h
@@ -30,7 +30,6 @@
   void RemoveCollectionObserver(
       tabs::TabCollectionObserver* collection_observer) override;
   std::vector<tabs::TabHandle> GetTabs() const override;
-  TabRendererData GetTabRendererData(int index) const override;
   converters::TabStates GetTabStates(tabs::TabHandle) const override;
   const ui::ColorProvider& GetColorProvider() const override;
   void CloseTab(size_t tab_index) override;
diff --git a/chrome/browser/ui/tabs/tab_strip_api/adapters/tree_builder/tab_walker.cc b/chrome/browser/ui/tabs/tab_strip_api/adapters/tree_builder/tab_walker.cc
index 37d0d76..cf6d24d 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/adapters/tree_builder/tab_walker.cc
+++ b/chrome/browser/ui/tabs/tab_strip_api/adapters/tree_builder/tab_walker.cc
@@ -22,7 +22,7 @@
   CHECK(contents);
   const ui::ColorProvider& provider = contents->GetColorProvider();
   mojom::TabPtr mojo_tab = converters::BuildMojoTab(
-      target_->GetHandle(), TabRendererData::FromTabInterface(target_),
+      target_,
       // TODO(crbug.com/438632110): this is dup code with the adapter. See if
       // we can combine state computation.
       provider,
diff --git a/chrome/browser/ui/tabs/tab_strip_api/converters/BUILD.gn b/chrome/browser/ui/tabs/tab_strip_api/converters/BUILD.gn
index 1385cc9..cac8018 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/converters/BUILD.gn
+++ b/chrome/browser/ui/tabs/tab_strip_api/converters/BUILD.gn
@@ -8,25 +8,40 @@
     "tab_converters.h",
   ]
 
+  # Remove this once tab_ui_helper.cc is modularized.
+  public_deps = [ "//chrome/browser:browser_public_dependencies" ]
+
   deps = [
+    "//base",
     "//chrome/browser/ui/tabs:tab_strip",
     "//chrome/browser/ui/tabs/alert:tab_alert",
     "//chrome/browser/ui/tabs/alert:tab_alert_enum",
     "//components/browser_apis/tab_strip:mojom",
-    "//components/browser_apis/tab_strip/types",
+    "//components/tabs:public",
+    "//ui/color",
   ]
 }
 
-source_set("unit_tests") {
+source_set("browser_tests") {
   testonly = true
 
-  sources = [ "tab_converters_unittest.cc" ]
+  defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]
+
+  sources = [ "tab_converters_browsertest.cc" ]
 
   deps = [
     ":converters",
+    "//base",
+    "//base/test:test_support",
+    "//chrome/browser/ui",
     "//chrome/browser/ui/tabs:tab_strip",
-    "//components/browser_apis/tab_strip:mojom",
+    "//chrome/browser/ui/tabs/alert:tab_alert",
+    "//chrome/test:test_support",
     "//components/browser_apis/tab_strip/types",
+    "//components/tabs:public",
+    "//content/test:test_support",
     "//testing/gtest",
+    "//ui/color",
+    "//url",
   ]
 }
diff --git a/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.cc b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.cc
index 835daf8..fb8c42ac 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.cc
+++ b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.cc
@@ -8,8 +8,10 @@
 #include "base/notreached.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/ui/tab_ui_helper.h"
 #include "chrome/browser/ui/tabs/alert/tab_alert.h"
 #include "chrome/browser/ui/tabs/alert/tab_alert_controller.h"
+#include "chrome/browser/ui/tabs/tab_network_state.h"
 #include "components/split_tabs/split_tab_visual_data.h"
 #include "components/tab_groups/tab_group_visual_data.h"
 #include "components/tabs/public/split_tab_collection.h"
@@ -84,26 +86,29 @@
   return result;
 }
 
-tabs_api::mojom::TabPtr BuildMojoTab(tabs::TabHandle handle,
-                                     const TabRendererData& data,
+tabs_api::mojom::TabPtr BuildMojoTab(tabs::TabInterface* tab,
                                      const ui::ColorProvider& color_provider,
                                      const TabStates& states) {
   auto result = tabs_api::mojom::Tab::New();
+  TabUIHelper* tab_ui_helper = TabUIHelper::From(tab);
+  CHECK(tab_ui_helper);
 
-  result->id = tabs_api::NodeId(tabs_api::NodeId::Type::kContent,
-                               base::NumberToString(handle.raw_value()));
-  result->title = base::UTF16ToUTF8(data.title);
-  result->favicon = data.favicon.Rasterize(&color_provider);
-  result->url = data.visible_url;
-  result->network_state = ToMojo(data.network_state);
-  if (handle.Get() != nullptr) {
-    result->alert_states = ToMojo(
-        tabs::TabAlertController::From(handle.Get())->GetAllActiveAlerts());
+  result->id =
+      tabs_api::NodeId(tabs_api::NodeId::Type::kContent,
+                       base::NumberToString(tab->GetHandle().raw_value()));
+  result->title = base::UTF16ToUTF8(tab_ui_helper->GetTitle());
+  result->favicon = tab_ui_helper->GetFavicon().Rasterize(&color_provider);
+  result->url = tab_ui_helper->GetVisibleURL();
+  result->network_state = ToMojo(tab_ui_helper->GetTabNetworkState());
+  if (tab->GetHandle().Get() != nullptr) {
+    result->alert_states =
+        ToMojo(tabs::TabAlertController::From(tab->GetHandle().Get())
+                   ->GetAllActiveAlerts());
   }
 
   result->is_active = states.is_active;
   result->is_selected = states.is_selected;
-  result->is_blocked = data.blocked;
+  result->is_blocked = tab->IsBlocked();
 
   return result;
 }
diff --git a/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.h b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.h
index 93ab4730..105c8cdc 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.h
+++ b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.h
@@ -18,8 +18,7 @@
   bool is_selected;
 };
 
-tabs_api::mojom::TabPtr BuildMojoTab(tabs::TabHandle handle,
-                                     const TabRendererData& data,
+tabs_api::mojom::TabPtr BuildMojoTab(tabs::TabInterface* tab,
                                      const ui::ColorProvider& color_provider,
                                      const TabStates& states);
 
diff --git a/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters_browsertest.cc b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters_browsertest.cc
new file mode 100644
index 0000000..6f589e27
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters_browsertest.cc
@@ -0,0 +1,95 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.h"
+
+#include "base/strings/string_number_conversions.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/tab_ui_helper.h"
+#include "chrome/browser/ui/tabs/alert/tab_alert_controller.h"
+#include "chrome/browser/ui/tabs/tab_network_state.h"
+#include "chrome/browser/ui/tabs/tab_renderer_data.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "components/browser_apis/tab_strip/tab_strip_api.mojom.h"
+#include "components/browser_apis/tab_strip/tab_strip_api_data_model.mojom-shared.h"
+#include "components/browser_apis/tab_strip/types/node_id.h"
+#include "components/tabs/public/tab_collection.h"
+#include "components/tabs/public/tab_interface.h"
+#include "content/public/test/browser_test.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/color/color_provider.h"
+#include "url/gurl.h"
+
+namespace tabs_api::converters {
+namespace {
+
+class FakeTabCollection : public tabs::TabCollection {
+ public:
+  explicit FakeTabCollection(Type type) : TabCollection(type, {}, true) {}
+  ~FakeTabCollection() override = default;
+};
+
+using TabStripServiceConvertersBrowserTest = InProcessBrowserTest;
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceConvertersBrowserTest, ConvertTab) {
+  // Use a real browser and tab for testing.
+  ASSERT_TRUE(AddTabAtIndex(0, GURL("chrome://newtab"),
+                            ui::PageTransition::PAGE_TRANSITION_LINK));
+
+  tabs::TabInterface* tab = browser()->tab_strip_model()->GetActiveTab();
+  ASSERT_TRUE(tab);
+
+  TabUIHelper* const tab_ui_helper = TabUIHelper::From(tab);
+  ASSERT_TRUE(tab_ui_helper);
+
+  const ui::ColorProvider& color_provider =
+      tab->GetContents()->GetColorProvider();
+
+  // Simulate setting some data that TabUIHelper would provide
+  tab_ui_helper->SetNeedsAttention(true);
+  auto mojo = BuildMojoTab(tab, color_provider,
+                           {
+                               .is_active = true,
+                               .is_selected = true,
+                           });
+
+  ASSERT_EQ(base::NumberToString(tab->GetHandle().raw_value()), mojo->id.Id());
+  ASSERT_EQ(NodeId::Type::kContent, mojo->id.Type());
+  ASSERT_EQ(tab_ui_helper->GetVisibleURL(), mojo->url);
+  ASSERT_EQ(base::UTF16ToUTF8(tab_ui_helper->GetTitle()), mojo->title);
+  ASSERT_TRUE(mojo->is_active);
+  ASSERT_TRUE(mojo->is_selected);
+  ASSERT_EQ(TabNetworkStateForWebContents(tab->GetContents()),
+            FromMojo(mojo->network_state));
+
+  std::vector<mojom::AlertState> tab_alerts = mojo->alert_states;
+  std::vector<tabs::TabAlert> mojom_tab_alerts = {};
+  mojom_tab_alerts.reserve(tab_alerts.size());
+
+  for (auto state : tab_alerts) {
+    mojom_tab_alerts.push_back(FromMojo(state));
+  }
+
+  ASSERT_EQ(tabs::TabAlertController::From(tab)->GetAllActiveAlerts(),
+            mojom_tab_alerts);
+  ASSERT_EQ(tab->IsBlocked(), mojo->is_blocked);
+}
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceConvertersBrowserTest,
+                       ConvertTabCollection) {
+  FakeTabCollection collection(tabs::TabCollection::Type::TABSTRIP);
+  const std::string expected_id =
+      base::NumberToString(collection.GetHandle().raw_value());
+  auto mojo = BuildMojoTabCollectionData(collection.GetHandle());
+
+  ASSERT_TRUE(mojo->is_tab_strip());
+
+  const auto& tab_strip = mojo->get_tab_strip();
+  ASSERT_EQ(expected_id, tab_strip->id.Id());
+  ASSERT_EQ(NodeId::Type::kCollection, tab_strip->id.Type());
+}
+
+}  // namespace
+}  // namespace tabs_api::converters
diff --git a/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters_unittest.cc b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters_unittest.cc
deleted file mode 100644
index ac0864e..0000000
--- a/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters_unittest.cc
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright 2025 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.h"
-
-#include "base/strings/string_number_conversions.h"
-#include "chrome/browser/ui/tabs/tab_renderer_data.h"
-#include "components/browser_apis/tab_strip/types/node_id.h"
-#include "components/tabs/public/tab_collection.h"
-#include "components/tabs/public/tab_interface.h"
-#include "testing/gtest/include/gtest/gtest.h"
-#include "ui/color/color_provider.h"
-#include "url/gurl.h"
-
-namespace tabs_api::converters {
-namespace {
-
-class FakeTabCollection : public tabs::TabCollection {
- public:
-  explicit FakeTabCollection(Type type) : TabCollection(type, {}, true) {}
-  ~FakeTabCollection() override = default;
-};
-
-TEST(TabStripServiceConverters, ConvertTab) {
-  tabs::TabHandle handle(888);
-  ui::ColorProvider color_provider;
-  TabRendererData data;
-  data.visible_url = GURL("http://nowhere");
-  data.title = std::u16string(u"title");
-
-  auto mojo = BuildMojoTab(handle, data, color_provider,
-                           {
-                               .is_active = true,
-                               .is_selected = true,
-                           });
-
-  ASSERT_EQ("888", mojo->id.Id());
-  ASSERT_EQ(NodeId::Type::kContent, mojo->id.Type());
-  ASSERT_EQ(GURL("http://nowhere"), mojo->url);
-  ASSERT_EQ("title", mojo->title);
-  ASSERT_TRUE(mojo->is_active);
-  ASSERT_TRUE(mojo->is_selected);
-}
-
-TEST(TabStripServiceConverters, ConvertTabCollection) {
-  FakeTabCollection collection(tabs::TabCollection::Type::TABSTRIP);
-  const std::string expected_id =
-      base::NumberToString(collection.GetHandle().raw_value());
-  auto mojo = BuildMojoTabCollectionData(collection.GetHandle());
-
-  ASSERT_TRUE(mojo->is_tab_strip());
-
-  const auto& tab_strip = mojo->get_tab_strip();
-  ASSERT_EQ(expected_id, tab_strip->id.Id());
-  ASSERT_EQ(NodeId::Type::kCollection, tab_strip->id.Type());
-}
-
-}  // namespace
-}  // namespace tabs_api::converters
diff --git a/chrome/browser/ui/tabs/tab_strip_api/events/event_transformation.cc b/chrome/browser/ui/tabs/tab_strip_api/events/event_transformation.cc
index 14be5f9..2d30d72 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/events/event_transformation.cc
+++ b/chrome/browser/ui/tabs/tab_strip_api/events/event_transformation.cc
@@ -26,11 +26,9 @@
   auto tab_created = tabs_api::mojom::TabCreatedContainer::New();
   tab_created->position = tabs_api::Position(
       position.index, adapter->GetPathForCollection(position.parent_handle));
-  auto renderer_data =
-      adapter->GetTabRendererData(adapter->GetIndexForHandle(handle).value());
   const ui::ColorProvider& provider = adapter->GetColorProvider();
   auto mojo_tab = tabs_api::converters::BuildMojoTab(
-      handle, renderer_data, provider, adapter->GetTabStates(handle));
+      handle.Get(), provider, adapter->GetTabStates(handle));
 
   tab_created->tab = std::move(mojo_tab);
   event->tabs.emplace_back(std::move(tab_created));
@@ -117,10 +115,10 @@
   auto tabs = adapter->GetTabs();
   if (index < tabs.size()) {
     auto& handle = tabs.at(index);
-    auto renderer_data = adapter->GetTabRendererData(index);
     const ui::ColorProvider& color_provider = adapter->GetColorProvider();
+
     auto mojo_tab = tabs_api::converters::BuildMojoTab(
-        handle, renderer_data, color_provider, adapter->GetTabStates(handle));
+        handle.Get(), color_provider, adapter->GetTabStates(handle));
     event->data = mojom::Data::NewTab(std::move(mojo_tab));
   }
 
@@ -166,12 +164,10 @@
       continue;
     }
     auto event = mojom::OnDataChangedEvent::New();
-    auto renderer_data = adapter->GetTabRendererData(
-        adapter->GetIndexForHandle(affected_tab).value());
     const ui::ColorProvider& color_provider = adapter->GetColorProvider();
-    auto mojo_tab = tabs_api::converters::BuildMojoTab(
-        affected_tab, renderer_data, color_provider,
-        adapter->GetTabStates(affected_tab));
+    auto mojo_tab =
+        tabs_api::converters::BuildMojoTab(affected_tab.Get(), color_provider,
+                                           adapter->GetTabStates(affected_tab));
     event->data = mojom::Data::NewTab(std::move(mojo_tab));
     events.push_back(std::move(event));
   }
diff --git a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.cc b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.cc
index 839b5be8..0c9896e9 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.cc
+++ b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.cc
@@ -130,11 +130,10 @@
   for (unsigned int i = 0; i < tabs.size(); ++i) {
     auto& handle = tabs.at(i);
     if (tab_id == handle.raw_value()) {
-      auto renderer_data = tab_strip_model_adapter_->GetTabRendererData(i);
       const ui::ColorProvider& color_provider =
           tab_strip_model_adapter_->GetColorProvider();
       tab_result = tabs_api::converters::BuildMojoTab(
-          handle, renderer_data, color_provider,
+          handle.Get(), color_provider,
           tab_strip_model_adapter_->GetTabStates(handle));
     }
   }
@@ -184,12 +183,10 @@
         "Could not find the index of the newly created tab"));
   }
 
-  auto renderer_data =
-      tab_strip_model_adapter_->GetTabRendererData(tab_index.value());
   const ui::ColorProvider& color_provider =
       tab_strip_model_adapter_->GetColorProvider();
   auto mojo_tab = tabs_api::converters::BuildMojoTab(
-      tab_handle, renderer_data, color_provider,
+      tab_handle.Get(), color_provider,
       tab_strip_model_adapter_->GetTabStates(tab_handle));
   return mojo_tab->Clone();
 }
diff --git a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_browsertest.cc b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_browsertest.cc
index e8889f85..5ef12b1b 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_browsertest.cc
+++ b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_browsertest.cc
@@ -17,6 +17,9 @@
 #include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
 #include "chrome/browser/ui/tabs/split_tab_metrics.h"
 #include "chrome/browser/ui/tabs/tab_group_model.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter_impl.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.h"
 #include "chrome/browser/ui/tabs/tab_strip_api/observation/tab_strip_api_batched_observer.h"
 #include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_mojo_handler.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
@@ -272,6 +275,210 @@
   std::unique_ptr<TabStripServiceMojoHandler> tab_strip_service_mojo_handler_;
 };
 
+class TabStripServiceDirectBrowserTest : public InProcessBrowserTest {
+ public:
+  TabStripServiceDirectBrowserTest() {
+    feature_list_.InitWithFeatures({features::kTabStripBrowserApi}, {});
+  }
+
+  void SetUpOnMainThread() override {
+    InProcessBrowserTest::SetUpOnMainThread();
+    service_ = std::make_unique<tabs_api::TabStripServiceImpl>(
+        std::make_unique<tabs_api::BrowserAdapterImpl>(browser()),
+        std::make_unique<tabs_api::TabStripModelAdapterImpl>(
+            browser()->tab_strip_model(),
+            base::NumberToString(browser()->GetSessionID().id())));
+  }
+
+  void TearDownOnMainThread() override { service_.reset(); }
+
+ protected:
+  TabStripModel* GetTabStripModel() { return browser()->tab_strip_model(); }
+
+  base::test::ScopedFeatureList feature_list_;
+  std::unique_ptr<tabs_api::TabStripServiceImpl> service_;
+};
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceDirectBrowserTest, CreateNewTab) {
+  TabStripModel* model = GetTabStripModel();
+
+  ASSERT_EQ(1, model->count());
+
+  auto result = service_->CreateTabAt(std::nullopt, std::nullopt);
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(2, model->count());
+
+  tabs::TabHandle created_handle = model->GetTabAtIndex(1)->GetHandle();
+  ASSERT_EQ(base::NumberToString(created_handle.raw_value()),
+            result.value()->id.Id());
+  ASSERT_EQ(tabs_api::NodeId::Type::kContent, result.value()->id.Type());
+}
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceDirectBrowserTest, GetTabs) {
+  auto result = service_->GetTabs();
+
+  ASSERT_TRUE(result.has_value());
+  const auto& window = result.value();
+  ASSERT_TRUE(window->data->is_window());
+  ASSERT_EQ(1u, window->children.size());
+
+  const auto& tab_strip = window->children[0];
+  ASSERT_TRUE(tab_strip->data->is_tab_strip());
+
+  // Root collection has Pinned and Unpinned collections.
+  ASSERT_EQ(2u, tab_strip->children.size());
+  ASSERT_TRUE(tab_strip->children[0]->data->is_pinned_tabs());
+  ASSERT_TRUE(tab_strip->children[1]->data->is_unpinned_tabs());
+
+  const auto& unpinned_tabs = tab_strip->children[1];
+  // The browser starts with 1 tab by default in the unpinned collection.
+  ASSERT_EQ(1u, unpinned_tabs->children.size());
+  ASSERT_TRUE(unpinned_tabs->children[0]->data->is_tab());
+
+  auto handle = GetTabStripModel()->GetTabAtIndex(0)->GetHandle();
+  ASSERT_EQ(base::NumberToString(handle.raw_value()),
+            unpinned_tabs->children[0]->data->get_tab()->id.Id());
+  ASSERT_EQ(tabs_api::NodeId::Type::kContent,
+            unpinned_tabs->children[0]->data->get_tab()->id.Type());
+}
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceDirectBrowserTest, GetTab) {
+  auto handle = GetTabStripModel()->GetTabAtIndex(0)->GetHandle();
+  tabs_api::NodeId tab_id(tabs_api::NodeId::Type::kContent,
+                          base::NumberToString(handle.raw_value()));
+  auto result = service_->GetTab(tab_id);
+
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(result.value()->id.Id(), base::NumberToString(handle.raw_value()));
+  ASSERT_EQ(result.value()->id.Type(), tabs_api::NodeId::Type::kContent);
+}
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceDirectBrowserTest, GetTab_NotFound) {
+  tabs_api::NodeId tab_id(tabs_api::NodeId::Type::kContent, "666");
+
+  auto result = service_->GetTab(tab_id);
+
+  ASSERT_FALSE(result.has_value());
+  ASSERT_EQ(result.error()->code, mojo_base::mojom::Code::kNotFound);
+}
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceDirectBrowserTest, CloseTabs) {
+  // Add a tab so we can close it without closing the browser.
+  auto create_result = service_->CreateTabAt(std::nullopt, std::nullopt);
+  ASSERT_TRUE(create_result.has_value());
+  ASSERT_EQ(2, GetTabStripModel()->count());
+
+  auto result = service_->CloseTabs({create_result.value()->id});
+
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(1, GetTabStripModel()->count());
+}
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceDirectBrowserTest, ActivateTab) {
+  auto tab1_handle = GetTabStripModel()->GetTabAtIndex(0)->GetHandle();
+
+  auto create_result = service_->CreateTabAt(std::nullopt, std::nullopt);
+  auto tab2_handle = GetTabStripModel()->GetTabAtIndex(1)->GetHandle();
+
+  // New tab should be activated.
+  ASSERT_EQ(GetTabStripModel()->GetActiveTab()->GetHandle(), tab2_handle);
+
+  tabs_api::NodeId tab1_id(tabs_api::NodeId::Type::kContent,
+                           base::NumberToString(tab1_handle.raw_value()));
+
+  auto result = service_->ActivateTab(tab1_id);
+  ASSERT_TRUE(result.has_value());
+
+  ASSERT_EQ(GetTabStripModel()->GetActiveTab()->GetHandle(), tab1_handle);
+}
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceDirectBrowserTest, ActivateTab_NotFound) {
+  tabs_api::NodeId tab_id(tabs_api::NodeId::Type::kContent, "111");
+
+  auto result = service_->ActivateTab(tab_id);
+
+  ASSERT_FALSE(result.has_value());
+  ASSERT_EQ(result.error()->code, mojo_base::mojom::Code::kNotFound);
+}
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceDirectBrowserTest, SetSelectedTabs) {
+  auto tab1_handle = GetTabStripModel()->GetTabAtIndex(0)->GetHandle();
+  auto create_result = service_->CreateTabAt(std::nullopt, std::nullopt);
+
+  tabs_api::NodeId tab1_id(tabs_api::NodeId::Type::kContent,
+                           base::NumberToString(tab1_handle.raw_value()));
+  tabs_api::NodeId tab2_id = create_result.value()->id;
+
+  // tab2 is currently active and selected.
+  ASSERT_TRUE(GetTabStripModel()->GetTabAtIndex(1)->IsActivated());
+
+  auto result = service_->SetSelectedTabs({tab1_id}, tab1_id);
+  ASSERT_TRUE(result.has_value());
+
+  ASSERT_TRUE(GetTabStripModel()->GetTabAtIndex(0)->IsActivated());
+  ASSERT_TRUE(GetTabStripModel()->GetTabAtIndex(0)->IsSelected());
+  ASSERT_FALSE(GetTabStripModel()->GetTabAtIndex(1)->IsActivated());
+  ASSERT_FALSE(GetTabStripModel()->GetTabAtIndex(1)->IsSelected());
+}
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceDirectBrowserTest,
+                       SetSelectedTabs_MultipleSelection) {
+  // Browser starts with 1 tab. Add 3 more for a total of 4.
+  auto create_tab_result = service_->CreateTabAt(std::nullopt, std::nullopt);
+  ASSERT_TRUE(create_tab_result.has_value());
+  create_tab_result = service_->CreateTabAt(std::nullopt, std::nullopt);
+  ASSERT_TRUE(create_tab_result.has_value());
+  create_tab_result = service_->CreateTabAt(std::nullopt, std::nullopt);
+  ASSERT_TRUE(create_tab_result.has_value());
+
+  ASSERT_EQ(4, GetTabStripModel()->count());
+
+  std::vector<tabs_api::NodeId> selection;
+  for (int i = 0; i < 4; ++i) {
+    selection.push_back(tabs_api::NodeId::FromTabHandle(
+        GetTabStripModel()->GetTabAtIndex(i)->GetHandle()));
+  }
+
+  tabs_api::NodeId active_id = selection.back();
+
+  auto result = service_->SetSelectedTabs(selection, active_id);
+  ASSERT_TRUE(result.has_value());
+
+  for (int i = 0; i < 4; ++i) {
+    ASSERT_TRUE(GetTabStripModel()->GetTabAtIndex(i)->IsSelected());
+  }
+  ASSERT_TRUE(GetTabStripModel()->GetTabAtIndex(3)->IsActivated());
+}
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceDirectBrowserTest, MoveTab) {
+  // Add 2 more tabs for a total of 3.
+  auto create_tab_result = service_->CreateTabAt(std::nullopt, std::nullopt);
+  ASSERT_TRUE(create_tab_result.has_value());
+  create_tab_result = service_->CreateTabAt(std::nullopt, std::nullopt);
+  ASSERT_TRUE(create_tab_result.has_value());
+
+  auto handle_to_move = GetTabStripModel()->GetTabAtIndex(0)->GetHandle();
+  tabs_api::NodeId to_move_id(tabs_api::NodeId::Type::kContent,
+                              base::NumberToString(handle_to_move.raw_value()));
+
+  // Move tab 0 to index 2.
+  auto result = service_->MoveNode(to_move_id, tabs_api::Position(2));
+  ASSERT_TRUE(result.has_value());
+
+  ASSERT_EQ(GetTabStripModel()->GetTabAtIndex(2)->GetHandle(), handle_to_move);
+}
+
+IN_PROC_BROWSER_TEST_F(TabStripServiceDirectBrowserTest, MoveTab_OutOfRange) {
+  auto handle = GetTabStripModel()->GetTabAtIndex(0)->GetHandle();
+  tabs_api::NodeId tab_id(tabs_api::NodeId::Type::kContent,
+                          base::NumberToString(handle.raw_value()));
+
+  auto result = service_->MoveNode(tab_id, tabs_api::Position(9001));
+
+  ASSERT_FALSE(result.has_value());
+  ASSERT_EQ(result.error()->code, mojo_base::mojom::Code::kInvalidArgument);
+}
+
 IN_PROC_BROWSER_TEST_F(TabStripServiceImplBrowserTest, SynchronousObserver) {
   ReallyVerySimpleSyncObserver observer;
 
diff --git a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_unittest.cc b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_unittest.cc
deleted file mode 100644
index 397b9b92..0000000
--- a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_unittest.cc
+++ /dev/null
@@ -1,297 +0,0 @@
-// Copyright 2025 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.h"
-
-#include "base/run_loop.h"
-#include "base/strings/string_number_conversions.h"
-#include "base/test/bind.h"
-#include "base/test/task_environment.h"
-#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
-#include "chrome/browser/ui/browser_window/test/mock_browser_window_interface.h"
-#include "chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter.h"
-#include "chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter.h"
-#include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.h"
-#include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_mojo_handler.h"
-#include "chrome/browser/ui/tabs/tab_strip_api/testing/toy_tab_strip.h"
-#include "chrome/browser/ui/tabs/tab_strip_api/testing/toy_tab_strip_browser_adapter.h"
-#include "chrome/browser/ui/tabs/tab_strip_api/testing/toy_tab_strip_model_adapter.h"
-#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
-#include "chrome/test/base/testing_profile.h"
-#include "components/browser_apis/tab_strip/tab_strip_api.mojom.h"
-#include "components/browser_apis/tab_strip/types/node_id.h"
-#include "components/tabs/public/tab_collection.h"
-#include "components/tabs/public/tab_interface.h"
-#include "content/public/test/browser_task_environment.h"
-#include "mojo/public/cpp/bindings/receiver.h"
-#include "mojo/public/cpp/bindings/remote.h"
-#include "mojo/public/mojom/base/error.mojom.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace tabs_api {
-namespace {
-
-// Really a hermatic integration test.
-class TabStripServiceImplTest : public ::testing::Test {
- protected:
-  TabStripServiceImplTest() = default;
-  TabStripServiceImplTest(const TabStripServiceImplTest&) = delete;
-  TabStripServiceImplTest operator=(const TabStripServiceImplTest&) = delete;
-  ~TabStripServiceImplTest() override = default;
-
-  void SetUp() override {
-    tab_strip_ = std::make_unique<testing::ToyTabStrip>();
-    service_ = std::make_unique<TabStripServiceImpl>(
-        std::make_unique<testing::ToyTabStripBrowserAdapter>(tab_strip_.get()),
-        std::make_unique<testing::ToyTabStripModelAdapter>(tab_strip_.get()));
-  }
-
- protected:
-  std::unique_ptr<testing::ToyTabStrip> tab_strip_;
-  std::unique_ptr<TabStripServiceImpl> service_;
-
- private:
-  content::BrowserTaskEnvironment task_environment_;
-};
-
-TEST_F(TabStripServiceImplTest, CreateNewTab) {
-  // We should start with nothing.
-  ASSERT_EQ(0ul, tab_strip_->GetTabs().size());
-
-  auto result = service_->CreateTabAt(std::nullopt, std::nullopt);
-
-  ASSERT_TRUE(result.has_value());
-
-  // One tab should have been created. Now we assert its shape.
-  ASSERT_EQ(1ul, tab_strip_->GetTabs().size());
-  auto created = tab_strip_->GetTabs().at(0);
-  ASSERT_EQ(base::NumberToString(created.raw_value()), result.value()->id.Id());
-  ASSERT_EQ(NodeId::Type::kContent, result.value()->id.Type());
-}
-
-TEST_F(TabStripServiceImplTest, GetTabs) {
-  tab_strip_->AddTab({tabs::TabHandle(888), GURL("hihi")});
-
-  auto result = service_->GetTabs();
-
-  const auto& window = result.value();
-  ASSERT_TRUE(window->data->is_window());
-  ASSERT_EQ(1u, window->children.size());
-  const auto& tab_strip = window->children[0];
-  ASSERT_TRUE(tab_strip->data->is_tab_strip());
-  ASSERT_EQ(1u, tab_strip->children.size());
-  ASSERT_TRUE(tab_strip->children[0]->data->is_tab());
-  ASSERT_EQ("888", tab_strip->children[0]->data->get_tab()->id.Id());
-  ASSERT_EQ(NodeId::Type::kContent,
-            tab_strip->children[0]->data->get_tab()->id.Type());
-}
-
-TEST_F(TabStripServiceImplTest, GetTab) {
-  tab_strip_->AddTab({tabs::TabHandle(666), GURL("hihi")});
-
-  tabs_api::NodeId tab_id(NodeId::Type::kContent, "666");
-  auto result = service_->GetTab(tab_id);
-
-  ASSERT_TRUE(result.has_value());
-  ASSERT_EQ(result.value()->id.Id(), "666");
-  ASSERT_EQ(result.value()->id.Type(), NodeId::Type::kContent);
-}
-
-TEST_F(TabStripServiceImplTest, GetTab_NotFound) {
-  tabs_api::NodeId tab_id(NodeId::Type::kContent, "666");
-
-  auto result = service_->GetTab(tab_id);
-
-  ASSERT_FALSE(result.has_value());
-  ASSERT_EQ(result.error()->code, mojo_base::mojom::Code::kNotFound);
-}
-
-TEST_F(TabStripServiceImplTest, CloseTabs) {
-  tabs_api::NodeId tab_id1(NodeId::Type::kContent, "123");
-  tabs_api::NodeId tab_id2(NodeId::Type::kContent, "321");
-
-  // insert fake tab entries.
-  tab_strip_->AddTab({tabs::TabHandle(123), GURL("1")});
-  tab_strip_->AddTab({tabs::TabHandle(321), GURL("2")});
-
-  auto result = service_->CloseTabs({tab_id1, tab_id2});
-
-  ASSERT_TRUE(result.has_value());
-  // tab entries should be removed.
-  ASSERT_EQ(0ul, tab_strip_->GetTabs().size());
-}
-
-TEST_F(TabStripServiceImplTest, ActivateTab) {
-  // We start with this being active.
-  auto tab1 = testing::ToyTab{
-      tabs::TabHandle(1),
-      GURL("1"),
-  };
-
-  // And end with this one being active.
-  auto tab2 = testing::ToyTab{
-      tabs::TabHandle(2),
-      GURL("1"),
-  };
-
-  tab_strip_->AddTab(tab1);
-  tab_strip_->AddTab(tab2);
-  tab_strip_->ActivateTab(tab1.tab_handle);
-  ASSERT_EQ(tab_strip_->FindActiveTab(), tab1.tab_handle);
-
-  tabs_api::NodeId tab2_id(NodeId::Type::kContent,
-                          base::NumberToString(tab2.tab_handle.raw_value()));
-
-  auto result = service_->ActivateTab(tab2_id);
-
-  ASSERT_EQ(tab_strip_->FindActiveTab(), tab2.tab_handle);
-}
-
-TEST_F(TabStripServiceImplTest, ActivateTab_NotFound) {
-  tabs_api::NodeId tab2_id(NodeId::Type::kContent, "111");
-
-  auto result = service_->ActivateTab(tab2_id);
-
-  ASSERT_EQ(result.error()->code, mojo_base::mojom::Code::kNotFound);
-}
-
-TEST_F(TabStripServiceImplTest, SetSelectedTabs) {
-  // We start with this being active (and therefore selected).
-  auto tab1 = testing::ToyTab{
-      tabs::TabHandle(1),
-      GURL("1"),
-  };
-
-  // And end with this one being active.
-  auto tab2 = testing::ToyTab{
-      tabs::TabHandle(2),
-      GURL("1"),
-  };
-
-  tab_strip_->AddTab(tab1);
-  tab_strip_->AddTab(tab2);
-  tab_strip_->ActivateTab(tab1.tab_handle);
-
-  ASSERT_FALSE(tab_strip_->GetToyTabFor(tab2.tab_handle).active);
-  ASSERT_FALSE(tab_strip_->GetToyTabFor(tab2.tab_handle).selected);
-
-  ASSERT_TRUE(tab_strip_->GetToyTabFor(tab1.tab_handle).active);
-  ASSERT_TRUE(tab_strip_->GetToyTabFor(tab1.tab_handle).selected);
-
-  tabs_api::NodeId tab2_id(NodeId::Type::kContent,
-                           base::NumberToString(tab2.tab_handle.raw_value()));
-
-  auto result = service_->SetSelectedTabs({tab2_id}, tab2_id);
-
-  ASSERT_TRUE(tab_strip_->GetToyTabFor(tab2.tab_handle).active);
-  ASSERT_TRUE(tab_strip_->GetToyTabFor(tab2.tab_handle).selected);
-
-  ASSERT_FALSE(tab_strip_->GetToyTabFor(tab1.tab_handle).active);
-  ASSERT_FALSE(tab_strip_->GetToyTabFor(tab1.tab_handle).selected);
-}
-
-TEST_F(TabStripServiceImplTest, SetSelectedTabs_MultipleSelection) {
-  auto tab1 = testing::ToyTab{
-      tabs::TabHandle(1),
-      GURL("1"),
-  };
-
-  auto tab2 = testing::ToyTab{
-      tabs::TabHandle(2),
-      GURL("1"),
-  };
-
-  auto tab3 = testing::ToyTab{
-      tabs::TabHandle(3),
-      GURL("1"),
-  };
-
-  auto tab4 = testing::ToyTab{
-      tabs::TabHandle(4),
-      GURL("1"),
-  };
-
-  tab_strip_->AddTab(tab1);
-  tab_strip_->AddTab(tab2);
-  tab_strip_->AddTab(tab3);
-  tab_strip_->AddTab(tab4);
-
-  tabs_api::NodeId tab1_id(NodeId::Type::kContent,
-                           base::NumberToString(tab1.tab_handle.raw_value()));
-  tabs_api::NodeId tab2_id(NodeId::Type::kContent,
-                           base::NumberToString(tab2.tab_handle.raw_value()));
-  tabs_api::NodeId tab3_id(NodeId::Type::kContent,
-                           base::NumberToString(tab3.tab_handle.raw_value()));
-  tabs_api::NodeId tab4_id(NodeId::Type::kContent,
-                           base::NumberToString(tab4.tab_handle.raw_value()));
-
-  auto result =
-      service_->SetSelectedTabs({tab1_id, tab2_id, tab3_id, tab4_id}, tab4_id);
-
-  ASSERT_FALSE(tab_strip_->GetToyTabFor(tab1.tab_handle).active);
-  ASSERT_TRUE(tab_strip_->GetToyTabFor(tab1.tab_handle).selected);
-
-  ASSERT_FALSE(tab_strip_->GetToyTabFor(tab2.tab_handle).active);
-  ASSERT_TRUE(tab_strip_->GetToyTabFor(tab2.tab_handle).selected);
-
-  ASSERT_FALSE(tab_strip_->GetToyTabFor(tab3.tab_handle).active);
-  ASSERT_TRUE(tab_strip_->GetToyTabFor(tab3.tab_handle).selected);
-
-  ASSERT_TRUE(tab_strip_->GetToyTabFor(tab4.tab_handle).active);
-  ASSERT_TRUE(tab_strip_->GetToyTabFor(tab4.tab_handle).selected);
-}
-
-TEST_F(TabStripServiceImplTest, MoveTab) {
-  // Move the first tab to the last spot.
-  tab_strip_->AddTab(testing::ToyTab{
-      tabs::TabHandle(1),
-      GURL("1"),
-  });
-  tab_strip_->AddTab(testing::ToyTab{
-      tabs::TabHandle(2),
-      GURL("2"),
-  });
-  tab_strip_->AddTab(testing::ToyTab{
-      tabs::TabHandle(3),
-      GURL("3"),
-  });
-
-  tabs_api::NodeId tab_id(NodeId::Type::kContent, "1");
-
-  auto position = tabs_api::Position(2);
-
-  auto target_handle = tabs::TabHandle(1);
-  // Check that the target is at the beginning before the move.
-  ASSERT_EQ(0, tab_strip_->GetIndexForHandle(target_handle).value());
-
-  auto result = service_->MoveNode(tab_id, std::move(position));
-
-  ASSERT_TRUE(result.has_value());
-
-  // Check that the target is now at the end.
-  ASSERT_EQ(2, tab_strip_->GetIndexForHandle(target_handle).value());
-}
-
-// TODO(crbug.com/422263248): figure out a better way to test for common
-// validations. No point covering each of them in the test (or maybe just
-// a common framework to ensure that it is being checked?).
-
-TEST_F(TabStripServiceImplTest, MoveTab_OutOfRange) {
-  tab_strip_->AddTab(testing::ToyTab{
-      tabs::TabHandle(1),
-      GURL("1"),
-  });
-
-  tabs_api::NodeId tab_id(NodeId::Type::kContent, "1");
-
-  auto position = tabs_api::Position(9001);
-
-  auto result = service_->MoveNode(tab_id, std::move(position));
-
-  ASSERT_FALSE(result.has_value());
-  ASSERT_EQ(result.error()->code, mojo_base::mojom::Code::kInvalidArgument);
-}
-
-}  // namespace
-}  // namespace tabs_api
diff --git a/chrome/browser/ui/tabs/tab_strip_api/testing/toy_tab_strip_model_adapter.cc b/chrome/browser/ui/tabs/tab_strip_api/testing/toy_tab_strip_model_adapter.cc
index 9b37d9a..ee148011 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/testing/toy_tab_strip_model_adapter.cc
+++ b/chrome/browser/ui/tabs/tab_strip_api/testing/toy_tab_strip_model_adapter.cc
@@ -29,10 +29,6 @@
   return tab_strip_->GetTabs();
 }
 
-TabRendererData ToyTabStripModelAdapter::GetTabRendererData(int index) const {
-  return TabRendererData();
-}
-
 tabs_api::converters::TabStates ToyTabStripModelAdapter::GetTabStates(
     tabs::TabHandle handle) const {
   return {
diff --git a/chrome/browser/ui/tabs/tab_strip_api/testing/toy_tab_strip_model_adapter.h b/chrome/browser/ui/tabs/tab_strip_api/testing/toy_tab_strip_model_adapter.h
index d546bb2..8b4f2c02 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/testing/toy_tab_strip_model_adapter.h
+++ b/chrome/browser/ui/tabs/tab_strip_api/testing/toy_tab_strip_model_adapter.h
@@ -28,7 +28,6 @@
   void RemoveCollectionObserver(
       tabs::TabCollectionObserver* collection_observer) override;
   std::vector<tabs::TabHandle> GetTabs() const override;
-  TabRendererData GetTabRendererData(int index) const override;
   tabs_api::converters::TabStates GetTabStates(
       tabs::TabHandle handle) const override;
   const ui::ColorProvider& GetColorProvider() const override;
diff --git a/chrome/browser/ui/tabs/tab_strip_model.cc b/chrome/browser/ui/tabs/tab_strip_model.cc
index 26327dd..4c246d4 100644
--- a/chrome/browser/ui/tabs/tab_strip_model.cc
+++ b/chrome/browser/ui/tabs/tab_strip_model.cc
@@ -1852,86 +1852,6 @@
   return active_tab && active_tab->IsSplit();
 }
 
-std::optional<split_tabs::SplitTabId>
-TabStripModel::InsertionBreaksSplitContiguity(int index) {
-  CHECK(index >= 0 && index <= count());
-  if (!ContainsIndex(index)) {
-    return std::nullopt;
-  }
-  tabs::TabInterface* tab = GetTabAtIndex(index);
-  if (tab->IsSplit() &&
-      contents_data_->GetSplitTabCollection(tab->GetSplit().value())
-              ->GetIndexOfTab(tab) > 0) {
-    return tab->GetSplit();
-  }
-  return std::nullopt;
-}
-
-std::optional<split_tabs::SplitTabId> TabStripModel::MoveBreaksSplitContiguity(
-    int start_index,
-    int length,
-    int final_index) {
-  // The logic for finding the previous and next tabs depends on
-  //  the relative position of the start_index and final_index as the indices of
-  //  the previous tab and next tab get updated if start_index < final_index but
-  //  otherwise the ordering is the same.
-  const int previous_tab_index =
-      start_index < final_index ? final_index - 1 + length : final_index - 1;
-
-  const int next_tab_index = previous_tab_index + 1;
-
-  if (!ContainsIndex(previous_tab_index) || !ContainsIndex(next_tab_index)) {
-    return std::nullopt;
-  }
-
-  std::optional<split_tabs::SplitTabId> previous_split =
-      GetSplitForTab(previous_tab_index);
-  std::optional<split_tabs::SplitTabId> next_split =
-      GetSplitForTab(next_tab_index);
-
-  // If both previous and next splits are nullopt this will return nullopt.
-  return (previous_split == next_split) ? previous_split : std::nullopt;
-}
-
-void TabStripModel::MaybeRemoveSplitsForMove(
-    int initial_index,
-    int final_index,
-    const std::optional<tab_groups::TabGroupId> group,
-    bool pin) {
-  tabs::TabInterface* const tab = GetTabAtIndex(initial_index);
-  const bool pinned_state_changed = tab->IsPinned() != pin;
-  const bool group_state_changed = tab->GetGroup() != group;
-
-  // This expects the tab should move in the collection hierarchy tree.
-  CHECK((initial_index != final_index) || pinned_state_changed ||
-        group_state_changed);
-
-  // If the move is within a split collection there is no need to remove any
-  // split.
-  if (tab->IsSplit() &&
-      tab->GetSplit() == GetTabAtIndex(final_index)->GetSplit() &&
-      !pinned_state_changed && !group_state_changed) {
-    return;
-  }
-
-  // Remove the split of the origin tab if it is not moving within the
-  // split collection.
-  if (tab->IsSplit()) {
-    RemoveSplitImpl(tab->GetSplit().value(),
-                    SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
-  }
-
-  // Maybe remove the split tab of the destination if it results in
-  // discontiguity.
-  std::optional<split_tabs::SplitTabId> destination_split =
-      MoveBreaksSplitContiguity(initial_index, 1, final_index);
-
-  if (destination_split.has_value()) {
-    RemoveSplitImpl(destination_split.value(),
-                    SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
-  }
-}
-
 void TabStripModel::UpdateSplitLayout(split_tabs::SplitTabId split_id,
                                       split_tabs::SplitTabLayout tab_layout) {
   ReentrancyCheck reentrancy_check(&reentrancy_guard_);
@@ -2061,6 +1981,10 @@
   CHECK(std::ranges::is_sorted(indices));
   CHECK(std::ranges::adjacent_find(indices) == indices.end());
 
+  // Extensions API may call this function on indices that contain only part of
+  // a split. In that case, unsplit said split tabs.
+  RemovePartialSplits(indices);
+
   // The odds of |new_group| colliding with an existing group are astronomically
   // low. If there is a collision, a DCHECK will fail in |AddToNewGroupImpl()|,
   // in which case there is probably something wrong with
@@ -2092,6 +2016,10 @@
   CHECK(ContainsIndex(*(indices.begin())));
   CHECK(ContainsIndex(*(indices.rbegin())));
 
+  // Extensions API may call this function on indices that contain only part of
+  // a split. In that case, unsplit said split tabs.
+  RemovePartialSplits(indices);
+
   AddToExistingGroupImpl(indices, group, add_to_end);
 }
 
@@ -2119,6 +2047,10 @@
     return;
   }
 
+  // Tab groups sync may call this function on indices that contain only part of
+  // a split. In that case, unsplit those split tabs.
+  RemovePartialSplits(indices);
+
   std::map<tab_groups::TabGroupId, std::vector<int>> indices_per_tab_group;
 
   for (int index : indices) {
@@ -2165,13 +2097,7 @@
 void TabStripModel::RemoveSplit(split_tabs::SplitTabId split_id) {
   ReentrancyCheck reentrancy_check(&reentrancy_guard_);
 
-  for (tabs::TabInterface* foreground_tab : GetForegroundTabs()) {
-    if (!foreground_tab->IsActivated()) {
-      static_cast<tabs::TabModel*>(foreground_tab)
-          ->WillBecomeHidden(base::PassKey<TabStripModel>());
-    }
-  }
-
+  NotifyInactiveSplitTabWillBecomeHidden(split_id);
   RemoveSplitImpl(split_id,
                   SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
 
@@ -5537,6 +5463,107 @@
   return split_data->GetIndexRange();
 }
 
+std::optional<split_tabs::SplitTabId>
+TabStripModel::InsertionBreaksSplitContiguity(int index) {
+  CHECK(index >= 0 && index <= count());
+  if (!ContainsIndex(index)) {
+    return std::nullopt;
+  }
+  tabs::TabInterface* tab = GetTabAtIndex(index);
+  if (tab->IsSplit() &&
+      contents_data_->GetSplitTabCollection(tab->GetSplit().value())
+              ->GetIndexOfTab(tab) > 0) {
+    return tab->GetSplit();
+  }
+  return std::nullopt;
+}
+
+std::optional<split_tabs::SplitTabId> TabStripModel::MoveBreaksSplitContiguity(
+    int start_index,
+    int length,
+    int final_index) {
+  // The logic for finding the previous and next tabs depends on
+  //  the relative position of the start_index and final_index as the indices of
+  //  the previous tab and next tab get updated if start_index < final_index but
+  //  otherwise the ordering is the same.
+  const int previous_tab_index =
+      start_index < final_index ? final_index - 1 + length : final_index - 1;
+
+  const int next_tab_index = previous_tab_index + 1;
+
+  if (!ContainsIndex(previous_tab_index) || !ContainsIndex(next_tab_index)) {
+    return std::nullopt;
+  }
+
+  std::optional<split_tabs::SplitTabId> previous_split =
+      GetSplitForTab(previous_tab_index);
+  std::optional<split_tabs::SplitTabId> next_split =
+      GetSplitForTab(next_tab_index);
+
+  // If both previous and next splits are nullopt this will return nullopt.
+  return (previous_split == next_split) ? previous_split : std::nullopt;
+}
+
+void TabStripModel::MaybeRemoveSplitsForMove(
+    int initial_index,
+    int final_index,
+    const std::optional<tab_groups::TabGroupId> group,
+    bool pin) {
+  tabs::TabInterface* const tab = GetTabAtIndex(initial_index);
+  const bool pinned_state_changed = tab->IsPinned() != pin;
+  const bool group_state_changed = tab->GetGroup() != group;
+
+  // This expects the tab should move in the collection hierarchy tree.
+  CHECK((initial_index != final_index) || pinned_state_changed ||
+        group_state_changed);
+
+  // If the move is within a split collection there is no need to remove any
+  // split.
+  if (tab->IsSplit() &&
+      tab->GetSplit() == GetTabAtIndex(final_index)->GetSplit() &&
+      !pinned_state_changed && !group_state_changed) {
+    return;
+  }
+
+  // Remove the split of the origin tab if it is not moving within the
+  // split collection.
+  if (tab->IsSplit()) {
+    RemoveSplitImpl(tab->GetSplit().value(),
+                    SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
+  }
+
+  // Maybe remove the split tab of the destination if it results in
+  // discontiguity.
+  std::optional<split_tabs::SplitTabId> destination_split =
+      MoveBreaksSplitContiguity(initial_index, 1, final_index);
+
+  if (destination_split.has_value()) {
+    RemoveSplitImpl(destination_split.value(),
+                    SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
+  }
+}
+
+void TabStripModel::RemovePartialSplits(const std::vector<int>& indices) {
+  std::map<split_tabs::SplitTabId, size_t> num_tabs_per_split;
+
+  for (int index : indices) {
+    std::optional<split_tabs::SplitTabId> split = GetSplitForTab(index);
+    if (!split.has_value()) {
+      continue;
+    }
+    num_tabs_per_split[*split]++;
+  }
+
+  for (const auto& [split, count] : num_tabs_per_split) {
+    if (count <
+        contents_data_->GetSplitTabCollection(split)->TabCountRecursive()) {
+      NotifyInactiveSplitTabWillBecomeHidden(split);
+      RemoveSplitImpl(split,
+                      SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
+    }
+  }
+}
+
 void TabStripModel::NotifyForegroundTabsWillEnterBackground() {
   for (tabs::TabInterface* tab : GetForegroundTabs()) {
     if (tab->IsActivated()) {
@@ -5548,6 +5575,20 @@
   }
 }
 
+void TabStripModel::NotifyInactiveSplitTabWillBecomeHidden(
+    split_tabs::SplitTabId split_id) {
+  if (GetActiveTab()->GetSplit() != split_id) {
+    return;
+  }
+
+  for (tabs::TabInterface* foreground_tab : GetForegroundTabs()) {
+    if (!foreground_tab->IsActivated()) {
+      static_cast<tabs::TabModel*>(foreground_tab)
+          ->WillBecomeHidden(base::PassKey<TabStripModel>());
+    }
+  }
+}
+
 TabStripModel::ScopedTabStripModalUIImpl::ScopedTabStripModalUIImpl(
     TabStripModel* model)
     : model_(model) {
diff --git a/chrome/browser/ui/tabs/tab_strip_model.h b/chrome/browser/ui/tabs/tab_strip_model.h
index b2a2be8..54e115b7 100644
--- a/chrome/browser/ui/tabs/tab_strip_model.h
+++ b/chrome/browser/ui/tabs/tab_strip_model.h
@@ -662,14 +662,16 @@
   // Create a new tab group and add the set of tabs pointed to be |indices| to
   // it. Pins all of the tabs if any of them were pinned, and reorders the tabs
   // so they are contiguous and do not split an existing group in half. Returns
-  // the new group. |indices| must be sorted in ascending order.
+  // the new group. This may unsplit split tabs if they are only partially
+  // contained in |indices|. |indices| must be sorted in ascending order.
   tab_groups::TabGroupId AddToNewGroup(const std::vector<int> indices);
 
   // Add the set of tabs pointed to by |indices| to the given tab group |group|.
   // The tabs take on the pinnedness of the tabs already in the group. Tabs
   // before the group will move to the start, while tabs after the group will
   // move to the end. If |add_to_end| is true, all tabs will instead move to
-  // the end. |indices| must be sorted in ascending order.
+  // the end. This may unsplit split tabs if they are only partially contained
+  // in |indices|. |indices| must be sorted in ascending order.
   void AddToExistingGroup(const std::vector<int> indices,
                           const tab_groups::TabGroupId group,
                           const bool add_to_end = false);
@@ -681,8 +683,9 @@
                             const tab_groups::TabGroupId& group);
 
   // Removes the set of tabs pointed to by |indices| from the the groups they
-  // are in, if any. The tabs are moved out of the group if necessary. |indices|
-  // must be sorted in ascending order.
+  // are in, if any. The tabs are moved out of the group if necessary. This
+  // may unsplit split tabs if they are only partially contained in |indices|.
+  // |indices| must be sorted in ascending order.
   void RemoveFromGroup(const std::vector<int>& indices);
 
   // Unsplits all the tabs that are part of the split with `split_id`. The tabs
@@ -1399,8 +1402,17 @@
       const std::optional<tab_groups::TabGroupId> group,
       bool pin);
 
+  // For each split that has tabs in `indices`, remove any of them that contain
+  // tabs not in `indices`.
+  void RemovePartialSplits(const std::vector<int>& indices);
+
   void NotifyForegroundTabsWillEnterBackground();
 
+  // Prior to a split being removed, if the split is currently active, notify
+  // the inactive tab(s) in the split will become hidden as a result of the
+  // split being removed.
+  void NotifyInactiveSplitTabWillBecomeHidden(split_tabs::SplitTabId split_id);
+
   // Assues |left| and |right| have the same root tab collection, and that
   // |left| comes before |right| in traversal order. Returns a vector of tabs
   // ordered by the traversal order starting from |left| and ending at |right|.
diff --git a/chrome/browser/ui/tabs/tab_strip_model_unittest.cc b/chrome/browser/ui/tabs/tab_strip_model_unittest.cc
index e0ce71b..7be68d3 100644
--- a/chrome/browser/ui/tabs/tab_strip_model_unittest.cc
+++ b/chrome/browser/ui/tabs/tab_strip_model_unittest.cc
@@ -2596,6 +2596,93 @@
   EXPECT_TRUE(tabstrip()->empty());
 }
 
+TEST_P(TabStripModelTest, AddToNewGroupRemovesPartialSplits) {
+  ASSERT_NO_FATAL_FAILURE(
+      PrepareTabstripForSelectionTest(tabstrip(), 5, 0, {0}));
+
+  // Create a split with tabs 0 and 1.
+  tabstrip()->ActivateTabAt(
+      0, TabStripUserGestureDetails(
+             TabStripUserGestureDetails::GestureType::kOther));
+  split_tabs::SplitTabId retain_split = tabstrip()->AddToNewSplit(
+      {1}, split_tabs::SplitTabVisualData(),
+      split_tabs::SplitTabCreatedSource::kToolbarButton);
+
+  // Create a split with tabs 2 and 3.
+  tabstrip()->ActivateTabAt(
+      2, TabStripUserGestureDetails(
+             TabStripUserGestureDetails::GestureType::kOther));
+  split_tabs::SplitTabId remove_split = tabstrip()->AddToNewSplit(
+      {3}, split_tabs::SplitTabVisualData(),
+      split_tabs::SplitTabCreatedSource::kToolbarButton);
+
+  // Add the entire first split, and part of the second split to a new group.
+  // Expect only the second split to be removed.
+  tabstrip()->AddToNewGroup({0, 1, 2});
+  EXPECT_TRUE(tabstrip()->ContainsSplit(retain_split));
+  EXPECT_FALSE(tabstrip()->ContainsSplit(remove_split));
+}
+
+TEST_P(TabStripModelTest, AddToExistingGroupRemovesPartialSplits) {
+  ASSERT_NO_FATAL_FAILURE(
+      PrepareTabstripForSelectionTest(tabstrip(), 6, 0, {0}));
+
+  // Create an existing group with tab 5.
+  tab_groups::TabGroupId group = tabstrip()->AddToNewGroup({5});
+
+  // Create a split with tabs 0 and 1.
+  tabstrip()->ActivateTabAt(
+      0, TabStripUserGestureDetails(
+             TabStripUserGestureDetails::GestureType::kOther));
+  split_tabs::SplitTabId retain_split = tabstrip()->AddToNewSplit(
+      {1}, split_tabs::SplitTabVisualData(),
+      split_tabs::SplitTabCreatedSource::kToolbarButton);
+
+  // Create a split with tabs 2 and 3.
+  tabstrip()->ActivateTabAt(
+      2, TabStripUserGestureDetails(
+             TabStripUserGestureDetails::GestureType::kOther));
+  split_tabs::SplitTabId remove_split = tabstrip()->AddToNewSplit(
+      {3}, split_tabs::SplitTabVisualData(),
+      split_tabs::SplitTabCreatedSource::kToolbarButton);
+
+  // Add the entire first split, and part of the second split to the existing
+  // group. Expect only the second split to be removed.
+  tabstrip()->AddToExistingGroup({0, 1, 2}, group);
+  EXPECT_TRUE(tabstrip()->ContainsSplit(retain_split));
+  EXPECT_FALSE(tabstrip()->ContainsSplit(remove_split));
+}
+
+TEST_P(TabStripModelTest, RemoveFromGroupRemovesPartialSplits) {
+  ASSERT_NO_FATAL_FAILURE(
+      PrepareTabstripForSelectionTest(tabstrip(), 5, 0, {0}));
+
+  // Add tabs 0 - 3 to a group.
+  tabstrip()->AddToNewGroup({0, 1, 2, 3});
+
+  // Create a split with tabs 0 and 1.
+  tabstrip()->ActivateTabAt(
+      0, TabStripUserGestureDetails(
+             TabStripUserGestureDetails::GestureType::kOther));
+  split_tabs::SplitTabId retain_split = tabstrip()->AddToNewSplit(
+      {1}, split_tabs::SplitTabVisualData(),
+      split_tabs::SplitTabCreatedSource::kToolbarButton);
+
+  // Create a split with tabs 2 and 3.
+  tabstrip()->ActivateTabAt(
+      2, TabStripUserGestureDetails(
+             TabStripUserGestureDetails::GestureType::kOther));
+  split_tabs::SplitTabId remove_split = tabstrip()->AddToNewSplit(
+      {3}, split_tabs::SplitTabVisualData(),
+      split_tabs::SplitTabCreatedSource::kToolbarButton);
+
+  // Remove the entire first split, and part of the second split. Expect only
+  // the second split to be removed.
+  tabstrip()->RemoveFromGroup({0, 1, 2});
+  EXPECT_TRUE(tabstrip()->ContainsSplit(retain_split));
+  EXPECT_FALSE(tabstrip()->ContainsSplit(remove_split));
+}
+
 TEST_P(TabStripModelTest, SplitLayoutTest) {
   // Create five tabs with two pinned, select the last.
   ASSERT_NO_FATAL_FAILURE(
diff --git a/chrome/browser/ui/views/bookmarks/bookmark_bar_view_unittest.cc b/chrome/browser/ui/views/bookmarks/bookmark_bar_view_unittest.cc
index 58cfa1b..3c08251 100644
--- a/chrome/browser/ui/views/bookmarks/bookmark_bar_view_unittest.cc
+++ b/chrome/browser/ui/views/bookmarks/bookmark_bar_view_unittest.cc
@@ -241,7 +241,7 @@
     BookmarkBarViewBaseTest::SetUp();
 
     widget_ =
-        CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+        CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
     bookmark_bar_view_ =
         widget_->SetContentsView(CreateBookmarkModelAndBookmarkBarView());
   }
diff --git a/chrome/browser/ui/views/bubble/webui_bubble_dialog_view_unittest.cc b/chrome/browser/ui/views/bubble/webui_bubble_dialog_view_unittest.cc
index f63c03e..3b25426f 100644
--- a/chrome/browser/ui/views/bubble/webui_bubble_dialog_view_unittest.cc
+++ b/chrome/browser/ui/views/bubble/webui_bubble_dialog_view_unittest.cc
@@ -60,7 +60,7 @@
     profile_ = std::make_unique<TestingProfile>();
 
     anchor_widget_ =
-        CreateTestWidget(Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
+        CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET,
                          Widget::InitParams::TYPE_WINDOW);
     anchor_widget_->Show();
     contents_wrapper_ = std::make_unique<TestWebUIContentsWrapper>(
diff --git a/chrome/browser/ui/views/bubble/webui_bubble_manager_unittest.cc b/chrome/browser/ui/views/bubble/webui_bubble_manager_unittest.cc
index 6c3061f..e82688e 100644
--- a/chrome/browser/ui/views/bubble/webui_bubble_manager_unittest.cc
+++ b/chrome/browser/ui/views/bubble/webui_bubble_manager_unittest.cc
@@ -84,7 +84,7 @@
 
 TEST_F(WebUIBubbleManagerTest, CreateWebUIBubbleDialogWithAnchorProvided) {
   std::unique_ptr<views::Widget> anchor_widget =
-      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
+      CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET,
                        views::Widget::InitParams::TYPE_WINDOW);
   auto bubble_manager = WebUIBubbleManager::Create<TestWebUIController>(
       anchor_widget->GetContentsView(), browser_window_interface(),
diff --git a/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller_browsertest.cc b/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller_browsertest.cc
index 8e916c8..c64bcc4 100644
--- a/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller_browsertest.cc
+++ b/chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller_browsertest.cc
@@ -176,7 +176,7 @@
   EXPECT_TRUE(toolbar_button(browser())->GetVisible());
   // Create another browser and set it as active so the button becomes dormant.
   Browser* extra_browser = CreateBrowser(browser()->profile());
-  BrowserList::SetLastActive(extra_browser);
+  ui_test_utils::DeprecatedFakeActivateBrowser(extra_browser);
   views::test::WaitForAnimatingLayoutManager(toolbar_container(browser()));
   EXPECT_NE(toolbar_button(extra_browser), nullptr);
   EXPECT_TRUE(toolbar_button(extra_browser)->GetVisible());
@@ -355,7 +355,7 @@
   EXPECT_FALSE(controller(browser())->IsProgressRingInDormantStateForTesting());
   // Create another browser and set it as active so the button becomes dormant.
   Browser* extra_browser = CreateBrowser(browser()->profile());
-  BrowserList::SetLastActive(extra_browser);
+  ui_test_utils::DeprecatedFakeActivateBrowser(extra_browser);
   views::test::WaitForAnimatingLayoutManager(toolbar_container(extra_browser));
 
   EXPECT_TRUE(controller(browser())->IsProgressRingInDormantStateForTesting());
diff --git a/chrome/browser/ui/views/extensions/chooser_dialog_view_unittest.cc b/chrome/browser/ui/views/extensions/chooser_dialog_view_unittest.cc
index bc4dfe4..71cfd01 100644
--- a/chrome/browser/ui/views/extensions/chooser_dialog_view_unittest.cc
+++ b/chrome/browser/ui/views/extensions/chooser_dialog_view_unittest.cc
@@ -41,7 +41,7 @@
     // We need a native view parent for the dialog to avoid a DCHECK
     // on Mac.
     parent_widget_ =
-        CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+        CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
     parent = parent_widget_->GetNativeView();
 #endif
     widget_ = views::DialogDelegate::CreateDialogWidget(dialog_, GetContext(),
diff --git a/chrome/browser/ui/views/extensions/extensions_menu_entry_unittest.cc b/chrome/browser/ui/views/extensions/extensions_menu_entry_unittest.cc
index dc27885..eaa0592 100644
--- a/chrome/browser/ui/views/extensions/extensions_menu_entry_unittest.cc
+++ b/chrome/browser/ui/views/extensions/extensions_menu_entry_unittest.cc
@@ -59,7 +59,7 @@
 
   widget_ = std::make_unique<views::Widget>();
   views::Widget::InitParams init_params(
-      views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
+      views::Widget::InitParams::CLIENT_OWNS_WIDGET,
       views::Widget::InitParams::TYPE_POPUP);
 #if !BUILDFLAG(IS_CHROMEOS) && !BUILDFLAG(IS_MAC)
   // This was copied from BookmarkBarViewTest:
diff --git a/chrome/browser/ui/views/extensions/extensions_menu_item_unittest.cc b/chrome/browser/ui/views/extensions/extensions_menu_item_unittest.cc
index aabe900..c33a8a3b 100644
--- a/chrome/browser/ui/views/extensions/extensions_menu_item_unittest.cc
+++ b/chrome/browser/ui/views/extensions/extensions_menu_item_unittest.cc
@@ -51,7 +51,7 @@
 
   widget_ = std::make_unique<views::Widget>();
   views::Widget::InitParams init_params(
-      views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
+      views::Widget::InitParams::CLIENT_OWNS_WIDGET,
       views::Widget::InitParams::TYPE_POPUP);
 #if !BUILDFLAG(IS_CHROMEOS) && !BUILDFLAG(IS_MAC)
   // This was copied from BookmarkBarViewTest:
diff --git a/chrome/browser/ui/views/extensions/extensions_toolbar_desktop.cc b/chrome/browser/ui/views/extensions/extensions_toolbar_desktop.cc
index fa2690aa..7182f309 100644
--- a/chrome/browser/ui/views/extensions/extensions_toolbar_desktop.cc
+++ b/chrome/browser/ui/views/extensions/extensions_toolbar_desktop.cc
@@ -22,6 +22,7 @@
 #include "chrome/browser/ui/browser_window/public/browser_window_features.h"
 #include "chrome/browser/ui/extensions/extension_action_view_model.h"
 #include "chrome/browser/ui/extensions/extensions_toolbar_view_model.h"
+#include "chrome/browser/ui/extensions/settings_api_bubble_helpers.h"
 #include "chrome/browser/ui/layout_constants.h"
 #include "chrome/browser/ui/side_panel/side_panel_ui.h"
 #include "chrome/browser/ui/toolbar/toolbar_action_hover_card_types.h"
@@ -754,6 +755,10 @@
     if (request_access_button_) {
       CollapseConfirmation();
     }
+    if (current_web_contents) {
+      extensions::MaybeShowExtensionControlledNewTabPage(browser_,
+                                                         current_web_contents);
+    }
   } else {
     // Navigation
     if (!is_same_document) {
diff --git a/chrome/browser/ui/views/extensions/extensions_toolbar_desktop_view_controller.cc b/chrome/browser/ui/views/extensions/extensions_toolbar_desktop_view_controller.cc
index e4cd0dc..8949648 100644
--- a/chrome/browser/ui/views/extensions/extensions_toolbar_desktop_view_controller.cc
+++ b/chrome/browser/ui/views/extensions/extensions_toolbar_desktop_view_controller.cc
@@ -11,9 +11,7 @@
 ExtensionsToolbarDesktopViewController::ExtensionsToolbarDesktopViewController(
     Browser* browser,
     ExtensionsToolbarDesktop* extensions_container)
-    : browser_(browser), extensions_container_(extensions_container) {
-  browser_->tab_strip_model()->AddObserver(this);
-}
+    : browser_(browser), extensions_container_(extensions_container) {}
 
 ExtensionsToolbarDesktopViewController::
     ~ExtensionsToolbarDesktopViewController() {
@@ -39,15 +37,3 @@
           .WithOrder(ExtensionsToolbarDesktopViewController::
                          kFlexOrderExtensionsButton));
 }
-
-void ExtensionsToolbarDesktopViewController::OnTabStripModelChanged(
-    TabStripModel* tab_strip_model,
-    const TabStripModelChange& change,
-    const TabStripSelectionChange& selection) {
-  if (tab_strip_model->empty() || !selection.active_tab_changed()) {
-    return;
-  }
-
-  extensions::MaybeShowExtensionControlledNewTabPage(browser_,
-                                                     selection.new_contents);
-}
diff --git a/chrome/browser/ui/views/extensions/extensions_toolbar_desktop_view_controller.h b/chrome/browser/ui/views/extensions/extensions_toolbar_desktop_view_controller.h
index 44639044..8fcf34a 100644
--- a/chrome/browser/ui/views/extensions/extensions_toolbar_desktop_view_controller.h
+++ b/chrome/browser/ui/views/extensions/extensions_toolbar_desktop_view_controller.h
@@ -12,8 +12,7 @@
 class Browser;
 class ExtensionsToolbarDesktop;
 
-class ExtensionsToolbarDesktopViewController final
-    : public TabStripModelObserver {
+class ExtensionsToolbarDesktopViewController final {
  public:
   // Flex behavior precedence for the container's views.
   static constexpr int kFlexOrderExtensionsButton = 1;
@@ -27,7 +26,7 @@
       const ExtensionsToolbarDesktopViewController&) = delete;
   const ExtensionsToolbarDesktopViewController& operator=(
       const ExtensionsToolbarDesktopViewController&) = delete;
-  ~ExtensionsToolbarDesktopViewController() override;
+  ~ExtensionsToolbarDesktopViewController();
 
   // Updates the flex layout rules for the extension toolbar container to have
   // views::MinimumFlexSizeRule::kPreferred when WindowControlsOverlay (WCO) is
@@ -37,12 +36,6 @@
   void WindowControlsOverlayEnabledChanged(bool enabled);
 
  private:
-  // TabStripModelObserver:
-  void OnTabStripModelChanged(
-      TabStripModel* tab_strip_model,
-      const TabStripModelChange& change,
-      const TabStripSelectionChange& selection) override;
-
   const raw_ptr<Browser> browser_;
 
   raw_ptr<ExtensionsToolbarDesktop> extensions_container_;
diff --git a/chrome/browser/ui/views/extensions/media_galleries_dialog_views_unittest.cc b/chrome/browser/ui/views/extensions/media_galleries_dialog_views_unittest.cc
index aec4db99..0744420 100644
--- a/chrome/browser/ui/views/extensions/media_galleries_dialog_views_unittest.cc
+++ b/chrome/browser/ui/views/extensions/media_galleries_dialog_views_unittest.cc
@@ -120,7 +120,7 @@
       .WillRepeatedly(Return(attached_permissions));
 
   std::unique_ptr<views::Widget> widget =
-      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+      CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
 
   EXPECT_CALL(*controller(), DidToggleEntry(1, false));
   views::test::ButtonTestApi test_api(checkbox());
diff --git a/chrome/browser/ui/views/frame/browser_view.cc b/chrome/browser/ui/views/frame/browser_view.cc
index 5fc1d72..205fe4c 100644
--- a/chrome/browser/ui/views/frame/browser_view.cc
+++ b/chrome/browser/ui/views/frame/browser_view.cc
@@ -4333,8 +4333,7 @@
 }
 
 void BrowserView::CreateTabSearchBubble(
-    const tab_search::mojom::TabSearchSection section,
-    const tab_search::mojom::TabOrganizationFeature organization_feature) {
+    const tab_search::mojom::TabSearchSection section) {
   // Do not spawn the bubble if using the WebUITabStrip.
 #if BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
   if (WebUITabStripContainerView::UseTouchableTabStrip(browser_.get())) {
@@ -4343,7 +4342,7 @@
 #endif  // BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
 
   if (auto* tab_search_host = GetTabSearchBubbleHost()) {
-    tab_search_host->ShowTabSearchBubble(true, section, organization_feature);
+    tab_search_host->ShowTabSearchBubble(true, section);
   }
 }
 
diff --git a/chrome/browser/ui/views/frame/browser_view.h b/chrome/browser/ui/views/frame/browser_view.h
index 1a8ee13..2bf15d2 100644
--- a/chrome/browser/ui/views/frame/browser_view.h
+++ b/chrome/browser/ui/views/frame/browser_view.h
@@ -808,10 +808,9 @@
   // feature is enabled).
   std::vector<views::NativeViewHost*> GetNativeViewHostsForTopControlsSlide();
 
-  using BrowserWindow::CreateTabSearchBubble;
   void CreateTabSearchBubble(
-      tab_search::mojom::TabSearchSection section,
-      tab_search::mojom::TabOrganizationFeature organization_feature) override;
+      tab_search::mojom::TabSearchSection section =
+          tab_search::mojom::TabSearchSection::kSearch) override;
   void CloseTabSearchBubble() override;
 
 #if !BUILDFLAG(IS_CHROMEOS)
diff --git a/chrome/browser/ui/views/location_bar/icon_label_bubble_view_unittest.cc b/chrome/browser/ui/views/location_bar/icon_label_bubble_view_unittest.cc
index 892121c5..f262a05b 100644
--- a/chrome/browser/ui/views/location_bar/icon_label_bubble_view_unittest.cc
+++ b/chrome/browser/ui/views/location_bar/icon_label_bubble_view_unittest.cc
@@ -167,7 +167,7 @@
     gfx::FontList font_list;
 
     widget_ =
-        CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+        CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
     generator_ = std::make_unique<ui::test::EventGenerator>(
         GetRootWindow(widget_.get()));
     view_ = widget_->SetContentsView(
@@ -835,7 +835,7 @@
        GetPreferredSizeDoesntCrashWhenNoCompositor) {
   gfx::FontList font_list;
   std::unique_ptr<views::Widget> widget =
-      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+      CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
   IconLabelBubbleView* icon_label_bubble_view = widget->SetContentsView(
       std::make_unique<TestIconLabelBubbleView>(font_list, this));
   icon_label_bubble_view->SetLabel(u"x");
diff --git a/chrome/browser/ui/views/location_bar/location_icon_view_unittest.cc b/chrome/browser/ui/views/location_bar/location_icon_view_unittest.cc
index 40bf9b1..d78f53aee 100644
--- a/chrome/browser/ui/views/location_bar/location_icon_view_unittest.cc
+++ b/chrome/browser/ui/views/location_bar/location_icon_view_unittest.cc
@@ -71,7 +71,7 @@
     gfx::FontList font_list;
 
     widget_ =
-        CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+        CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
 
     location_bar_model_ = std::make_unique<TestLocationBarModel>();
     delegate_ =
diff --git a/chrome/browser/ui/views/page_action/page_action_controller.cc b/chrome/browser/ui/views/page_action/page_action_controller.cc
index 49df57f4..b711c70c 100644
--- a/chrome/browser/ui/views/page_action/page_action_controller.cc
+++ b/chrome/browser/ui/views/page_action/page_action_controller.cc
@@ -9,6 +9,7 @@
 
 #include "base/functional/bind.h"
 #include "base/functional/callback_forward.h"
+#include "base/timer/timer.h"
 #include "chrome/browser/ui/toolbar/pinned_toolbar/pinned_toolbar_actions_model.h"
 #include "chrome/browser/ui/views/page_action/chip_selector.h"
 #include "chrome/browser/ui/views/page_action/page_action_metrics_recorder.h"
@@ -199,6 +200,13 @@
     actions::ActionId action_id) {
   FindPageActionModel(action_id).SetShouldShowAnchoredMessage(PassKey(),
                                                               /*show=*/true);
+  active_anchored_message_ = action_id;
+  anchored_message_timeout_.Start(
+      FROM_HERE, base::Seconds(12),
+      base::BindRepeating(
+          static_cast<void (PageActionControllerImpl::*)(actions::ActionId)>(
+              &PageActionControllerImpl::ShowSuggestionChip),
+          base::Unretained(this), action_id));
 }
 
 void PageActionControllerImpl::HideAnchoredMessage(
@@ -210,6 +218,12 @@
     actions::ActionId action_id) {
   FindPageActionModel(action_id).SetShouldShowAnchoredMessage(PassKey(),
                                                               /*show=*/false);
+  if (active_anchored_message_ == action_id) {
+    active_anchored_message_ = std::nullopt;
+    if (anchored_message_timeout_.IsRunning()) {
+      anchored_message_timeout_.Stop();
+    }
+  }
 }
 
 ScopedPageActionActivity PageActionControllerImpl::AddActivity(
@@ -239,6 +253,9 @@
 
 void PageActionControllerImpl::OnTabActivated(tabs::TabInterface* tab) {
   SetModelsTabActive(/*is_active=*/true);
+  if (anchored_message_timeout_.IsRunning()) {
+    anchored_message_timeout_.Reset();
+  }
 }
 
 void PageActionControllerImpl::OnTabWillDeactivate(tabs::TabInterface* tab) {
diff --git a/chrome/browser/ui/views/page_action/page_action_controller.h b/chrome/browser/ui/views/page_action/page_action_controller.h
index 801b4cac..4524d26 100644
--- a/chrome/browser/ui/views/page_action/page_action_controller.h
+++ b/chrome/browser/ui/views/page_action/page_action_controller.h
@@ -17,6 +17,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "base/scoped_observation.h"
+#include "base/timer/timer.h"
 #include "base/types/pass_key.h"
 #include "chrome/browser/ui/toolbar/pinned_toolbar/pinned_toolbar_actions_model.h"
 #include "chrome/browser/ui/views/page_action/chip_selector.h"
@@ -390,6 +391,8 @@
   base::OnceCallbackList<void(PageActionController&)>
       on_will_destroy_callback_list_;
   std::unique_ptr<ChipSelector> chip_selector_;
+  base::RetainingOneShotTimer anchored_message_timeout_;
+  std::optional<actions::ActionId> active_anchored_message_;
 
   base::WeakPtrFactory<PageActionControllerImpl> weak_factory_{this};
 };
diff --git a/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_platform_impl_desktop.cc b/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_platform_impl_desktop.cc
index 38b3551c..f65c7d6d 100644
--- a/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_platform_impl_desktop.cc
+++ b/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_platform_impl_desktop.cc
@@ -10,9 +10,9 @@
 #include "chrome/browser/lifetime/application_lifetime.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_finder.h"
-#include "chrome/browser/ui/browser_list.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
+#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
 #include "chrome/browser/ui/views/relaunch_notification/relaunch_recommended_bubble_view.h"
 #include "chrome/browser/ui/views/relaunch_notification/relaunch_required_dialog_view.h"
 #include "ui/views/widget/widget.h"
@@ -41,11 +41,7 @@
 RelaunchNotificationControllerPlatformImpl::
     ~RelaunchNotificationControllerPlatformImpl() {
   DCHECK(!widget_);
-  if (on_visible_) {
-    BrowserList::RemoveObserver(this);
-  }
   CHECK(!WidgetObserver::IsInObserverList());
-  CHECK(!BrowserListObserver::IsInObserverList());
 }
 
 void RelaunchNotificationControllerPlatformImpl::NotifyRelaunchRecommended(
@@ -90,7 +86,8 @@
   // If the instance is not already waiting for one to become active from a
   // previous call, start observing now.
   if (!on_visible_) {
-    BrowserList::AddObserver(this);
+    browser_collection_observation_.Observe(
+        GlobalBrowserCollection::GetInstance());
   }
 
   is_notification_style_ap_required_ = is_notification_style_ap_required;
@@ -107,7 +104,7 @@
     widget_ = nullptr;
   }
   if (on_visible_) {
-    BrowserList::RemoveObserver(this);
+    browser_collection_observation_.Reset();
     on_visible_.Reset();
     last_relaunch_deadline_ = base::Time();
   }
@@ -144,14 +141,14 @@
   widget_ = nullptr;
 }
 
-void RelaunchNotificationControllerPlatformImpl::OnBrowserSetLastActive(
-    Browser* browser) {
+void RelaunchNotificationControllerPlatformImpl::OnBrowserActivated(
+    BrowserWindowInterface* browser) {
   // Ignore non-tabbed browsers.
-  if (!browser->is_type_normal()) {
+  if (browser->GetType() != BrowserWindowInterface::TYPE_NORMAL) {
     return;
   }
 
-  BrowserList::RemoveObserver(this);
+  browser_collection_observation_.Reset();
 
   base::Time new_deadline =
       has_shown_ ? last_relaunch_deadline_ : std::move(on_visible_).Run();
@@ -165,7 +162,7 @@
 }
 
 void RelaunchNotificationControllerPlatformImpl::ShowRequiredNotification(
-    Browser* browser,
+    BrowserWindowInterface* browser,
     base::Time deadline,
     bool is_notification_style_ap_required) {
   widget_ = RelaunchRequiredDialogView::Show(
diff --git a/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_platform_impl_desktop.h b/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_platform_impl_desktop.h
index b67ae63..3f4e5e4 100644
--- a/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_platform_impl_desktop.h
+++ b/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_platform_impl_desktop.h
@@ -7,16 +7,20 @@
 
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
+#include "base/scoped_observation.h"
 #include "base/time/time.h"
-#include "chrome/browser/ui/browser_list_observer.h"
+#include "chrome/browser/ui/browser_window/public/browser_collection_observer.h"
 #include "ui/views/widget/widget_observer.h"
 
+class GlobalBrowserCollection;
+
 namespace views {
 class Widget;
 }
 
-class RelaunchNotificationControllerPlatformImpl : public views::WidgetObserver,
-                                                   public BrowserListObserver {
+class RelaunchNotificationControllerPlatformImpl
+    : public views::WidgetObserver,
+      public BrowserCollectionObserver {
  public:
   RelaunchNotificationControllerPlatformImpl();
 
@@ -56,14 +60,14 @@
   // views::WidgetObserver:
   void OnWidgetDestroying(views::Widget* widget) override;
 
-  // BrowserListObserver:
-  void OnBrowserSetLastActive(Browser* browser) override;
+  // BrowserCollectionObserver:
+  void OnBrowserActivated(BrowserWindowInterface* browser) override;
 
  private:
   // Shows the notification in |browser| for a relaunch that will take place
   // at |deadline|. If |is_notification_style_ap_required| the relaunch required
   // notification is shown with Advanced Protection string and icon.
-  void ShowRequiredNotification(Browser* browser,
+  void ShowRequiredNotification(BrowserWindowInterface* browser,
                                 base::Time deadline,
                                 bool is_notification_style_ap_required);
 
@@ -83,6 +87,9 @@
 
   // The last relaunch deadline if the relaunch notification has_shown_.
   base::Time last_relaunch_deadline_;
+
+  base::ScopedObservation<GlobalBrowserCollection, BrowserCollectionObserver>
+      browser_collection_observation_{this};
 };
 
 #endif  // CHROME_BROWSER_UI_VIEWS_RELAUNCH_NOTIFICATION_RELAUNCH_NOTIFICATION_CONTROLLER_PLATFORM_IMPL_DESKTOP_H_
diff --git a/chrome/browser/ui/views/relaunch_notification/relaunch_required_dialog_view.cc b/chrome/browser/ui/views/relaunch_notification/relaunch_required_dialog_view.cc
index 606f4207..9ed6d21 100644
--- a/chrome/browser/ui/views/relaunch_notification/relaunch_required_dialog_view.cc
+++ b/chrome/browser/ui/views/relaunch_notification/relaunch_required_dialog_view.cc
@@ -11,14 +11,14 @@
 #include "base/metrics/user_metrics_action.h"
 #include "build/branding_buildflags.h"
 #include "chrome/app/vector_icons/vector_icons.h"
-#include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_finder.h"
-#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
 #include "chrome/browser/ui/views/chrome_layout_provider.h"
 #include "chrome/grit/branded_strings.h"
 #include "chrome/grit/generated_resources.h"
 #include "components/constrained_window/constrained_window_views.h"
 #include "components/vector_icons/vector_icons.h"
+#include "ui/base/base_window.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/base/models/image_model.h"
 #include "ui/base/mojom/dialog_button.mojom.h"
@@ -39,13 +39,13 @@
 
 // static
 views::Widget* RelaunchRequiredDialogView::Show(
-    Browser* browser,
+    BrowserWindowInterface* browser,
     base::Time deadline,
     bool ap_style,
     base::RepeatingClosure on_accept) {
   views::Widget* widget = constrained_window::CreateBrowserModalDialogViews(
       new RelaunchRequiredDialogView(deadline, ap_style, std::move(on_accept)),
-      browser->window()->GetNativeWindow());
+      browser->GetWindow()->GetNativeWindow());
   widget->Show();
   return widget;
 }
diff --git a/chrome/browser/ui/views/relaunch_notification/relaunch_required_dialog_view.h b/chrome/browser/ui/views/relaunch_notification/relaunch_required_dialog_view.h
index 5d82e28..e3a6ce10 100644
--- a/chrome/browser/ui/views/relaunch_notification/relaunch_required_dialog_view.h
+++ b/chrome/browser/ui/views/relaunch_notification/relaunch_required_dialog_view.h
@@ -10,7 +10,7 @@
 #include "chrome/browser/ui/views/relaunch_notification/relaunch_required_timer.h"
 #include "ui/views/window/dialog_delegate.h"
 
-class Browser;
+class BrowserWindowInterface;
 namespace views {
 class Widget;
 }  // namespace views
@@ -23,7 +23,7 @@
   // Shows the dialog in |browser| for a relaunch that will be forced at
   // |deadline|. |on_accept| is run if the user accepts the prompt to restart.
   // If |ap_style|, the dialog uses Advanced Protection string and icon.
-  static views::Widget* Show(Browser* browser,
+  static views::Widget* Show(BrowserWindowInterface* browser,
                              base::Time deadline,
                              bool ap_style,
                              base::RepeatingClosure on_accept);
diff --git a/chrome/browser/ui/views/side_panel/side_panel.cc b/chrome/browser/ui/views/side_panel/side_panel.cc
index 00910e46..265c6278 100644
--- a/chrome/browser/ui/views/side_panel/side_panel.cc
+++ b/chrome/browser/ui/views/side_panel/side_panel.cc
@@ -16,6 +16,7 @@
 #include "chrome/browser/ui/color/chrome_color_id.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry.h"
 #include "chrome/browser/ui/side_panel/side_panel_enums.h"
+#include "chrome/browser/ui/side_panel/side_panel_metrics.h"
 #include "chrome/browser/ui/side_panel/side_panel_ui.h"
 #include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/ui/views/chrome_layout_provider.h"
@@ -785,7 +786,7 @@
 
     int side_panel_contents_width = width() - GetBorderInsets().width();
     int browser_window_width = browser_view_->width();
-    SidePanelUtil::RecordSidePanelResizeMetrics(
+    SidePanelMetrics::RecordSidePanelResizeMetrics(
         type_, id.value(), side_panel_contents_width, browser_window_width);
     did_resize_ = false;
   }
diff --git a/chrome/browser/ui/views/side_panel/side_panel_animation_coordinator.cc b/chrome/browser/ui/views/side_panel/side_panel_animation_coordinator.cc
index 6feae31b..68c69c6 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_animation_coordinator.cc
+++ b/chrome/browser/ui/views/side_panel/side_panel_animation_coordinator.cc
@@ -24,7 +24,7 @@
 constexpr base::TimeDelta kSidePanelAnimationDuration = base::Milliseconds(350);
 
 // Returns true if the AnimationCoordinator is in an open state.
-bool IsAnimatingOpen(SidePanelAnimationCoordinator ::AnimationType type) {
+bool IsAnimatingOpen(SidePanelAnimationCoordinator::AnimationType type) {
   return type != SidePanelAnimationCoordinator::AnimationType::kClose;
 }
 
diff --git a/chrome/browser/ui/views/side_panel/side_panel_animation_coordinator.h b/chrome/browser/ui/views/side_panel/side_panel_animation_coordinator.h
index 09d4afe..dc617cbd 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_animation_coordinator.h
+++ b/chrome/browser/ui/views/side_panel/side_panel_animation_coordinator.h
@@ -11,6 +11,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/observer_list_types.h"
 #include "base/time/time.h"
+#include "chrome/browser/ui/side_panel/side_panel_enums.h"
 #include "chrome/browser/ui/views/side_panel/side_panel_animation_ids.h"
 #include "ui/gfx/animation/animation.h"
 #include "ui/gfx/animation/slide_animation.h"
@@ -51,10 +52,7 @@
 class SidePanelAnimationCoordinator : public views::AnimationDelegateViews {
  public:
   using SidePanelAnimationId = ui::ElementIdentifier;
-
-  // LINT.IfChange(AnimationType)
-  enum class AnimationType { kOpen, kOpenWithContentTransition, kClose };
-  // LINT.ThenChange(//tools/metrics/histograms/metadata/browser/enums.xml:SidePanelAnimationType)
+  using AnimationType = SidePanelAnimationType;
 
   // Represents a single animation sequence for a specific animation id.
   struct AnimationSequence {
diff --git a/chrome/browser/ui/views/side_panel/side_panel_animation_perf_reporter.cc b/chrome/browser/ui/views/side_panel/side_panel_animation_perf_reporter.cc
index 765ade5b..03a22b6 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_animation_perf_reporter.cc
+++ b/chrome/browser/ui/views/side_panel/side_panel_animation_perf_reporter.cc
@@ -8,6 +8,7 @@
 
 #include <string_view>
 
+#include "chrome/browser/ui/side_panel/side_panel_metrics.h"
 #include "chrome/browser/ui/views/side_panel/side_panel.h"
 #include "chrome/browser/ui/views/side_panel/side_panel_util.h"
 #include "ui/views/widget/widget.h"
@@ -27,7 +28,7 @@
   int animation_fps = static_cast<int>(std::round(
       animation_presented_times_.size() / animation_duration_.InSecondsF()));
 
-  SidePanelUtil::RecordSidePanelAnimationMetrics(
+  SidePanelMetrics::RecordSidePanelAnimationMetrics(
       side_panel_->type(), animation_type_, largest_animation_step_time_,
       animation_fps);
 }
diff --git a/chrome/browser/ui/views/side_panel/side_panel_coordinator.cc b/chrome/browser/ui/views/side_panel/side_panel_coordinator.cc
index 9f9c08b..cc34ded 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_coordinator.cc
+++ b/chrome/browser/ui/views/side_panel/side_panel_coordinator.cc
@@ -13,12 +13,12 @@
 #include "base/time/time.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
-#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
-#include "chrome/browser/ui/side_panel/side_panel_entry.h"
+#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry_id.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry_key.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry_waiter.h"
 #include "chrome/browser/ui/side_panel/side_panel_enums.h"
+#include "chrome/browser/ui/side_panel/side_panel_metrics.h"
 #include "chrome/browser/ui/side_panel/side_panel_registry.h"
 #include "chrome/browser/ui/side_panel/side_panel_ui.h"
 #include "chrome/browser/ui/tabs/public/tab_features.h"
@@ -148,7 +148,7 @@
 
   if (!IsSidePanelShowing(entry->type())) {
     SetOpenedTimestamp(entry->type(), base::TimeTicks::Now());
-    SidePanelUtil::RecordSidePanelOpen(entry->type(), open_trigger);
+    SidePanelMetrics::RecordSidePanelOpen(entry->type(), open_trigger);
 
     // If opening the toolbar height side panel, make sure the content height
     // side panel is closed and vice versa.
@@ -156,7 +156,7 @@
                               ? SidePanelEntry::PanelType::kToolbar
                               : SidePanelEntry::PanelType::kContent;
         IsSidePanelShowing(other_type)) {
-      SidePanelUtil::RecordPanelClosedForOtherPanelTypeMetrics(
+      SidePanelMetrics::RecordPanelClosedForOtherPanelTypeMetrics(
           other_type, entry->type(), GetCurrentEntryId(other_type).value(),
           entry->key().id());
       Close(other_type, SidePanelEntryHideReason::kSidePanelClosed,
@@ -180,8 +180,8 @@
     }
   }
 
-  SidePanelUtil::RecordSidePanelShowOrChangeEntryTrigger(entry->type(),
-                                                         open_trigger);
+  SidePanelMetrics::RecordSidePanelShowOrChangeEntryTrigger(entry->type(),
+                                                            open_trigger);
 
   // If the side panel is already showing, cancel all loads and do nothing.
   if (IsSidePanelShowing(entry->type()) &&
@@ -200,8 +200,8 @@
     return;
   }
 
-  SidePanelUtil::RecordEntryShowTriggeredMetrics(
-      entry->type(), browser_view_->browser(), entry->key().id(), open_trigger);
+  SidePanelMetrics::RecordEntryShowTriggeredMetrics(
+      entry->type(), entry->key().id(), open_trigger);
 
   waiter(entry->type())
       ->WaitForEntry(entry,
@@ -470,8 +470,8 @@
     content_wrapper->RemoveChildViewT(content_wrapper->children().front());
   }
   side_panel->RemoveHeaderView();
-  SidePanelUtil::RecordSidePanelClosed(side_panel->type(),
-                                       opened_timestamp(side_panel->type()));
+  SidePanelMetrics::RecordSidePanelClosed(side_panel->type(),
+                                          opened_timestamp(side_panel->type()));
 }
 
 void SidePanelCoordinator::ClosePromoAndMaybeNotifyUsed(
diff --git a/chrome/browser/ui/views/side_panel/side_panel_header_controller.cc b/chrome/browser/ui/views/side_panel/side_panel_header_controller.cc
index f9bd4e2a..e33fcb6 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_header_controller.cc
+++ b/chrome/browser/ui/views/side_panel/side_panel_header_controller.cc
@@ -21,6 +21,7 @@
 #include "chrome/browser/ui/color/chrome_color_id.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry_key.h"
+#include "chrome/browser/ui/side_panel/side_panel_metrics.h"
 #include "chrome/browser/ui/side_panel/side_panel_ui.h"
 #include "chrome/browser/ui/user_education/browser_user_education_interface.h"
 #include "chrome/browser/ui/views/chrome_layout_provider.h"
@@ -327,7 +328,7 @@
 
   base::WeakPtr<SidePanelHeaderController> weak_this =
       weak_pointer_factor_.GetWeakPtr();
-  SidePanelUtil::RecordNewTabButtonClicked(side_panel_entry_->key().id());
+  SidePanelMetrics::RecordNewTabButtonClicked(side_panel_entry_->key().id());
   content::OpenURLParams params(new_tab_url, content::Referrer(),
                                 WindowOpenDisposition::NEW_FOREGROUND_TAB,
                                 ui::PAGE_TRANSITION_AUTO_BOOKMARK,
diff --git a/chrome/browser/ui/views/side_panel/side_panel_toolbar_pinning_controller.cc b/chrome/browser/ui/views/side_panel/side_panel_toolbar_pinning_controller.cc
index 42cfaeedf3..543bfcde3 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_toolbar_pinning_controller.cc
+++ b/chrome/browser/ui/views/side_panel/side_panel_toolbar_pinning_controller.cc
@@ -9,6 +9,7 @@
 #include "chrome/browser/ui/browser_actions.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry_id.h"
+#include "chrome/browser/ui/side_panel/side_panel_metrics.h"
 #include "chrome/browser/ui/side_panel/side_panel_registry.h"
 #include "chrome/browser/ui/views/extensions/extensions_toolbar_desktop.h"
 #include "chrome/browser/ui/views/frame/browser_view.h"
@@ -100,7 +101,8 @@
     actions_model->UpdatePinnedState(action_id.value(), updated_pin_state);
   }
 
-  SidePanelUtil::RecordPinnedButtonClicked(entry_key.id(), updated_pin_state);
+  SidePanelMetrics::RecordPinnedButtonClicked(entry_key.id(),
+                                              updated_pin_state);
 }
 
 void SidePanelToolbarPinningController::UpdateActiveState(
diff --git a/chrome/browser/ui/views/side_panel/side_panel_util.cc b/chrome/browser/ui/views/side_panel/side_panel_util.cc
index f3bb1cba..22bc537d 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_util.cc
+++ b/chrome/browser/ui/views/side_panel/side_panel_util.cc
@@ -6,12 +6,7 @@
 
 #include <string_view>
 
-#include "base/metrics/histogram_functions.h"
-#include "base/metrics/user_metrics.h"
-#include "base/metrics/user_metrics_action.h"
 #include "base/notreached.h"
-#include "base/strings/strcat.h"
-#include "base/time/time.h"
 #include "chrome/browser/history_clusters/history_clusters_service_factory.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
@@ -37,32 +32,6 @@
 
 namespace {
 
-std::string_view GetSidePanelNameFor(SidePanelEntry::PanelType panel_type) {
-  switch (panel_type) {
-    case SidePanelEntry::PanelType::kContent:
-      return "SidePanel";
-    case SidePanelEntry::PanelType::kToolbar:
-      return "SidePanelToolbarHeight";
-  }
-
-  NOTREACHED() << "Invalid PanelType " << static_cast<int>(panel_type);
-}
-
-std::string_view GetAnimationNameFor(
-    SidePanelAnimationCoordinator::AnimationType animation_type) {
-  switch (animation_type) {
-    case SidePanelAnimationCoordinator::AnimationType::kOpen:
-      return "Open";
-    case SidePanelAnimationCoordinator::AnimationType::
-        kOpenWithContentTransition:
-      return "OpenWithContentTransition";
-    case SidePanelAnimationCoordinator::AnimationType::kClose:
-      return "Close";
-  }
-
-  NOTREACHED() << "Invalid AnimationType " << static_cast<int>(animation_type);
-}
-
 }  // namespace
 
 // static
@@ -138,166 +107,3 @@
   return actions::ActionManager::Get().FindAction(
       action_id.value(), browser_actions->root_action_item());
 }
-
-void SidePanelUtil::RecordSidePanelOpen(
-    SidePanelEntry::PanelType type,
-    std::optional<SidePanelOpenTrigger> trigger) {
-  base::RecordAction(base::UserMetricsAction(
-      base::StrCat({GetSidePanelNameFor(type), ".Show"}).c_str()));
-
-  if (trigger.has_value()) {
-    base::UmaHistogramEnumeration(
-        base::StrCat({GetSidePanelNameFor(type), ".OpenTrigger"}),
-        trigger.value());
-  }
-}
-
-void SidePanelUtil::RecordSidePanelShowOrChangeEntryTrigger(
-    SidePanelEntry::PanelType type,
-    std::optional<SidePanelOpenTrigger> trigger) {
-  if (trigger.has_value()) {
-    base::UmaHistogramEnumeration(
-        base::StrCat({GetSidePanelNameFor(type), ".OpenOrChangeEntryTrigger"}),
-        trigger.value());
-  }
-}
-
-void SidePanelUtil::RecordSidePanelClosed(SidePanelEntry::PanelType type,
-                                          base::TimeTicks opened_timestamp) {
-  base::RecordAction(base::UserMetricsAction(
-      base::StrCat({GetSidePanelNameFor(type), ".Hide"}).c_str()));
-
-  base::UmaHistogramLongTimes(
-      base::StrCat({GetSidePanelNameFor(type), ".OpenDuration"}),
-      base::TimeTicks::Now() - opened_timestamp);
-}
-
-void SidePanelUtil::RecordSidePanelResizeMetrics(SidePanelEntry::PanelType type,
-                                                 SidePanelEntry::Id id,
-                                                 int side_panel_contents_width,
-                                                 int browser_window_width) {
-  std::string_view entry_name = SidePanelEntryIdToHistogramName(id);
-
-  // Metrics per-id and overall for side panel width after resize.
-  base::UmaHistogramCounts10000(base::StrCat({GetSidePanelNameFor(type), ".",
-                                              entry_name, ".ResizedWidth"}),
-                                side_panel_contents_width);
-  base::UmaHistogramCounts10000(
-      base::StrCat({GetSidePanelNameFor(type), ".ResizedWidth"}),
-      side_panel_contents_width);
-
-  // Metrics per-id and overall for side panel width after resize as a
-  // percentage of browser width.
-  int width_percentage = side_panel_contents_width * 100 / browser_window_width;
-  base::UmaHistogramPercentage(
-      base::StrCat({GetSidePanelNameFor(type), ".", entry_name,
-                    ".ResizedWidthPercentage"}),
-      width_percentage);
-  base::UmaHistogramPercentage(
-      base::StrCat({GetSidePanelNameFor(type), ".ResizedWidthPercentage"}),
-      width_percentage);
-}
-
-void SidePanelUtil::RecordNewTabButtonClicked(SidePanelEntry::Id id) {
-  base::RecordComputedAction(
-      base::StrCat({"SidePanel.", SidePanelEntryIdToHistogramName(id),
-                    ".NewTabButtonClicked"}));
-}
-
-void SidePanelUtil::RecordEntryShownMetrics(
-    SidePanelEntry::PanelType type,
-    SidePanelEntry::Id id,
-    base::TimeTicks load_started_timestamp) {
-  base::RecordComputedAction(
-      base::StrCat({GetSidePanelNameFor(type), ".",
-                    SidePanelEntryIdToHistogramName(id), ".Shown"}));
-  if (load_started_timestamp != base::TimeTicks()) {
-    base::UmaHistogramLongTimes(
-        base::StrCat({GetSidePanelNameFor(type), ".",
-                      SidePanelEntryIdToHistogramName(id),
-                      ".TimeFromEntryTriggerToShown"}),
-        base::TimeTicks::Now() - load_started_timestamp);
-  }
-}
-
-void SidePanelUtil::RecordEntryHiddenMetrics(SidePanelEntry::PanelType type,
-                                             SidePanelEntry::Id id,
-                                             base::TimeTicks shown_timestamp) {
-  base::UmaHistogramLongTimes(
-      base::StrCat({GetSidePanelNameFor(type), ".",
-                    SidePanelEntryIdToHistogramName(id), ".ShownDuration"}),
-      base::TimeTicks::Now() - shown_timestamp);
-  // To measure extended usage times, Read Anything also needs a higher maximum
-  // than what's supported by the standard ShownDuration histogram.
-  if (type == SidePanelEntry::PanelType::kContent &&
-      id == SidePanelEntryId::kReadAnything) {
-    // TODO(crbug.com/456824119): Consider removing the standard ShownDuration
-    // histogram for Read Anything after this one has gathered enough data.
-    base::UmaHistogramCustomTimes("SidePanel.ReadAnything.ShownDurationMax1Day",
-                                  base::TimeTicks::Now() - shown_timestamp,
-                                  /*min=*/base::Seconds(1),
-                                  /*max=*/base::Hours(24),
-                                  /*buckets=*/100);
-  }
-}
-
-void SidePanelUtil::RecordEntryShowTriggeredMetrics(
-    SidePanelEntry::PanelType type,
-    Browser* browser,
-    SidePanelEntry::Id id,
-    std::optional<SidePanelOpenTrigger> trigger) {
-  if (trigger.has_value()) {
-    base::UmaHistogramEnumeration(
-        base::StrCat({GetSidePanelNameFor(type), ".",
-                      SidePanelEntryIdToHistogramName(id), ".ShowTriggered"}),
-        trigger.value());
-  }
-}
-
-void SidePanelUtil::RecordPinnedButtonClicked(SidePanelEntry::Id id,
-                                              bool is_pinned) {
-  base::RecordComputedAction(base::StrCat(
-      {"SidePanel.", SidePanelEntryIdToHistogramName(id), ".",
-       is_pinned ? "Pinned" : "Unpinned", ".BySidePanelHeaderButton"}));
-}
-
-void SidePanelUtil::RecordSidePanelAnimationMetrics(
-    SidePanelEntry::PanelType panel_type,
-    SidePanelAnimationCoordinator::AnimationType animation_type,
-    base::TimeDelta largest_step_time,
-    int frames_per_second) {
-  if (!largest_step_time.is_zero()) {
-    base::UmaHistogramTimes(base::StrCat({GetSidePanelNameFor(panel_type),
-                                          ".TimeOfLongestAnimationStep"}),
-                            largest_step_time);
-  }
-
-  if (frames_per_second > 0) {
-    base::UmaHistogramCounts100(
-        base::StrCat({GetSidePanelNameFor(panel_type), ".",
-                      GetAnimationNameFor(animation_type), ".AnimationFPS"}),
-        frames_per_second);
-  }
-}
-
-void SidePanelUtil::RecordPanelClosedForOtherPanelTypeMetrics(
-    SidePanelEntry::PanelType closing_panel_type,
-    SidePanelEntry::PanelType opening_panel_type,
-    SidePanelEntryId closing_panel_id,
-    SidePanelEntryId opening_panel_id) {
-  base::RecordComputedAction(
-      base::StrCat({GetSidePanelNameFor(closing_panel_type), ".ClosedToOpen.",
-                    GetSidePanelNameFor(opening_panel_type)}));
-  if (closing_panel_id == SidePanelEntryId::kContextualTasks) {
-    base::RecordComputedAction(base::StrCat(
-        {GetSidePanelNameFor(closing_panel_type), ".",
-         SidePanelEntryIdToHistogramName(closing_panel_id), ".ClosedToOpen.",
-         GetSidePanelNameFor(opening_panel_type)}));
-    if (opening_panel_id == SidePanelEntryId::kGlic) {
-      base::RecordComputedAction(base::StrCat(
-          {GetSidePanelNameFor(closing_panel_type), ".",
-           SidePanelEntryIdToHistogramName(closing_panel_id), ".EntryClosedToOpen.",
-           SidePanelEntryIdToHistogramName(opening_panel_id)}));
-    }
-  }
-}
diff --git a/chrome/browser/ui/views/side_panel/side_panel_util.h b/chrome/browser/ui/views/side_panel/side_panel_util.h
index 97e2c45..a658707 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_util.h
+++ b/chrome/browser/ui/views/side_panel/side_panel_util.h
@@ -5,14 +5,11 @@
 #ifndef CHROME_BROWSER_UI_VIEWS_SIDE_PANEL_SIDE_PANEL_UTIL_H_
 #define CHROME_BROWSER_UI_VIEWS_SIDE_PANEL_SIDE_PANEL_UTIL_H_
 
-#include <optional>
 #include <type_traits>
 
-#include "base/time/time.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry.h"
 #include "chrome/browser/ui/side_panel/side_panel_entry_id.h"
 #include "chrome/browser/ui/side_panel/side_panel_enums.h"
-#include "chrome/browser/ui/views/side_panel/side_panel_animation_coordinator.h"
 #include "ui/base/class_property.h"
 
 class Browser;
@@ -41,41 +38,6 @@
 
   static actions::ActionItem* GetActionItem(Browser* browser,
                                             SidePanelEntryKey entry_key);
-
-  static void RecordNewTabButtonClicked(SidePanelEntry::Id id);
-  static void RecordSidePanelOpen(SidePanelEntry::PanelType type,
-                                  std::optional<SidePanelOpenTrigger> trigger);
-  static void RecordSidePanelShowOrChangeEntryTrigger(
-      SidePanelEntry::PanelType type,
-      std::optional<SidePanelOpenTrigger> trigger);
-  static void RecordSidePanelClosed(SidePanelEntry::PanelType type,
-                                    base::TimeTicks opened_timestamp);
-  static void RecordSidePanelResizeMetrics(SidePanelEntry::PanelType type,
-                                           SidePanelEntry::Id id,
-                                           int side_panel_contents_width,
-                                           int browser_window_width);
-  static void RecordEntryShownMetrics(SidePanelEntry::PanelType type,
-                                      SidePanelEntry::Id id,
-                                      base::TimeTicks load_started_timestamp);
-  static void RecordEntryHiddenMetrics(SidePanelEntry::PanelType type,
-                                       SidePanelEntry::Id id,
-                                       base::TimeTicks shown_timestamp);
-  static void RecordEntryShowTriggeredMetrics(
-      SidePanelEntry::PanelType type,
-      Browser* browser,
-      SidePanelEntry::Id id,
-      std::optional<SidePanelOpenTrigger> trigger);
-  static void RecordPinnedButtonClicked(SidePanelEntry::Id id, bool is_pinned);
-  static void RecordSidePanelAnimationMetrics(
-      SidePanelEntry::PanelType panel_type,
-      SidePanelAnimationCoordinator::AnimationType animation_type,
-      base::TimeDelta largest_step_time,
-      int frames_per_second);
-  static void RecordPanelClosedForOtherPanelTypeMetrics(
-      SidePanelEntry::PanelType closing_panel_type,
-      SidePanelEntry::PanelType opening_panel_type,
-      SidePanelEntryId closing_panel_id,
-      SidePanelEntryId opening_panel_id);
 };
 
 #endif  // CHROME_BROWSER_UI_VIEWS_SIDE_PANEL_SIDE_PANEL_UTIL_H_
diff --git a/chrome/browser/ui/views/tab_search_bubble_host.cc b/chrome/browser/ui/views/tab_search_bubble_host.cc
index c2dbaaa2..8dff541f 100644
--- a/chrome/browser/ui/views/tab_search_bubble_host.cc
+++ b/chrome/browser/ui/views/tab_search_bubble_host.cc
@@ -169,9 +169,7 @@
 
 void TabSearchBubbleHost::OnUserInvokedFeature(const Browser* browser) {
   if (browser == GetBrowser()) {
-    ShowTabSearchBubble(
-        false, tab_search::mojom::TabSearchSection::kOrganize,
-        tab_search::mojom::TabOrganizationFeature::kAutoTabGroups);
+    ShowTabSearchBubble(false, tab_search::mojom::TabSearchSection::kOrganize);
   }
 }
 
@@ -208,8 +206,7 @@
 
 bool TabSearchBubbleHost::ShowTabSearchBubble(
     bool triggered_by_keyboard_shortcut,
-    tab_search::mojom::TabSearchSection section,
-    tab_search::mojom::TabOrganizationFeature organization_feature) {
+    tab_search::mojom::TabSearchSection section) {
   TRACE_EVENT0("ui", "TabSearchBubbleHost::ShowTabSearchBubble");
   base::trace_event::EmitNamedTrigger("show-tab-search-bubble");
   if (section != tab_search::mojom::TabSearchSection::kNone) {
@@ -218,14 +215,6 @@
         tab_search_prefs::GetIntFromTabSearchSection(section));
   }
 
-  if (organization_feature !=
-      tab_search::mojom::TabOrganizationFeature::kNone) {
-    profile_->GetPrefs()->SetInteger(
-        tab_search_prefs::kTabOrganizationFeature,
-        tab_search_prefs::GetIntFromTabOrganizationFeature(
-            organization_feature));
-  }
-
   if (webui_bubble_manager_->GetBubbleWidget()) {
     return false;
   }
diff --git a/chrome/browser/ui/views/tab_search_bubble_host.h b/chrome/browser/ui/views/tab_search_bubble_host.h
index c16d8ba..3ec9a6f3 100644
--- a/chrome/browser/ui/views/tab_search_bubble_host.h
+++ b/chrome/browser/ui/views/tab_search_bubble_host.h
@@ -58,12 +58,9 @@
   // This returns true if the method call results in the creation of a new Tab
   // Search bubble. Optionally use section to force the bubble to open to the
   // given tab, even if the bubble is already showing.
-  bool ShowTabSearchBubble(
-      bool triggered_by_keyboard_shortcut = false,
-      tab_search::mojom::TabSearchSection section =
-          tab_search::mojom::TabSearchSection::kSearch,
-      tab_search::mojom::TabOrganizationFeature organization_feature =
-          tab_search::mojom::TabOrganizationFeature::kNone);
+  bool ShowTabSearchBubble(bool triggered_by_keyboard_shortcut = false,
+                           tab_search::mojom::TabSearchSection section =
+                               tab_search::mojom::TabSearchSection::kSearch);
   void CloseTabSearchBubble();
 
   Browser* GetBrowser();
diff --git a/chrome/browser/ui/views/tab_sharing/tab_sharing_status_message_view.cc b/chrome/browser/ui/views/tab_sharing/tab_sharing_status_message_view.cc
index 78a96e98..8d97810 100644
--- a/chrome/browser/ui/views/tab_sharing/tab_sharing_status_message_view.cc
+++ b/chrome/browser/ui/views/tab_sharing/tab_sharing_status_message_view.cc
@@ -19,6 +19,7 @@
 #include "ui/views/layout/flex_layout.h"
 #include "ui/views/metadata/view_factory.h"
 #include "ui/views/view_class_properties.h"
+#include "ui/views/view_utils.h"
 
 namespace {
 using EndpointInfo = ::TabSharingStatusMessageView::EndpointInfo;
diff --git a/chrome/browser/ui/views/tabs/dragging/tab_drag_controller_interactive_uitest.cc b/chrome/browser/ui/views/tabs/dragging/tab_drag_controller_interactive_uitest.cc
index d65acd47..49bd96357 100644
--- a/chrome/browser/ui/views/tabs/dragging/tab_drag_controller_interactive_uitest.cc
+++ b/chrome/browser/ui/views/tabs/dragging/tab_drag_controller_interactive_uitest.cc
@@ -26,6 +26,7 @@
 #include "base/memory/ptr_util.h"
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
+#include "base/scoped_observation.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/bind.h"
@@ -38,10 +39,11 @@
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_commands.h"
 #include "chrome/browser/ui/browser_finder.h"
-#include "chrome/browser/ui/browser_list.h"
 #include "chrome/browser/ui/browser_tabstrip.h"
+#include "chrome/browser/ui/browser_window/public/browser_collection_observer.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
+#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
 #include "chrome/browser/ui/tabs/features.h"
 #include "chrome/browser/ui/tabs/split_tab_metrics.h"
 #include "chrome/browser/ui/tabs/tab_group_model.h"
@@ -355,7 +357,7 @@
 //   by releasing the mouse or cancelling the DnD session;
 // - else, that the move loop ends, i.e. attaching to an existing browser or
 //   fully ending the tab drag.
-class BrowserChangeWaiter : public BrowserListObserver {
+class BrowserChangeWaiter : public BrowserCollectionObserver {
  public:
   enum class ChangeType {
     kAdded,
@@ -363,11 +365,12 @@
   };
 
   explicit BrowserChangeWaiter(ChangeType type) : type_(type) {
-    BrowserList::AddObserver(this);
+    browser_collection_observation_.Observe(
+        GlobalBrowserCollection::GetInstance());
   }
   BrowserChangeWaiter(const BrowserChangeWaiter&) = delete;
   BrowserChangeWaiter& operator=(const BrowserChangeWaiter&) = delete;
-  ~BrowserChangeWaiter() override { BrowserList::RemoveObserver(this); }
+  ~BrowserChangeWaiter() override = default;
 
   // The closure must ensure the system DnD session/move loop ends (see comment
   // above).
@@ -376,14 +379,14 @@
     run_loop_.Run();
   }
 
-  // BrowserListObserver:
-  void OnBrowserAdded(Browser* browser) override {
+  // BrowserCollectionObserver:
+  void OnBrowserCreated(BrowserWindowInterface* browser) override {
     if (type_ == ChangeType::kAdded) {
       Quit();
     }
   }
 
-  void OnBrowserRemoved(Browser* browser) override {
+  void OnBrowserClosed(BrowserWindowInterface* browser) override {
     if (type_ == ChangeType::kRemoved) {
       Quit();
     }
@@ -401,16 +404,21 @@
     quit_called_ = true;
     if (closure_) {
       // For ChangeType::kRemoved, the browser is still closing and
-      // synchronously running the closure now can lead to reentrancy issues, so
-      // we instead PostTask() it. We also need to make sure we only quit the
-      // RunLoop after the closure has run.
+      // synchronously running the closure now can lead to reentrancy issues,
+      // so we instead PostTask() it. We also need to make sure we only quit
+      // the RunLoop after the closure has run.
+      // Use a double-PostTask to leverage SequencedTaskRunner FIFO ordering:
+      // the first PostTask runs before SynchronouslyDestroyBrowser (enqueued
+      // later in OnWindowClosing), and the second runs after it, ensuring the
+      // closure executes after ~Browser() completes. See crbug.com/431671320.
       // It won't hurt to use the same approach for ChangeType::kAdded.
       base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
           FROM_HERE,
           base::BindOnce(
               [](base::OnceClosure closure, base::OnceClosure quit_closure) {
-                std::move(closure).Run();
-                std::move(quit_closure).Run();
+                base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+                    FROM_HERE,
+                    std::move(closure).Then(std::move(quit_closure)));
               },
               std::move(closure_), run_loop_.QuitClosure()));
     } else {
@@ -422,6 +430,8 @@
   bool quit_called_ = false;
   base::OnceClosure closure_;
   base::RunLoop run_loop_{base::RunLoop::Type::kNestableTasksAllowed};
+  base::ScopedObservation<GlobalBrowserCollection, BrowserCollectionObserver>
+      browser_collection_observation_{this};
 };
 
 void SetID(WebContents* web_contents, int id) {
diff --git a/chrome/browser/ui/views/tabs/projects/BUILD.gn b/chrome/browser/ui/views/tabs/projects/BUILD.gn
index 6379a78..7353e6f 100644
--- a/chrome/browser/ui/views/tabs/projects/BUILD.gn
+++ b/chrome/browser/ui/views/tabs/projects/BUILD.gn
@@ -93,6 +93,7 @@
     "//components/saved_tab_groups/public",
     "//components/saved_tab_groups/test_support",
     "//components/sync/base",
+    "//ui/events:test_support",
     "//ui/views",
     "//ui/views:test_support",
   ]
diff --git a/chrome/browser/ui/views/tabs/projects/layout_constants.h b/chrome/browser/ui/views/tabs/projects/layout_constants.h
index b23966a..e3eb8ed2 100644
--- a/chrome/browser/ui/views/tabs/projects/layout_constants.h
+++ b/chrome/browser/ui/views/tabs/projects/layout_constants.h
@@ -40,6 +40,12 @@
 // The padding around a list.
 inline constexpr gfx::Insets kListPadding = gfx::Insets::VH(0, 4);
 
+// The size of the Tab groups icon.
+inline constexpr int kTabGroupIconSize = 12;
+
+// The margins for the Tab groups icon.
+inline constexpr auto kTabGroupIconMargins = gfx::Insets(6);
+
 // Minimum width of the projects panel.
 inline constexpr int kProjectsPanelMinWidth = 240;
 
diff --git a/chrome/browser/ui/views/tabs/projects/projects_panel_controller.cc b/chrome/browser/ui/views/tabs/projects/projects_panel_controller.cc
index 53c66ac..0f74365 100644
--- a/chrome/browser/ui/views/tabs/projects/projects_panel_controller.cc
+++ b/chrome/browser/ui/views/tabs/projects/projects_panel_controller.cc
@@ -43,8 +43,32 @@
 
 void ProjectsPanelController::MoveTabGroup(const base::Uuid& group_guid,
                                            int new_index) {
-  tab_group_sync_service_->UpdateGroupPosition(group_guid, std::nullopt,
-                                               new_index);
+  if (new_index < 0 || new_index >= static_cast<int>(tab_groups_.size())) {
+    return;
+  }
+
+  auto it = std::ranges::find(tab_groups_, group_guid,
+                              &tab_groups::SavedTabGroup::saved_guid);
+  if (it == tab_groups_.end()) {
+    return;
+  }
+
+  int old_index = std::distance(tab_groups_.begin(), it);
+  if (old_index == new_index) {
+    return;
+  }
+
+  if (new_index < old_index) {
+    // Moving up (to a lower index). Place before the group currently at
+    // new_index.
+    tab_group_sync_service_->ReorderGroupBefore(
+        group_guid, tab_groups_[new_index].saved_guid());
+  } else {
+    // Moving down (to a higher index). Place after the group currently at
+    // new_index.
+    tab_group_sync_service_->ReorderGroupAfter(
+        group_guid, tab_groups_[new_index].saved_guid());
+  }
 }
 
 const std::vector<contextual_tasks::Thread>&
diff --git a/chrome/browser/ui/views/tabs/projects/projects_panel_controller_unittest.cc b/chrome/browser/ui/views/tabs/projects/projects_panel_controller_unittest.cc
index f26a06d..28aefbc 100644
--- a/chrome/browser/ui/views/tabs/projects/projects_panel_controller_unittest.cc
+++ b/chrome/browser/ui/views/tabs/projects/projects_panel_controller_unittest.cc
@@ -207,16 +207,40 @@
   controller->OpenTabGroup(uuid);
 }
 
-TEST_F(ProjectsPanelControllerTest, MoveTabGroupCallsService) {
-  auto controller = GetInitializedController();
-  const base::Uuid uuid = base::Uuid::GenerateRandomV4();
+TEST_F(ProjectsPanelControllerTest, MoveTabGroupUpCallsReorderGroupBefore) {
+  std::vector<tab_groups::SavedTabGroup> groups = {
+      GetGroup(), GetGroup1DayOlder(), GetNewGroup()};
+  EXPECT_CALL(mock_tab_group_sync_service_, GetAllGroups())
+      .WillOnce(testing::Return(groups));
 
+  auto controller = GetInitializedController();
+
+  // Move "New Group" (index 2) to index 0.
+  // Should call ReorderGroupBefore("New Group", "Group 1").
   EXPECT_CALL(mock_tab_group_sync_service_,
-              UpdateGroupPosition(testing::Eq(uuid), testing::Eq(std::nullopt),
-                                  testing::Eq(2)))
+              ReorderGroupBefore(testing::Eq(GetNewGroup().saved_guid()),
+                                 testing::Eq(GetGroup().saved_guid())))
       .Times(1);
 
-  controller->MoveTabGroup(uuid, 2);
+  controller->MoveTabGroup(GetNewGroup().saved_guid(), 0);
+}
+
+TEST_F(ProjectsPanelControllerTest, MoveTabGroupDownCallsReorderGroupAfter) {
+  std::vector<tab_groups::SavedTabGroup> groups = {
+      GetGroup(), GetGroup1DayOlder(), GetNewGroup()};
+  EXPECT_CALL(mock_tab_group_sync_service_, GetAllGroups())
+      .WillOnce(testing::Return(groups));
+
+  auto controller = GetInitializedController();
+
+  // Move "Group 1" (index 0) to index 2.
+  // Should call ReorderGroupAfter("Group 1", "New Group").
+  EXPECT_CALL(mock_tab_group_sync_service_,
+              ReorderGroupAfter(testing::Eq(GetGroup().saved_guid()),
+                                testing::Eq(GetNewGroup().saved_guid())))
+      .Times(1);
+
+  controller->MoveTabGroup(GetGroup().saved_guid(), 2);
 }
 
 TEST_F(ProjectsPanelControllerTest, OpenTabGroupAutofocus) {
diff --git a/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view.cc b/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view.cc
index b67006eb..5ee1c785 100644
--- a/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view.cc
+++ b/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view.cc
@@ -4,6 +4,8 @@
 
 #include "chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view.h"
 
+#include "build/build_config.h"
+#include "build/buildflag.h"
 #include "chrome/app/vector_icons/vector_icons.h"
 #include "chrome/browser/ui/browser_element_identifiers.h"
 #include "chrome/browser/ui/tabs/saved_tab_groups/tab_group_menu_utils.h"
@@ -51,12 +53,6 @@
                       4,
                       4);
 
-// The size of the Tab groups icon.
-constexpr int kTabGroupIconSize = 12;
-
-// The margins for the Tab groups icon.
-constexpr auto kTabGroupsIconMargins = gfx::Insets(6);
-
 // Height and width of shared Tab group icon and more button icon.
 constexpr int kTrailingIconSize = 16;
 
@@ -83,7 +79,8 @@
   projects_panel::ConfigureInkDropForButton(this);
 
   tab_group_icon_ = AddChildView(std::make_unique<views::ImageView>());
-  tab_group_icon_->SetProperty(views::kMarginsKey, kTabGroupsIconMargins);
+  tab_group_icon_->SetProperty(views::kMarginsKey,
+                               projects_panel::kTabGroupIconMargins);
 
   auto group_title = tab_groups::TabGroupMenuUtils::GetMenuTextForGroup(group);
   title_ = AddChildView(std::make_unique<views::Label>(group_title));
@@ -217,7 +214,7 @@
   ui::ColorId color_id = GetTabGroupContextMenuColorId(tab_group_color_id_);
   tab_group_icon_->SetImage(ui::ImageModel::FromVectorIcon(
       *tab_group_vector_icon_, GetColorProvider()->GetColor(color_id),
-      kTabGroupIconSize));
+      projects_panel::kTabGroupIconSize));
 }
 
 void ProjectsPanelTabGroupsItemView::OnMouseEntered(
@@ -227,7 +224,13 @@
 
 void ProjectsPanelTabGroupsItemView::OnMouseExited(
     const ui::MouseEvent& event) {
+#if BUILDFLAG(IS_LINUX)
+  // Bypasses the synchronous IsMouseHovered() check which can be stale on Linux
+  // Wayland/X11 due to asynchronous cursor updates during mouse exit events.
+  UpdateHoverStateForced(/*is_hovered=*/false);
+#else
   UpdateHoverState();
+#endif
 }
 
 void ProjectsPanelTabGroupsItemView::OnMouseMoved(const ui::MouseEvent& event) {
@@ -269,6 +272,11 @@
   disable_animations_for_testing_ = true;
 }
 
+// static
+void ProjectsPanelTabGroupsItemView::enable_animations_for_testing() {
+  disable_animations_for_testing_ = false;
+}
+
 void ProjectsPanelTabGroupsItemView::OnMoreButtonPressed() {
   more_button_callback_.Run(group_guid_, *more_button_);
   UpdateHoverState();
@@ -279,10 +287,13 @@
 }
 
 void ProjectsPanelTabGroupsItemView::UpdateHoverState() {
+  UpdateHoverStateForced(IsMouseHovered());
+}
+
+void ProjectsPanelTabGroupsItemView::UpdateHoverStateForced(bool is_hovered) {
   const bool show_more =
       !dragging_ &&
-      (IsMouseHovered() || (more_button_ && more_button_->GetState() ==
-                                                views::Button::STATE_PRESSED));
+      (is_hovered || more_button_->GetState() == views::Button::STATE_PRESSED);
 
   if (!disable_animations_for_testing_) {
     if (show_more) {
diff --git a/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view.h b/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view.h
index 65c0aed5..c6350344 100644
--- a/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view.h
+++ b/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view.h
@@ -71,12 +71,16 @@
   }
 
   static void disable_animations_for_testing();
+  static void enable_animations_for_testing();
 
  private:
   void OnMoreButtonPressed();
   void OnMoreButtonStateChanged();
 
   void UpdateHoverState();
+  // Bypasses the synchronous IsMouseHovered() check which can be stale on Linux
+  // Wayland/X11 due to asynchronous cursor updates during mouse exit events.
+  void UpdateHoverStateForced(bool is_hovered);
 
   const base::Uuid group_guid_;
   MoreButtonPressedCallback more_button_callback_;
diff --git a/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view_unittest.cc b/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view_unittest.cc
index a1eb376..fa5d06d3 100644
--- a/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view_unittest.cc
+++ b/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view_unittest.cc
@@ -5,13 +5,30 @@
 #include "chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_item_view.h"
 
 #include "chrome/app/vector_icons/vector_icons.h"
+#include "chrome/test/views/chrome_views_test_base.h"
 #include "components/saved_tab_groups/public/saved_tab_group_tab.h"
 #include "components/sync/base/collaboration_id.h"
+#include "ui/events/test/event_generator.h"
+#include "ui/views/controls/button/menu_button.h"
 #include "ui/views/controls/image_view.h"
 #include "ui/views/controls/label.h"
-#include "ui/views/test/views_test_base.h"
+#include "ui/views/widget/widget.h"
 
-class ProjectsPanelTabGroupsItemViewTest : public views::ViewsTestBase {};
+namespace {
+
+class ScopedAnimationDisabler {
+ public:
+  ScopedAnimationDisabler() {
+    ProjectsPanelTabGroupsItemView::disable_animations_for_testing();
+  }
+  ~ScopedAnimationDisabler() {
+    ProjectsPanelTabGroupsItemView::enable_animations_for_testing();
+  }
+};
+
+}  // namespace
+
+class ProjectsPanelTabGroupsItemViewTest : public ChromeViewsTestBase {};
 
 TEST_F(ProjectsPanelTabGroupsItemViewTest, TestDisplay) {
   tab_groups::SavedTabGroup group(std::u16string(u"my_group"),
@@ -71,3 +88,46 @@
   EXPECT_EQ(&kPeopleGroupIcon,
             collaboration_view->GetImageModel().GetVectorIcon().vector_icon());
 }
+
+TEST_F(ProjectsPanelTabGroupsItemViewTest, HoverStateChanges) {
+  ScopedAnimationDisabler animation_disabler;
+
+  tab_groups::SavedTabGroup group(std::u16string(u"my_group"),
+                                  tab_groups::TabGroupColorId::kGrey, {},
+                                  std::nullopt);
+
+  std::unique_ptr<views::Widget> widget =
+      CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
+  auto* item_view =
+      widget->SetContentsView(std::make_unique<ProjectsPanelTabGroupsItemView>(
+          group, base::DoNothing(), base::DoNothing()));
+  widget->Show();
+
+  ui::test::EventGenerator generator(GetContext(), widget->GetNativeWindow());
+
+  auto move_mouse_to = [&](bool inside_view) {
+    if (inside_view) {
+      generator.MoveMouseTo(item_view->GetBoundsInScreen().CenterPoint());
+    } else {
+      generator.MoveMouseTo(item_view->GetBoundsInScreen().bottom_right() +
+                            gfx::Vector2d(10, 10));
+    }
+  };
+
+  auto check_more_button_visible = [&](bool expected_visibility) {
+    EXPECT_EQ(expected_visibility,
+              item_view->more_button_for_testing()->GetVisible());
+  };
+
+  // Move mouse outside the view.
+  move_mouse_to(false);
+  check_more_button_visible(false);
+
+  // Move mouse over the view.
+  move_mouse_to(true);
+  check_more_button_visible(true);
+
+  // Move mouse outside the view again.
+  move_mouse_to(false);
+  check_more_button_visible(false);
+}
diff --git a/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_view.cc b/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_view.cc
index 00e8e2d..208c3f3 100644
--- a/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_view.cc
+++ b/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_view.cc
@@ -28,14 +28,17 @@
 #include "ui/compositor/paint_recorder.h"
 #include "ui/gfx/canvas.h"
 #include "ui/gfx/text_constants.h"
+#include "ui/views/accessibility/view_accessibility.h"
 #include "ui/views/animation/ink_drop.h"
 #include "ui/views/background.h"
 #include "ui/views/button_drag_utils.h"
-#include "ui/views/controls/button/label_button.h"
+#include "ui/views/controls/button/button.h"
 #include "ui/views/controls/focus_ring.h"
 #include "ui/views/controls/highlight_path_generator.h"
+#include "ui/views/controls/image_view.h"
 #include "ui/views/controls/label.h"
 #include "ui/views/layout/box_layout.h"
+#include "ui/views/layout/flex_layout.h"
 #include "ui/views/style/typography.h"
 #include "ui/views/view_class_properties.h"
 #include "ui/views/view_utils.h"
@@ -43,19 +46,39 @@
 namespace {
 constexpr gfx::Insets kNoTabsInteriorMargins = gfx::Insets::VH(0, 8);
 
-class ProjectsPanelNewTabGroupButton : public views::LabelButton {
-  METADATA_HEADER(ProjectsPanelNewTabGroupButton, views::LabelButton)
+class ProjectsPanelNewTabGroupButton : public views::Button {
+  METADATA_HEADER(ProjectsPanelNewTabGroupButton, views::Button)
 
  public:
   explicit ProjectsPanelNewTabGroupButton(base::RepeatingClosure callback)
-      : views::LabelButton(
-            std::move(callback),
-            l10n_util::GetStringUTF16(IDS_CREATE_NEW_TAB_GROUP)) {
-    SetImageModel(views::Button::STATE_NORMAL,
-                  ui::ImageModel::FromVectorIcon(
-                      kCreateNewTabGroupIcon, kColorProjectsPanelButtonIcon));
-    SetHorizontalAlignment(gfx::ALIGN_LEFT);
+      : views::Button(std::move(callback)) {
+    SetLayoutManager(std::make_unique<views::FlexLayout>())
+        ->SetInteriorMargin(projects_panel::kListItemMargins)
+        .SetOrientation(views::LayoutOrientation::kHorizontal)
+        .SetCrossAxisAlignment(views::LayoutAlignment::kCenter);
+
+    auto* icon = AddChildView(std::make_unique<views::ImageView>());
+    icon->SetProperty(views::kMarginsKey, projects_panel::kTabGroupIconMargins);
+    icon->SetImage(ui::ImageModel::FromVectorIcon(
+        kCreateNewTabGroupIcon, kColorProjectsPanelButtonIcon,
+        projects_panel::kTabGroupIconSize));
+
+    auto* title = AddChildView(std::make_unique<views::Label>(
+        l10n_util::GetStringUTF16(IDS_CREATE_NEW_TAB_GROUP)));
+    title->SetTextStyle(views::style::STYLE_BODY_3);
+    title->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD);
+    title->SetBackgroundColor(SK_ColorTRANSPARENT);
+    title->SetProperty(views::kMarginsKey,
+                       projects_panel::kListItemTitleMargins);
+    title->SetProperty(
+        views::kFlexBehaviorKey,
+        views::FlexSpecification(views::LayoutOrientation::kHorizontal,
+                                 views::MinimumFlexSizeRule::kScaleToMinimum,
+                                 views::MaximumFlexSizeRule::kUnbounded));
+
     projects_panel::ConfigureInkDropForButton(this);
+    GetViewAccessibility().SetName(
+        l10n_util::GetStringUTF16(IDS_CREATE_NEW_TAB_GROUP));
   }
   ProjectsPanelNewTabGroupButton(const ProjectsPanelNewTabGroupButton&) =
       delete;
diff --git a/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_view.h b/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_view.h
index 332b53f..999a743 100644
--- a/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_view.h
+++ b/chrome/browser/ui/views/tabs/projects/projects_panel_tab_groups_view.h
@@ -25,8 +25,8 @@
 
 namespace views {
 class ActionViewController;
+class Button;
 class Label;
-class LabelButton;
 }  // namespace views
 
 class ProjectsPanelNoTabGroupsView;
@@ -82,7 +82,7 @@
 
   std::optional<gfx::Rect> GetDropIndicatorBoundsForTesting() const;
 
-  const std::vector<ProjectsPanelTabGroupsItemView*> item_views_for_testing() {
+  std::vector<ProjectsPanelTabGroupsItemView*> item_views_for_testing() const {
     return item_views_;
   }
 
@@ -90,7 +90,7 @@
     return no_tab_groups_view_;
   }
 
-  views::LabelButton* create_new_tab_group_button_for_testing() {
+  views::Button* create_new_tab_group_button_for_testing() {
     return create_new_tab_group_button_;
   }
 
@@ -135,7 +135,7 @@
       more_button_callback_;
   TabGroupMovedCallback tab_group_moved_callback_;
   raw_ptr<views::Label> title_ = nullptr;
-  raw_ptr<views::LabelButton> create_new_tab_group_button_ = nullptr;
+  raw_ptr<views::Button> create_new_tab_group_button_ = nullptr;
   raw_ptr<ProjectsPanelNoTabGroupsView> no_tab_groups_view_ = nullptr;
   std::vector<ProjectsPanelTabGroupsItemView*> item_views_;
 
diff --git a/chrome/browser/ui/views/tabs/shared/tab_strip_combo_button.cc b/chrome/browser/ui/views/tabs/shared/tab_strip_combo_button.cc
index 772d65b..610f62b 100644
--- a/chrome/browser/ui/views/tabs/shared/tab_strip_combo_button.cc
+++ b/chrome/browser/ui/views/tabs/shared/tab_strip_combo_button.cc
@@ -179,6 +179,7 @@
 TabStripComboButton::CreateFlatEdgeButtonFor(actions::ActionId action_id,
                                              ui::ElementIdentifier element_id) {
   auto button = std::make_unique<TabStripFlatEdgeButton>();
+  button->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_CENTER);
   button->set_context_menu_controller(this);
   if (!browser_ || !browser_->GetActions()) {
     return button;
@@ -436,7 +437,7 @@
       height = std::max(height, child_size.height());
     } else {
       if (has_visible_child) {
-        width += spacing;
+        height += spacing;
       }
       height += child_size.height();
       width = std::max(width, child_size.width());
diff --git a/chrome/browser/ui/views/tabs/tab_group_views.cc b/chrome/browser/ui/views/tabs/tab_group_views.cc
index b8d2890..39d323a 100644
--- a/chrome/browser/ui/views/tabs/tab_group_views.cc
+++ b/chrome/browser/ui/views/tabs/tab_group_views.cc
@@ -137,10 +137,6 @@
       tab_slot_controller_->GetGroupColorId(group_));
 }
 
-bool TabGroupViews::InTearDown() const {
-  return !header_ || !header_->GetWidget() || !drag_underline_->GetWidget();
-}
-
 std::tuple<const views::View*, const views::View*>
 TabGroupViews::GetLeadingTrailingGroupViews() const {
   std::vector<raw_ptr<views::View, VectorExperimental>> children =
@@ -152,6 +148,10 @@
   return GetLeadingTrailingGroupViews(children);
 }
 
+bool TabGroupViews::InTearDown() const {
+  return !header_ || !header_->GetWidget() || !drag_underline_->GetWidget();
+}
+
 std::tuple<views::View*, views::View*>
 TabGroupViews::GetLeadingTrailingDraggedGroupViews() const {
   return GetLeadingTrailingGroupViews(drag_underline_->parent()->children());
diff --git a/chrome/browser/ui/views/tabs/tab_search_container.cc b/chrome/browser/ui/views/tabs/tab_search_container.cc
index b8ef5f2..ab1ebb1 100644
--- a/chrome/browser/ui/views/tabs/tab_search_container.cc
+++ b/chrome/browser/ui/views/tabs/tab_search_container.cc
@@ -487,9 +487,7 @@
 
 void TabSearchContainer::OnTabDeclutterButtonClicked() {
   BrowserView::GetBrowserViewForBrowser(browser_window_interface_)
-      ->CreateTabSearchBubble(
-          tab_search::mojom::TabSearchSection::kOrganize,
-          tab_search::mojom::TabOrganizationFeature::kDeclutter);
+      ->CreateTabSearchBubble(tab_search::mojom::TabSearchSection::kOrganize);
 
   // Force hide the button when pressed, bypassing locked expansion mode.
   ExecuteHideTabOrganization(tab_declutter_button_);
diff --git a/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.cc b/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.cc
index b6de444b..9c1dcdca 100644
--- a/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.cc
+++ b/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.cc
@@ -6,6 +6,8 @@
 
 #include <numeric>
 
+#include "build/build_config.h"
+#include "build/buildflag.h"
 #include "chrome/app/vector_icons/vector_icons.h"
 #include "chrome/browser/ui/browser_element_identifiers.h"
 #include "chrome/browser/ui/tabs/tab_group_theme.h"
@@ -248,7 +250,13 @@
 }
 
 void VerticalTabGroupHeaderView::OnMouseExited(const ui::MouseEvent& event) {
+#if BUILDFLAG(IS_LINUX)
+  // Bypasses the synchronous IsMouseHovered() check which can be stale on Linux
+  // Wayland/X11 due to asynchronous cursor updates during mouse exit events.
+  SetEditorBubbleButtonVisibilityOnHover(/*is_hovered=*/false);
+#else
   UpdateEditorBubbleButtonVisibility();
+#endif
 }
 
 void VerticalTabGroupHeaderView::ShowContextMenuForViewImpl(
@@ -402,8 +410,15 @@
 }
 
 void VerticalTabGroupHeaderView::UpdateEditorBubbleButtonVisibility() {
-  editor_bubble_button_->SetVisible(editor_bubble_tracker_.is_open() ||
-                                    IsMouseHovered());
+  SetEditorBubbleButtonVisibilityOnHover(IsMouseHovered());
+}
+
+void VerticalTabGroupHeaderView::SetEditorBubbleButtonVisibilityOnHover(
+    bool is_hovered) {
+  if (editor_bubble_button_) {
+    editor_bubble_button_->SetVisible(editor_bubble_tracker_.is_open() ||
+                                      is_hovered);
+  }
 }
 
 void VerticalTabGroupHeaderView::ShowEditorBubble() {
diff --git a/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.h b/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.h
index 1cd932031..abe74cc5 100644
--- a/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.h
+++ b/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.h
@@ -87,6 +87,9 @@
 
  private:
   void UpdateEditorBubbleButtonVisibility();
+  // Bypasses the synchronous IsMouseHovered() check which can be stale on Linux
+  // Wayland/X11 due to asynchronous cursor updates during mouse exit events.
+  void SetEditorBubbleButtonVisibilityOnHover(bool is_hovered);
   void ShowEditorBubble();
   void UpdateAccessibleName(
       const tab_groups::TabGroupVisualData* tab_group_visual_data,
diff --git a/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view_unittest.cc b/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view_unittest.cc
index c8c40e16..8690978 100644
--- a/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view_unittest.cc
+++ b/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view_unittest.cc
@@ -14,6 +14,7 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/events/test/event_generator.h"
+#include "ui/views/controls/button/label_button.h"
 #include "ui/views/test/views_test_base.h"
 #include "ui/views/widget/widget.h"
 
@@ -88,3 +89,44 @@
   ui::test::EventGenerator generator(GetContext(), widget->GetNativeWindow());
   generator.MoveMouseTo(header->GetBoundsInScreen().CenterPoint());
 }
+
+TEST_F(VerticalTabGroupHeaderViewTest, EditorBubbleButtonVisibilityOnHover) {
+  MockDelegate delegate;
+  tab_groups::TabGroupVisualData visual_data(
+      u"Group Title", tab_groups::TabGroupColorId::kBlue, false);
+
+  std::unique_ptr<views::Widget> widget =
+      CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
+  auto* header =
+      widget->SetContentsView(std::make_unique<VerticalTabGroupHeaderView>(
+          delegate, nullptr, &visual_data));
+  widget->Show();
+
+  ui::test::EventGenerator generator(GetContext(), widget->GetNativeWindow());
+
+  auto move_mouse_to = [&](bool inside_view) {
+    if (inside_view) {
+      generator.MoveMouseTo(header->GetBoundsInScreen().CenterPoint());
+    } else {
+      generator.MoveMouseTo(header->GetBoundsInScreen().bottom_right() +
+                            gfx::Vector2d(10, 10));
+    }
+  };
+
+  auto check_editor_bubble_button_visible = [&](bool expected_visibility) {
+    EXPECT_EQ(expected_visibility,
+              header->editor_bubble_button()->GetVisible());
+  };
+
+  // Move mouse outside the header.
+  move_mouse_to(false);
+  check_editor_bubble_button_visible(false);
+
+  // Move mouse over the header.
+  move_mouse_to(true);
+  check_editor_bubble_button_visible(true);
+
+  // Move mouse outside the header again.
+  move_mouse_to(false);
+  check_editor_bubble_button_visible(false);
+}
diff --git a/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_top_container.cc b/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_top_container.cc
index 0fc9b5d..1a107c5 100644
--- a/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_top_container.cc
+++ b/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_top_container.cc
@@ -11,6 +11,7 @@
 #include "chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h"
 #include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_everything_menu.h"
+#include "chrome/browser/ui/views/frame/vertical_tab_strip_region_view.h"
 #include "chrome/browser/ui/views/tabs/shared/tab_strip_combo_button.h"
 #include "chrome/browser/ui/views/tabs/shared/tab_strip_flat_edge_button.h"
 #include "chrome/browser/ui/views/tabs/vertical/top_container_button.h"
@@ -77,9 +78,10 @@
           : parent()->GetAvailableSize(this).width().value_or(0);
 
   if (combo_button_
-          ->GetPreferredSizeForOrientation(
-              views::LayoutOrientation::kHorizontal)
-          .width() >= available_width) {
+              ->GetPreferredSizeForOrientation(
+                  views::LayoutOrientation::kHorizontal)
+              .width() >= available_width ||
+      available_width <= VerticalTabStripRegionView::kCollapsedWidth) {
     combo_button_orientation_ = views::LayoutOrientation::kVertical;
     int current_y = 0;
 
diff --git a/chrome/browser/ui/views/user_education/browser_user_education_service.cc b/chrome/browser/ui/views/user_education/browser_user_education_service.cc
index f9b3b8a0..64a1436 100644
--- a/chrome/browser/ui/views/user_education/browser_user_education_service.cc
+++ b/chrome/browser/ui/views/user_education/browser_user_education_service.cc
@@ -1627,7 +1627,7 @@
     registry.RegisterFeature(
         std::move(FeaturePromoSpecification::CreateForCustomUi(
                       feature_engagement::kIPHiOSLensPromoDesktopFeature,
-                      kToolbarAppMenuButtonElementId,
+                      kIOSLensPromoAnchorElementId,
                       user_education::CreateCustomHelpBubbleViewFactoryCallback(
                           base::BindRepeating(
                               &IOSPromoBubbleView::Create,
diff --git a/chrome/browser/ui/views/user_education/ios_promo_bubble_view.cc b/chrome/browser/ui/views/user_education/ios_promo_bubble_view.cc
index 8fd7c43..bcc22b3 100644
--- a/chrome/browser/ui/views/user_education/ios_promo_bubble_view.cc
+++ b/chrome/browser/ui/views/user_education/ios_promo_bubble_view.cc
@@ -227,11 +227,17 @@
 gfx::Rect IOSPromoBubbleView::GetBubbleBounds() {
   gfx::Rect bubble_bounds = BubbleDialogDelegateView::GetBubbleBounds();
   gfx::Rect anchor_rect = GetAnchorRect();
-  // Manually position the bubble to be right-aligned with the anchor and below
-  // it. This mimics TOP_RIGHT anchor behavior but without the arrow inset
-  // logic.
-  bubble_bounds.set_x(anchor_rect.right() - bubble_bounds.width());
-  bubble_bounds.set_y(anchor_rect.bottom());
+
+  // Manually position the bubble relative to the anchor. This mimics TOP_RIGHT
+  // anchor behavior but without the arrow inset logic. For the Lens promo,
+  // the bubble is attached at the top-left corner of the anchor.
+  if (promo_type_ == PromoType::kLens) {
+    bubble_bounds.set_x(anchor_rect.x());
+    bubble_bounds.set_y(anchor_rect.y());
+  } else {
+    bubble_bounds.set_x(anchor_rect.right() - bubble_bounds.width());
+    bubble_bounds.set_y(anchor_rect.bottom());
+  }
   return bubble_bounds;
 }
 
diff --git a/chrome/browser/ui/webui/ash/dlp_internals/dlp_internals_page_handler.cc b/chrome/browser/ui/webui/ash/dlp_internals/dlp_internals_page_handler.cc
index aafbe33..77930399 100644
--- a/chrome/browser/ui/webui/ash/dlp_internals/dlp_internals_page_handler.cc
+++ b/chrome/browser/ui/webui/ash/dlp_internals/dlp_internals_page_handler.cc
@@ -201,54 +201,65 @@
 
 void DlpInternalsPageHandler::GetClipboardDataSource(
     GetClipboardDataSourceCallback callback) {
-  auto source = ui::Clipboard::GetForCurrentThread()->GetSource(
-      ui::ClipboardBuffer::kCopyPaste);
-  if (!source) {
-    std::move(callback).Run(std::move(nullptr));
-    return;
-  }
+  ui::Clipboard::GetForCurrentThread()->GetSource(
+      ui::ClipboardBuffer::kCopyPaste,
+      base::BindOnce(
+          [](GetClipboardDataSourceCallback callback,
+             std::optional<ui::DataTransferEndpoint> source) {
+            if (!source) {
+              std::move(callback).Run(std::move(nullptr));
+              return;
+            }
 
-  auto mojom_source = dlp_internals::mojom::DataTransferEndpoint::New();
-  switch (source->type()) {
-    case ui::EndpointType::kDefault:
-      mojom_source->type = dlp_internals::mojom::EndpointType::kDefault;
-      break;
+            auto mojom_source =
+                dlp_internals::mojom::DataTransferEndpoint::New();
+            switch (source->type()) {
+              case ui::EndpointType::kDefault:
+                mojom_source->type =
+                    dlp_internals::mojom::EndpointType::kDefault;
+                break;
 
-    case ui::EndpointType::kUrl:
-      mojom_source->type = dlp_internals::mojom::EndpointType::kUrl;
-      break;
+              case ui::EndpointType::kUrl:
+                mojom_source->type = dlp_internals::mojom::EndpointType::kUrl;
+                break;
 
-    case ui::EndpointType::kClipboardHistory:
-      mojom_source->type =
-          dlp_internals::mojom::EndpointType::kClipboardHistory;
-      break;
+              case ui::EndpointType::kClipboardHistory:
+                mojom_source->type =
+                    dlp_internals::mojom::EndpointType::kClipboardHistory;
+                break;
 
-    case ui::EndpointType::kUnknownVm:
-      mojom_source->type = dlp_internals::mojom::EndpointType::kUnknownVm;
-      break;
+              case ui::EndpointType::kUnknownVm:
+                mojom_source->type =
+                    dlp_internals::mojom::EndpointType::kUnknownVm;
+                break;
 
-    case ui::EndpointType::kArc:
-      mojom_source->type = dlp_internals::mojom::EndpointType::kArc;
-      break;
+              case ui::EndpointType::kArc:
+                mojom_source->type = dlp_internals::mojom::EndpointType::kArc;
+                break;
 
-    case ui::EndpointType::kBorealis:
-      mojom_source->type = dlp_internals::mojom::EndpointType::kBorealis;
-      break;
+              case ui::EndpointType::kBorealis:
+                mojom_source->type =
+                    dlp_internals::mojom::EndpointType::kBorealis;
+                break;
 
-    case ui::EndpointType::kCrostini:
-      mojom_source->type = dlp_internals::mojom::EndpointType::kCrostini;
-      break;
+              case ui::EndpointType::kCrostini:
+                mojom_source->type =
+                    dlp_internals::mojom::EndpointType::kCrostini;
+                break;
 
-    case ui::EndpointType::kPluginVm:
-      mojom_source->type = dlp_internals::mojom::EndpointType::kPluginVm;
-      break;
-  }
+              case ui::EndpointType::kPluginVm:
+                mojom_source->type =
+                    dlp_internals::mojom::EndpointType::kPluginVm;
+                break;
+            }
 
-  if (source->IsUrlType()) {
-    mojom_source->url = *source->GetURL();
-  }
+            if (source->IsUrlType()) {
+              mojom_source->url = *source->GetURL();
+            }
 
-  std::move(callback).Run(std::move(mojom_source));
+            std::move(callback).Run(std::move(mojom_source));
+          },
+          std::move(callback)));
 }
 
 void DlpInternalsPageHandler::GetContentRestrictionsInfo(
diff --git a/chrome/browser/ui/webui/ash/settings/pages/about/BUILD.gn b/chrome/browser/ui/webui/ash/settings/pages/about/BUILD.gn
index 836f528e..77c0981 100644
--- a/chrome/browser/ui/webui/ash/settings/pages/about/BUILD.gn
+++ b/chrome/browser/ui/webui/ash/settings/pages/about/BUILD.gn
@@ -29,6 +29,7 @@
     "//chrome/browser/ash/arc",
     "//chrome/browser/ash/arc:arc_util",
     "//chrome/browser/ash/policy/core",
+    "//chrome/browser/obsolete_system",
     "//chrome/browser/signin",
     "//chrome/browser/ui/webui/ash/settings/search",
     "//chrome/common",
diff --git a/chrome/browser/ui/webui/ash/settings/pages/device/input_device_settings/input_device_settings_provider_unittest.cc b/chrome/browser/ui/webui/ash/settings/pages/device/input_device_settings/input_device_settings_provider_unittest.cc
index 21a0daba..50d0df4 100644
--- a/chrome/browser/ui/webui/ash/settings/pages/device/input_device_settings/input_device_settings_provider_unittest.cc
+++ b/chrome/browser/ui/webui/ash/settings/pages/device/input_device_settings/input_device_settings_provider_unittest.cc
@@ -590,7 +590,7 @@
     feature_list_->InitWithFeatures({features::kPeripheralCustomization}, {});
     views::ViewsTestBase::SetUp();
     widget_ =
-        CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+        CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
     widget_->Show();
     scoped_resetter_ = std::make_unique<
         InputDeviceSettingsController::ScopedResetterForTest>();
diff --git a/chrome/browser/ui/webui/cr_components/searchbox/contextual_searchbox_handler.cc b/chrome/browser/ui/webui/cr_components/searchbox/contextual_searchbox_handler.cc
index f7bc89f1c9..fc289f1 100644
--- a/chrome/browser/ui/webui/cr_components/searchbox/contextual_searchbox_handler.cc
+++ b/chrome/browser/ui/webui/cr_components/searchbox/contextual_searchbox_handler.cc
@@ -336,21 +336,22 @@
 contextual_search::ContextualSearchSessionHandle*
 ContextualSearchboxHandler::GetContextualSessionHandle() {
   if (!get_session_callback_) {
+    context_controller_observation_.Reset();
     return nullptr;
   }
 
   auto* session_handle = get_session_callback_.Run();
   auto* context_controller =
       session_handle ? session_handle->GetController() : nullptr;
-  // Remove the old context controller if it's different from the new one.
-  if (context_controller_ && context_controller_.get() != context_controller) {
-    context_controller_->RemoveObserver(this);
-    context_controller_ = nullptr;
-  }
-  // Reset to the new context controller if it is different.
-  if (context_controller && !context_controller_) {
-    context_controller->AddObserver(this);
-    context_controller_ = context_controller->AsWeakPtr();
+
+  if (context_controller) {
+    if (!context_controller_observation_.IsObservingSource(
+            context_controller)) {
+      context_controller_observation_.Reset();
+      context_controller_observation_.Observe(context_controller);
+    }
+  } else {
+    context_controller_observation_.Reset();
   }
   return session_handle;
 }
@@ -361,9 +362,6 @@
   if (browser_window_interface) {
     browser_window_interface->GetTabStripModel()->RemoveObserver(this);
   }
-  if (context_controller_) {
-    context_controller_->RemoveObserver(this);
-  }
 }
 
 void ContextualSearchboxHandler::ResetInputStateModel() {
diff --git a/chrome/browser/ui/webui/cr_components/searchbox/contextual_searchbox_handler.h b/chrome/browser/ui/webui/cr_components/searchbox/contextual_searchbox_handler.h
index a5bcdc6..3f1c4c8 100644
--- a/chrome/browser/ui/webui/cr_components/searchbox/contextual_searchbox_handler.h
+++ b/chrome/browser/ui/webui/cr_components/searchbox/contextual_searchbox_handler.h
@@ -184,6 +184,13 @@
     OnInputStateChanged(state);
   }
 
+  base::ScopedObservation<contextual_search::ContextualSearchContextController,
+                          contextual_search::ContextualSearchContextController::
+                              FileUploadStatusObserver>&
+  context_controller_observation_for_testing() {
+    return context_controller_observation_;
+  }
+
  protected:
   // SearchboxHandler:
   omnibox::InputState GetInputState() const override;
@@ -274,8 +281,10 @@
 
   // The context controller this searchbox is listening to for file upload
   // status updates.
-  base::WeakPtr<contextual_search::ContextualSearchContextController>
-      context_controller_;
+  base::ScopedObservation<contextual_search::ContextualSearchContextController,
+                          contextual_search::ContextualSearchContextController::
+                              FileUploadStatusObserver>
+      context_controller_observation_{this};
 
   std::optional<lens::ContextualInputData> context_input_data_;
 
diff --git a/chrome/browser/ui/webui/cr_components/searchbox/searchbox_handler.cc b/chrome/browser/ui/webui/cr_components/searchbox/searchbox_handler.cc
index fb59c25f..840e12a9 100644
--- a/chrome/browser/ui/webui/cr_components/searchbox/searchbox_handler.cc
+++ b/chrome/browser/ui/webui/cr_components/searchbox/searchbox_handler.cc
@@ -397,6 +397,14 @@
       {"canvas", IDS_NTP_COMPOSE_CANVAS},
       {"geminiModelAuto", IDS_NTP_COMPOSE_AUTO_MODEL},
       {"geminiModelThinking", IDS_NTP_COMPOSE_THINKING_3_PRO},
+      {"composeboxHintTextAskAboutThese",
+       IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THESE},
+      {"composeboxHintTextAskAboutThisImage",
+       IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_IMAGE},
+      {"composeboxHintTextAskAboutThisTab",
+       IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_TAB},
+      {"composeboxHintTextAskAboutThisDoc",
+       IDS_COMPOSE_HINT_TEXT_ASK_ABOUT_THIS_DOC},
   };
   source->AddLocalizedStrings(kStrings);
   source->AddString("searchboxComposePlaceholder",
diff --git a/chrome/browser/ui/webui/searchbox/contextual_searchbox_handler_unittest.cc b/chrome/browser/ui/webui/searchbox/contextual_searchbox_handler_unittest.cc
index 3bab9a6f..fe5c1c80 100644
--- a/chrome/browser/ui/webui/searchbox/contextual_searchbox_handler_unittest.cc
+++ b/chrome/browser/ui/webui/searchbox/contextual_searchbox_handler_unittest.cc
@@ -110,6 +110,11 @@
                      bool shift_key) override {}
   void OnThumbnailRemoved() override {}
 
+  contextual_search::ContextualSearchSessionHandle*
+  GetContextualSessionHandle() {
+    return ContextualSearchboxHandler::GetContextualSessionHandle();
+  }
+
   contextual_search::ContextualSearchMetricsRecorder* GetMetricsRecorder() {
     return ContextualSearchboxHandler::GetMetricsRecorder();
   }
@@ -216,6 +221,21 @@
     ContextualSearchboxHandlerTestHarness::TearDown();
   }
 
+  void SetSessionHandle(
+      std::unique_ptr<contextual_search::ContextualSearchSessionHandle>
+          session_handle) {
+    contextual_session_handle_ = std::move(session_handle);
+  }
+
+  std::unique_ptr<contextual_search::ContextualSearchSessionHandle>
+  TakeSessionHandle() {
+    return std::move(contextual_session_handle_);
+  }
+
+  void ClearQueryController() { query_controller_ = nullptr; }
+
+  contextual_search::ContextualSearchService* service() { return service_; }
+
  protected:
   testing::NiceMock<MockSearchboxPage> mock_searchbox_page_;
   std::unique_ptr<FakeContextualSearchboxHandler> handler_;
@@ -696,6 +716,55 @@
       "ContextualSearch.Models.NewTabPage",
       composebox_query::mojom::ModelMode::kGeminiRegular, 1);
 }
+
+// Regression test for crbug.com/487773783.
+TEST_F(ContextualSearchboxHandlerTest, ManageObservationOfContextController) {
+  EXPECT_TRUE(
+      handler().context_controller_observation_for_testing().IsObserving());
+  EXPECT_TRUE(
+      handler().context_controller_observation_for_testing().IsObservingSource(
+          &query_controller()));
+
+  auto old_session_handle = TakeSessionHandle();
+  auto* old_query_controller =
+      static_cast<MockQueryController*>(old_session_handle->GetController());
+
+  ClearQueryController();
+
+  auto query_controller_config_params = std::make_unique<
+      contextual_search::ContextualSearchContextController::ConfigParams>();
+  query_controller_config_params->send_lns_surface = false;
+  query_controller_config_params->enable_viewport_images = true;
+  auto new_query_controller_ptr = std::make_unique<MockQueryController>(
+      /*identity_manager=*/nullptr, url_loader_factory(),
+      version_info::Channel::UNKNOWN, "en-US", template_url_service(),
+      fake_variations_client(), std::move(query_controller_config_params));
+  auto* new_query_controller = new_query_controller_ptr.get();
+  auto new_metrics_recorder_ptr =
+      std::make_unique<MockContextualSearchMetricsRecorder>();
+
+  SetSessionHandle(
+      service()->CreateSessionForTesting(std::move(new_query_controller_ptr),
+                                         std::move(new_metrics_recorder_ptr)));
+
+  handler().GetContextualSessionHandle();
+
+  // Verify observation is moved to the new controller.
+  EXPECT_TRUE(
+      handler().context_controller_observation_for_testing().IsObservingSource(
+          new_query_controller));
+  EXPECT_FALSE(
+      handler().context_controller_observation_for_testing().IsObservingSource(
+          old_query_controller));
+
+  auto last_session_handle = TakeSessionHandle();
+  handler().GetContextualSessionHandle();
+
+  // Verify observation is stopped.
+  EXPECT_FALSE(
+      handler().context_controller_observation_for_testing().IsObserving());
+}
+
 TEST_F(ContextualSearchboxHandlerTest, SubmitQueryWithAdditionalParams) {
   // Ensure udm param is always set as an additional param.
   SubmitQueryAndWaitForNavigation();
diff --git a/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc b/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
index 1d77591..8887506 100644
--- a/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
+++ b/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
@@ -862,9 +862,9 @@
       {"glicDefaultTabAccessToggleSublabelDataProtected",
        IDS_SETTINGS_GLIC_PERMISSIONS_DEFAULT_TAB_ACCESS_TOGGLE_SUBLABEL_DATA_PROTECTED},
       {"glicWebActuationToggle",
-       IDS_SETTINGS_GLIC_PERMISSIONS_WEB_ACTUATION_TOGGLE},
+       IDS_SETTINGS_GLIC_PERMISSIONS_CHROME_WEB_ACTUATION_TOGGLE},
       {"glicWebActuationToggleSublabel",
-       IDS_SETTINGS_GLIC_PERMISSIONS_WEB_ACTUATION_TOGGLE_SUBLABEL},
+       IDS_SETTINGS_GLIC_PERMISSIONS_CHROME_WEB_ACTUATION_TOGGLE_SUBLABEL},
       {"glicActivityButton", IDS_SETTINGS_GLIC_PERMISSIONS_ACTIVITY_BUTTON},
       {"glicActivityButtonSublabel",
        IDS_SETTINGS_GLIC_PERMISSIONS_ACTIVITY_BUTTON_SUBLABEL},
diff --git a/chrome/browser/ui/webui/side_panel/reading_list/reading_list_page_handler_unittest.cc b/chrome/browser/ui/webui/side_panel/reading_list/reading_list_page_handler_unittest.cc
index 46fb8156..ccc411e 100644
--- a/chrome/browser/ui/webui/side_panel/reading_list/reading_list_page_handler_unittest.cc
+++ b/chrome/browser/ui/webui/side_panel/reading_list/reading_list_page_handler_unittest.cc
@@ -19,6 +19,7 @@
 #include "chrome/common/webui_url_constants.h"
 #include "chrome/test/base/browser_with_test_window_test.h"
 #include "chrome/test/base/test_browser_window.h"
+#include "chrome/test/base/ui_test_utils.h"
 #include "components/policy/core/common/policy_pref_names.h"
 #include "components/reading_list/core/reading_list_model.h"
 #include "components/reading_list/core/reading_list_test_utils.h"
@@ -90,7 +91,7 @@
  public:
   void SetUp() override {
     BrowserWithTestWindowTest::SetUp();
-    BrowserList::SetLastActive(browser());
+    ui_test_utils::DeprecatedFakeActivateBrowser(browser());
 
     incognito_browser_ =
         CreateBrowserWithTestWindowForParams(Browser::CreateParams(
diff --git a/chrome/browser/ui/webui/tab_search/tab_search.mojom b/chrome/browser/ui/webui/tab_search/tab_search.mojom
index e0793b47..f5bf1abb 100644
--- a/chrome/browser/ui/webui/tab_search/tab_search.mojom
+++ b/chrome/browser/ui/webui/tab_search/tab_search.mojom
@@ -25,7 +25,7 @@
   [Default] kNone = 0,
   kSelector = 1,
   kAutoTabGroups = 2,
-  kDeclutter = 3,
+  kDeclutter = 3, // Deprecated
 };
 
 enum TabOrganizationError {
@@ -233,12 +233,6 @@
   array<RecentlyClosedTab> recently_closed_tabs;
 };
 
-// Information about unused tabs, inclusive of stale and duplicate tabs.
-struct UnusedTabInfo {
-  array<Tab> stale_tabs;
-  map<string, array<Tab>> duplicate_tabs;
-};
-
 // Used by the WebUI page to bootstrap bidirectional communication.
 interface PageHandlerFactory {
   // The WebUI calls this method when the page is first initialized.
@@ -254,11 +248,6 @@
   // Close the tab hosting this WebUI.
   CloseWebUiTab();
 
-  // Bulk close multiple tabs as part of the declutter workflow. All tabs with
-  // either an id in tab_ids should be closed. All tabs but the oldest with a
-  // url in urls should be closed.
-  DeclutterTabs(array<int32> tab_ids, array<url.mojom.Url> urls);
-
   // Accept the tab organization, updated with the specified tab list.
   AcceptTabOrganization(int32 session_id,
                         int32 organization_id,
@@ -272,26 +261,12 @@
                         int32 organization_id,
                         mojo_base.mojom.String16 name);
 
-  // Exclude the given tab from the stale tabs list.
-  ExcludeFromStaleTabs(int32 tab_id);
-
-  // Exclude all tabs with the given URL from the duplicate tabs list.
-  ExcludeFromDuplicateTabs(url.mojom.Url url);
-
   // Get window and tab data for the current profile.
   GetProfileData() => (ProfileData profile_data);
 
-  // Get all tabs that are considered stale based on their last active time,
-  // and tabs that are duplicates of other tabs based on partial URL match
-  // (excludes fragments).
-  GetUnusedTabs() => (UnusedTabInfo tabs);
-
   // Get the currently active section of the tab search bubble.
   GetTabSearchSection() => (TabSearchSection section);
 
-  // Get the currently active tab organization feature.
-  GetTabOrganizationFeature() => (TabOrganizationFeature feature);
-
   // Get the current tab organization session info.
   GetTabOrganizationSession() => (TabOrganizationSession session);
 
@@ -327,10 +302,6 @@
   // list section.
   SaveRecentlyClosedExpandedPref(bool expanded);
 
-  // Set the user preference for which feature the tab organization selector
-  // should display when next shown.
-  SetOrganizationFeature(TabOrganizationFeature feature);
-
   // Force trigger the tab group tutorial.
   StartTabGroupTutorial();
 
@@ -391,10 +362,6 @@
   // Called when the top level bubble tab should change.
   TabSearchSectionChanged(TabSearchSection section);
 
-  // Called when the feature shown by the tab organization selector should
-  // change.
-  TabOrganizationFeatureChanged(TabOrganizationFeature feature);
-
   // Called when state of whether the first run experience should be shown
   // changes.
   ShowFREChanged(bool show);
@@ -402,10 +369,6 @@
   // Called when the tab organization feature should be enabled/disabled.
   TabOrganizationEnabledChanged(bool enabled);
 
-  // Called when the list of tabs considered stale based on their last active
-  // time, or duplicate based on partial URL match, has changed.
-  UnusedTabsChanged(UnusedTabInfo tabs);
-
   // Called when the tab corresponding to this WebUI, if any, has become newly
   // unsplit.
   TabUnsplit();
diff --git a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc
index 62d19c2..5c680b7a 100644
--- a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc
+++ b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc
@@ -42,7 +42,6 @@
 #include "chrome/browser/ui/interaction/browser_elements.h"
 #include "chrome/browser/ui/tabs/alert/tab_alert.h"
 #include "chrome/browser/ui/tabs/alert/tab_alert_controller.h"
-#include "chrome/browser/ui/tabs/organization/tab_declutter_controller.h"
 #include "chrome/browser/ui/tabs/organization/tab_organization_request.h"
 #include "chrome/browser/ui/tabs/organization/tab_organization_service.h"
 #include "chrome/browser/ui/tabs/organization/tab_organization_service_factory.h"
@@ -94,13 +93,6 @@
       ui::TimeFormat::FORMAT_ELAPSED, ui::TimeFormat::LENGTH_SHORT, elapsed));
 }
 
-std::string GetLastActiveElapsedTextForDeclutter(
-    const base::Time& last_active_time) {
-  const base::TimeDelta elapsed = base::Time::Now() - last_active_time;
-  return l10n_util::GetPluralStringFUTF8(IDS_DECLUTTER_TIMESTAMP,
-                                         elapsed.InDays());
-}
-
 // If Tab Group has no timestamp, we find the tab in the tab group with
 // the most recent navigation last active time.
 base::Time GetTabGroupTimeStamp(
@@ -192,20 +184,6 @@
 
 }  // namespace
 
-DuplicateTabsObserver::DuplicateTabsObserver(
-    content::WebContents* web_contents,
-    base::RepeatingCallback<void()> on_url_changed_callback)
-    : content::WebContentsObserver(web_contents),
-      on_url_changed_callback_(std::move(on_url_changed_callback)) {}
-
-DuplicateTabsObserver::~DuplicateTabsObserver() = default;
-
-void DuplicateTabsObserver::PrimaryPageChanged(content::Page& page) {
-  if (on_url_changed_callback_) {
-    on_url_changed_callback_.Run();
-  }
-}
-
 TabSearchPageHandler::TabSearchPageHandler(
     mojo::PendingReceiver<tab_search::mojom::PageHandler> receiver,
     mojo::PendingRemote<tab_search::mojom::Page> page,
@@ -238,11 +216,6 @@
       base::BindRepeating(&TabSearchPageHandler::NotifyTabIndexPrefChanged,
                           base::Unretained(this), profile));
   pref_change_registrar_.Add(
-      tab_search_prefs::kTabOrganizationFeature,
-      base::BindRepeating(
-          &TabSearchPageHandler::NotifyOrganizationFeaturePrefChanged,
-          base::Unretained(this), profile));
-  pref_change_registrar_.Add(
       tab_search_prefs::kTabOrganizationShowFRE,
       base::BindRepeating(&TabSearchPageHandler::NotifyShowFREPrefChanged,
                           base::Unretained(this), profile));
@@ -322,35 +295,6 @@
   // Do not add code past this point.
 }
 
-void TabSearchPageHandler::DeclutterTabs(const std::vector<int32_t>& tab_ids,
-                                         const std::vector<GURL>& urls) {
-  // TODO(crbug.com/358382903): Add metrics logging.
-  // Potentially also invoke IPH pending UX.
-  if (!tab_declutter_controller_) {
-    return;
-  }
-
-  std::vector<tabs::TabInterface*> tabs;
-
-  // Add tabs that are present in the current browser.
-  for (const int32_t tab_id : tab_ids) {
-    const std::optional<TabDetails> details = GetTabDetails(tab_id);
-    if (!details ||
-        details->tab->GetBrowserWindowInterface()->GetTabStripModel() !=
-            tab_declutter_controller_->tab_strip_model()) {
-      continue;
-    }
-
-    tabs.push_back(details->tab);
-  }
-  tab_declutter_controller_->DeclutterTabs(tabs, urls);
-
-  auto embedder = webui_controller_->embedder();
-  if (embedder) {
-    embedder->CloseUI();
-  }
-}
-
 void TabSearchPageHandler::AcceptTabOrganization(
     int32_t session_id,
     int32_t organization_id,
@@ -414,176 +358,6 @@
   organization->SetCurrentName(name);
 }
 
-void TabSearchPageHandler::ExcludeFromStaleTabs(int32_t tab_id) {
-  if (!tab_declutter_controller_) {
-    return;
-  }
-
-  std::optional<TabDetails> details = GetTabDetails(tab_id);
-
-  if (!details ||
-      details->tab->GetBrowserWindowInterface()->GetTabStripModel() !=
-          tab_declutter_controller_->tab_strip_model()) {
-    return;
-  }
-
-  tab_declutter_controller_->ExcludeFromStaleTabs(details->tab);
-
-  RemoveStaleTab(details->tab);
-
-  page_->UnusedTabsChanged(GetMojoUnusedTabs());
-}
-
-void TabSearchPageHandler::ExcludeFromDuplicateTabs(const GURL& url) {
-  if (!tab_declutter_controller_) {
-    return;
-  }
-
-  CHECK(duplicate_tabs_.count(url.GetWithoutRef()) > 0);
-
-  tab_declutter_controller_->ExcludeFromDuplicateTabs(url.GetWithoutRef());
-
-  std::vector<tabs::TabInterface*> tabs = duplicate_tabs_[url.GetWithoutRef()];
-
-  for (tabs::TabInterface* tab : tabs) {
-    RemoveDuplicateTab(tab);
-  }
-
-  page_->UnusedTabsChanged(GetMojoUnusedTabs());
-}
-
-void TabSearchPageHandler::RegisterInactiveTabDeclutterCallbacks(
-    tabs::TabInterface* tab) {
-  std::vector<base::CallbackListSubscription> subscriptions;
-
-  subscriptions.push_back(tab->RegisterDidActivate(
-      base::BindRepeating(&TabSearchPageHandler::OnStaleTabDidEnterForeground,
-                          base::Unretained(this))));
-
-  subscriptions.push_back(tab->RegisterWillDetach(base::BindRepeating(
-      [](TabSearchPageHandler* handler, tabs::TabInterface* tab,
-         tabs::TabInterface::DetachReason reason) {
-        handler->OnUnusedTabWillDetach(tab, reason, UnusedTabType::kInactive);
-      },
-      base::Unretained(this))));
-
-  subscriptions.push_back(tab->RegisterPinnedStateChanged(base::BindRepeating(
-      [](TabSearchPageHandler* handler, tabs::TabInterface* tab,
-         bool new_pinned_state) {
-        handler->OnUnusedTabPinnedStateChanged(tab, new_pinned_state,
-                                               UnusedTabType::kInactive);
-      },
-      base::Unretained(this))));
-
-  subscriptions.push_back(tab->RegisterGroupChanged(base::BindRepeating(
-      [](TabSearchPageHandler* handler, tabs::TabInterface* tab,
-         std::optional<tab_groups::TabGroupId> new_group) {
-        handler->OnUnusedTabGroupChanged(tab, new_group,
-                                         UnusedTabType::kInactive);
-      },
-      base::Unretained(this))));
-
-  inactive_tab_subscriptions_map_[tab] = std::move(subscriptions);
-}
-
-void TabSearchPageHandler::RegisterDuplicateTabDeclutterCallbacks(
-    tabs::TabInterface* tab) {
-  std::vector<base::CallbackListSubscription> subscriptions;
-
-  subscriptions.push_back(tab->RegisterWillDetach(base::BindRepeating(
-      [](TabSearchPageHandler* handler, tabs::TabInterface* tab,
-         tabs::TabInterface::DetachReason reason) {
-        handler->OnUnusedTabWillDetach(tab, reason, UnusedTabType::kDuplicate);
-      },
-      base::Unretained(this))));
-
-  subscriptions.push_back(tab->RegisterPinnedStateChanged(base::BindRepeating(
-      [](TabSearchPageHandler* handler, tabs::TabInterface* tab,
-         bool new_pinned_state) {
-        handler->OnUnusedTabPinnedStateChanged(tab, new_pinned_state,
-                                               UnusedTabType::kDuplicate);
-      },
-      base::Unretained(this))));
-
-  subscriptions.push_back(tab->RegisterGroupChanged(base::BindRepeating(
-      [](TabSearchPageHandler* handler, tabs::TabInterface* tab,
-         std::optional<tab_groups::TabGroupId> new_group) {
-        handler->OnUnusedTabGroupChanged(tab, new_group,
-                                         UnusedTabType::kDuplicate);
-      },
-      base::Unretained(this))));
-
-  subscriptions.push_back(tab->RegisterWillDiscardContents(base::BindRepeating(
-      &TabSearchPageHandler::OnDuplicateTabWillDiscardWebContents,
-      base::Unretained(this))));
-
-  content::WebContents* web_contents = tab->GetContents();
-  if (web_contents) {
-    auto observer = std::make_unique<DuplicateTabsObserver>(
-        web_contents,
-        base::BindRepeating(
-            [](TabSearchPageHandler* handler, tabs::TabInterface* tab) {
-              handler->RemoveDuplicateTab(tab);
-              handler->page_->UnusedTabsChanged(handler->GetMojoUnusedTabs());
-            },
-            base::Unretained(this), tab));
-
-    duplicate_tab_webcontents_observers_[tab] = std::move(observer);
-  }
-
-  duplicate_tab_subscriptions_map_[tab] = std::move(subscriptions);
-}
-
-void TabSearchPageHandler::UnregisterTabCallbacks() {
-  inactive_tab_subscriptions_map_.clear();
-  duplicate_tab_subscriptions_map_.clear();
-  duplicate_tab_webcontents_observers_.clear();
-}
-
-void TabSearchPageHandler::RemoveStaleTab(tabs::TabInterface* tab) {
-  CHECK(tab);
-  CHECK(std::find(stale_tabs_.begin(), stale_tabs_.end(), tab) !=
-        stale_tabs_.end());
-  CHECK(inactive_tab_subscriptions_map_.find(tab) !=
-        inactive_tab_subscriptions_map_.end());
-
-  // Remove the TabInterface from stale_tabs_
-  std::erase(stale_tabs_, tab);
-
-  // Unregister the subscriptions for this TabInterface
-  inactive_tab_subscriptions_map_.erase(tab);
-}
-
-void TabSearchPageHandler::RemoveDuplicateTab(tabs::TabInterface* tab) {
-  CHECK(tab);
-
-  for (auto& [duplicate_url, duplicate_tab_list] : duplicate_tabs_) {
-    auto found_it = std::ranges::find(duplicate_tab_list, tab);
-    if (found_it != duplicate_tab_list.end()) {
-      // Remove the specific tab from `duplicate_tabs_` and subscription maps.
-      duplicate_tab_list.erase(found_it);
-      duplicate_tab_subscriptions_map_.erase(tab);
-      duplicate_tab_webcontents_observers_.erase(tab);
-
-      // If there is only one more element remove it as having one entry is
-      // equivalent to having no duplicate items.
-      if (duplicate_tab_list.size() == 1) {
-        tabs::TabInterface* last_tab = duplicate_tab_list.front();
-        duplicate_tab_subscriptions_map_.erase(last_tab);
-        duplicate_tab_webcontents_observers_.erase(last_tab);
-        duplicate_tab_list.clear();
-      }
-
-      // If the list is now empty, remove it from `duplicate_tabs_`.
-      if (duplicate_tab_list.empty()) {
-        duplicate_tabs_.erase(duplicate_url);
-      }
-
-      return;
-    }
-  }
-}
-
 // Tab Search UI can also hosted inside a tab and so we still need to
 // be able to handle browser window changes.
 void TabSearchPageHandler::BrowserWindowInterfaceChanged() {
@@ -592,81 +366,9 @@
   browser_ = browser_window_interface
                  ? browser_window_interface->GetBrowserForMigrationOnly()
                  : nullptr;
-  SetTabDeclutterController(
-      browser_window_interface
-          ? browser_window_interface->GetFeatures().tab_declutter_controller()
-          : nullptr);
   page_->HostWindowChanged();
 }
 
-std::vector<tabs::TabInterface*>
-TabSearchPageHandler::FilterDuplicateTabsFromStaleTabs(
-    std::vector<tabs::TabInterface*> stale_tabs,
-    std::map<GURL, std::vector<tabs::TabInterface*>> duplicate_tabs) {
-  std::vector<tabs::TabInterface*> filtered_stale_tabs;
-
-  for (tabs::TabInterface* stale_tab : stale_tabs) {
-    GURL tab_url =
-        stale_tab->GetContents()->GetLastCommittedURL().GetWithoutRef();
-    if (duplicate_tabs.find(tab_url) == duplicate_tabs.end()) {
-      filtered_stale_tabs.push_back(stale_tab);
-    }
-  }
-
-  return filtered_stale_tabs;
-}
-
-void TabSearchPageHandler::OnStaleTabDidEnterForeground(
-    tabs::TabInterface* tab) {
-  RemoveStaleTab(static_cast<tabs::TabInterface*>(tab));
-  page_->UnusedTabsChanged(GetMojoUnusedTabs());
-}
-
-void TabSearchPageHandler::OnDuplicateTabWillDiscardWebContents(
-    tabs::TabInterface* tab,
-    content::WebContents* old_content,
-    content::WebContents* new_content) {
-  RemoveDuplicateTab(tab);
-  page_->UnusedTabsChanged(GetMojoUnusedTabs());
-}
-
-void TabSearchPageHandler::OnUnusedTabWillDetach(
-    tabs::TabInterface* tab,
-    tabs::TabInterface::DetachReason reason,
-    UnusedTabType type) {
-  if (type == UnusedTabType::kInactive) {
-    RemoveStaleTab(tab);
-  } else {
-    RemoveDuplicateTab(tab);
-  }
-  page_->UnusedTabsChanged(GetMojoUnusedTabs());
-}
-
-void TabSearchPageHandler::OnUnusedTabPinnedStateChanged(
-    tabs::TabInterface* tab,
-    bool new_pinned_state,
-    UnusedTabType type) {
-  if (type == UnusedTabType::kInactive) {
-    RemoveStaleTab(tab);
-  } else {
-    RemoveDuplicateTab(tab);
-  }
-
-  page_->UnusedTabsChanged(GetMojoUnusedTabs());
-}
-
-void TabSearchPageHandler::OnUnusedTabGroupChanged(
-    tabs::TabInterface* tab,
-    std::optional<tab_groups::TabGroupId> new_group,
-    UnusedTabType type) {
-  if (type == UnusedTabType::kInactive) {
-    RemoveStaleTab(tab);
-  } else {
-    RemoveDuplicateTab(tab);
-  }
-  page_->UnusedTabsChanged(GetMojoUnusedTabs());
-}
-
 void TabSearchPageHandler::GetProfileData(GetProfileDataCallback callback) {
   TRACE_EVENT0("browser", "TabSearchPageHandler:GetProfileTabs");
   auto profile_tabs = CreateProfileData();
@@ -694,11 +396,6 @@
   std::move(callback).Run(std::move(profile_tabs));
 }
 
-void TabSearchPageHandler::GetUnusedTabs(GetUnusedTabsCallback callback) {
-  UpdateUnusedTabs();
-  std::move(callback).Run(GetMojoUnusedTabs());
-}
-
 void TabSearchPageHandler::GetTabSearchSection(
     GetTabSearchSectionCallback callback) {
   PrefService* prefs = Profile::FromWebUI(web_ui_)->GetPrefs();
@@ -711,15 +408,6 @@
   std::move(callback).Run(section);
 }
 
-void TabSearchPageHandler::GetTabOrganizationFeature(
-    GetTabOrganizationFeatureCallback callback) {
-  PrefService* prefs = Profile::FromWebUI(web_ui_)->GetPrefs();
-  const tab_search::mojom::TabOrganizationFeature feature =
-      tab_search_prefs::GetTabOrganizationFeatureFromInt(
-          prefs->GetInteger(tab_search_prefs::kTabOrganizationFeature));
-  std::move(callback).Run(feature);
-}
-
 void TabSearchPageHandler::GetTabOrganizationSession(
     GetTabOrganizationSessionCallback callback) {
   if (!browser_ || !browser_->tab_strip_model()->SupportsTabGroups() ||
@@ -941,13 +629,6 @@
                : TabSearchRecentlyClosedToggleAction::kCollapse);
 }
 
-void TabSearchPageHandler::SetOrganizationFeature(
-    tab_search::mojom::TabOrganizationFeature feature) {
-  Profile::FromWebUI(web_ui_)->GetPrefs()->SetInteger(
-      tab_search_prefs::kTabOrganizationFeature,
-      tab_search_prefs::GetIntFromTabOrganizationFeature(feature));
-}
-
 void TabSearchPageHandler::StartTabGroupTutorial() {
   // Close the tab search bubble if showing.
   auto embedder = webui_controller_->embedder();
@@ -1162,134 +843,6 @@
   return profile_data;
 }
 
-void TabSearchPageHandler::UpdateUnusedTabs() {
-  stale_tabs_.clear();
-  duplicate_tabs_.clear();
-
-  UnregisterTabCallbacks();
-  if (!tab_declutter_controller_) {
-    return;
-  }
-
-  std::vector<tabs::TabInterface*> stale_tabs =
-      tab_declutter_controller_->GetStaleTabs();
-  std::map<GURL, std::vector<tabs::TabInterface*>> duplicate_tabs;
-
-  if (features::IsTabstripDedupeEnabled()) {
-    duplicate_tabs = tab_declutter_controller_->GetDuplicateTabs();
-  }
-
-  duplicate_tabs_ = duplicate_tabs;
-  stale_tabs_ = FilterDuplicateTabsFromStaleTabs(stale_tabs, duplicate_tabs_);
-
-  for (tabs::TabInterface* tab : stale_tabs_) {
-    RegisterInactiveTabDeclutterCallbacks(tab);
-  }
-
-  for (auto& [url, tabs] : duplicate_tabs_) {
-    for (auto& tab : tabs) {
-      RegisterDuplicateTabDeclutterCallbacks(tab);
-    }
-  }
-}
-
-void TabSearchPageHandler::SetTabDeclutterController(
-    tabs::TabDeclutterController* tab_declutter_controller) {
-  if (tab_declutter_controller == tab_declutter_controller_) {
-    return;
-  }
-
-  tab_declutter_observation_.Reset();
-  tab_declutter_controller_ = tab_declutter_controller;
-  if (tab_declutter_controller_) {
-    tab_declutter_observation_.Observe(tab_declutter_controller_.get());
-    UpdateUnusedTabs();
-    page_->UnusedTabsChanged(GetMojoUnusedTabs());
-  }
-}
-
-void TabSearchPageHandler::OnUnusedTabsProcessed(
-    std::vector<tabs::TabInterface*> stale_tabs,
-    std::map<GURL, std::vector<tabs::TabInterface*>> duplicate_tabs) {
-  stale_tabs_.clear();
-  duplicate_tabs_.clear();
-  UnregisterTabCallbacks();
-
-  duplicate_tabs_ = duplicate_tabs;
-  stale_tabs_ = FilterDuplicateTabsFromStaleTabs(stale_tabs, duplicate_tabs_);
-
-  for (tabs::TabInterface* tab : stale_tabs_) {
-    RegisterInactiveTabDeclutterCallbacks(tab);
-  }
-
-  for (auto& [url, tabs] : duplicate_tabs_) {
-    for (auto& tab : tabs) {
-      RegisterDuplicateTabDeclutterCallbacks(tab);
-    }
-  }
-
-  page_->UnusedTabsChanged(GetMojoUnusedTabs());
-}
-
-mojo::StructPtr<tab_search::mojom::UnusedTabInfo>
-TabSearchPageHandler::GetMojoUnusedTabs() {
-  auto unused_tabs = tab_search::mojom::UnusedTabInfo::New();
-  unused_tabs->stale_tabs = GetMojoStaleTabs();
-  unused_tabs->duplicate_tabs = GetMojoDuplicateTabs();
-  return unused_tabs;
-}
-
-std::vector<mojo::StructPtr<tab_search::mojom::Tab>>
-TabSearchPageHandler::GetMojoStaleTabs() {
-  std::vector<mojo::StructPtr<tab_search::mojom::Tab>> mojo_tabs;
-  if (!tab_declutter_controller_) {
-    return mojo_tabs;
-  }
-  TabStripModel* tab_strip_model = tab_declutter_controller_->tab_strip_model();
-
-  for (tabs::TabInterface* tab : stale_tabs_) {
-    const int tab_index =
-        tab_strip_model->GetIndexOfWebContents(tab->GetContents());
-    const std::string last_active_text = GetLastActiveElapsedTextForDeclutter(
-        tab->GetContents()->GetLastActiveTime());
-    mojo_tabs.push_back(GetTab(tab_strip_model, tab->GetContents(), tab_index,
-                               last_active_text));
-  }
-  return mojo_tabs;
-}
-
-base::flat_map<std::string,
-               std::vector<mojo::StructPtr<tab_search::mojom::Tab>>>
-TabSearchPageHandler::GetMojoDuplicateTabs() {
-  base::flat_map<std::string,
-                 std::vector<mojo::StructPtr<tab_search::mojom::Tab>>>
-      mojo_duplicate_tabs;
-
-  if (!tab_declutter_controller_) {
-    return mojo_duplicate_tabs;
-  }
-
-  TabStripModel* tab_strip_model = tab_declutter_controller_->tab_strip_model();
-
-  for (const auto& [url, tabs] : duplicate_tabs_) {
-    std::vector<mojo::StructPtr<tab_search::mojom::Tab>> mojo_tabs;
-
-    for (tabs::TabInterface* tab : tabs) {
-      const int tab_index =
-          tab_strip_model->GetIndexOfWebContents(tab->GetContents());
-      std::string last_active_text = GetLastActiveElapsedTextForDeclutter(
-          tab->GetContents()->GetLastActiveTime());
-
-      mojo_tabs.push_back(GetTab(tab_strip_model, tab->GetContents(), tab_index,
-                                 last_active_text));
-    }
-
-    mojo_duplicate_tabs.emplace(url.spec(), std::move(mojo_tabs));
-  }
-
-  return mojo_duplicate_tabs;
-}
-
 void TabSearchPageHandler::AddRecentlyClosedEntries(
     std::vector<tab_search::mojom::RecentlyClosedTabPtr>& recently_closed_tabs,
     std::vector<tab_search::mojom::RecentlyClosedTabGroupPtr>&
@@ -1419,8 +972,7 @@
 tab_search::mojom::TabPtr TabSearchPageHandler::GetTab(
     const TabStripModel* tab_strip_model,
     content::WebContents* contents,
-    int index,
-    std::string custom_last_active_text) const {
+    int index) const {
   auto tab_data = tab_search::mojom::Tab::New();
   tabs::TabInterface* const tab = tab_strip_model->GetTabAtIndex(index);
 
@@ -1484,9 +1036,8 @@
   // view pops up. To make it consistent, override the string to something
   // constant.
   tab_data->last_active_elapsed_text =
-      disable_last_active_time_for_testing_ ? "0"
-      : custom_last_active_text.length() > 0
-          ? custom_last_active_text
+      disable_last_active_time_for_testing_
+          ? "0"
           : GetLastActiveElapsedText(last_active_time_ticks);
 
   std::vector<tabs::TabAlert> alert_states =
@@ -1653,14 +1204,6 @@
       tab_search_prefs::GetTabSearchSectionFromInt(section_int));
 }
 
-void TabSearchPageHandler::NotifyOrganizationFeaturePrefChanged(
-    const Profile* profile) {
-  const int32_t feature_int = profile->GetPrefs()->GetInteger(
-      tab_search_prefs::kTabOrganizationFeature);
-  page_->TabOrganizationFeatureChanged(
-      tab_search_prefs::GetTabOrganizationFeatureFromInt(feature_int));
-}
-
 void TabSearchPageHandler::NotifyShowFREPrefChanged(const Profile* profile) {
   const bool show_fre = profile->GetPrefs()->GetBoolean(
       tab_search_prefs::kTabOrganizationShowFRE);
@@ -1820,11 +1363,6 @@
   page_->TabOrganizationEnabledChanged(enabled && organization_service_);
 }
 
-void TabSearchPageHandler::SetTabDeclutterControllerForTesting(
-    tabs::TabDeclutterController* tab_declutter_controller) {
-  SetTabDeclutterController(tab_declutter_controller);
-}
-
 bool TabSearchPageHandler::ShouldTrackBrowser(BrowserWindowInterface* browser) {
   return browser->GetProfile() == Profile::FromWebUI(web_ui_) &&
          browser->GetType() == BrowserWindowInterface::TYPE_NORMAL;
diff --git a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.h b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.h
index ad6b60ca..6e593f23 100644
--- a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.h
+++ b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.h
@@ -13,8 +13,6 @@
 #include "chrome/browser/ui/browser_tab_strip_tracker_delegate.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
 #include "chrome/browser/ui/tabs/organization/tab_data.h"
-#include "chrome/browser/ui/tabs/organization/tab_declutter_controller.h"
-#include "chrome/browser/ui/tabs/organization/tab_declutter_observer.h"
 #include "chrome/browser/ui/tabs/organization/tab_organization.h"
 #include "chrome/browser/ui/tabs/organization/tab_organization_observer.h"
 #include "chrome/browser/ui/tabs/organization/tab_organization_session.h"
@@ -39,10 +37,6 @@
 class TabOrganizationService;
 class OptimizationGuideKeyedService;
 
-namespace tabs {
-class TabDeclutterController;
-}
-
 // These values are persisted to logs. Entries should not be renumbered and
 // numeric values should never be reused.
 enum class TabSearchCloseAction {
@@ -59,26 +53,12 @@
   kMaxValue = kCollapse,
 };
 
-class DuplicateTabsObserver : public content::WebContentsObserver {
- public:
-  DuplicateTabsObserver(
-      content::WebContents* web_contents,
-      base::RepeatingCallback<void()> on_url_changed_callback);
-  ~DuplicateTabsObserver() override;
-
-  void PrimaryPageChanged(content::Page& page) override;
-
- private:
-  base::RepeatingCallback<void()> on_url_changed_callback_;
-};
-
 class TabSearchPageHandler
     : public tab_search::mojom::PageHandler,
       public TabStripModelObserver,
       public BrowserTabStripTrackerDelegate,
       public TabOrganizationSession::Observer,
       public TabOrganizationObserver,
-      public TabDeclutterObserver,
       public optimization_guide::SettingsEnabledObserver {
  public:
   TabSearchPageHandler(
@@ -94,8 +74,6 @@
   // tab_search::mojom::PageHandler:
   void CloseTab(int32_t tab_id) override;
   void CloseWebUiTab() override;
-  void DeclutterTabs(const std::vector<int32_t>& tab_ids,
-                     const std::vector<GURL>& urls) override;
   void AcceptTabOrganization(
       int32_t session_id,
       int32_t organization_id,
@@ -105,13 +83,8 @@
   void RenameTabOrganization(int32_t session_id,
                              int32_t organization_id,
                              const std::u16string& name) override;
-  void ExcludeFromStaleTabs(int32_t tab_id) override;
-  void ExcludeFromDuplicateTabs(const GURL& url) override;
   void GetProfileData(GetProfileDataCallback callback) override;
-  void GetUnusedTabs(GetUnusedTabsCallback callback) override;
   void GetTabSearchSection(GetTabSearchSectionCallback callback) override;
-  void GetTabOrganizationFeature(
-      GetTabOrganizationFeatureCallback callback) override;
   void GetTabOrganizationSession(
       GetTabOrganizationSessionCallback callback) override;
   void GetTabOrganizationModelStrategy(
@@ -128,8 +101,6 @@
   void ReplaceActiveSplitTab(int32_t replacement_tab_id) override;
   void RestartSession() override;
   void SaveRecentlyClosedExpandedPref(bool expanded) override;
-  void SetOrganizationFeature(
-      tab_search::mojom::TabOrganizationFeature feature) override;
   void StartTabGroupTutorial() override;
   void TriggerFeedback(int32_t session_id) override;
   void TriggerSignIn() override;
@@ -153,11 +124,6 @@
                       TabChangeType change_type) override;
   void OnSplitTabChanged(const SplitTabChange& change) override;
 
-  // TabDeclutterObserver:
-  void OnUnusedTabsProcessed(
-      std::vector<tabs::TabInterface*> stale_tabs,
-      std::map<GURL, std::vector<tabs::TabInterface*>> duplicate_tabs) override;
-
   // BrowserTabStripTrackerDelegate:
   bool ShouldTrackBrowser(BrowserWindowInterface* browser) override;
 
@@ -189,18 +155,6 @@
     disable_last_active_time_for_testing_ = true;
   }
 
-  std::vector<tabs::TabInterface*> stale_tabs_for_testing() {
-    return stale_tabs_;
-  }
-
-  std::map<GURL, std::vector<tabs::TabInterface*>>
-  duplicate_tabs_for_testing() {
-    return duplicate_tabs_;
-  }
-
-  void SetTabDeclutterControllerForTesting(
-      tabs::TabDeclutterController* tab_declutter_controller);
-
   static constexpr int kMinRecentlyClosedItemDisplayCount = 8;
 
  protected:
@@ -213,8 +167,6 @@
   // leveraging DedupKey comparisons.
   typedef std::tuple<GURL, std::optional<base::Token>> DedupKey;
 
-  enum class UnusedTabType { kInactive, kDuplicate };
-
   // Encapsulates tab details to facilitate performing an action on a tab.
   struct TabDetails {
     explicit TabDetails(tabs::TabInterface* tab) : tab(tab) {}
@@ -232,10 +184,6 @@
   void MaybeShowUI();
 
   tab_search::mojom::ProfileDataPtr CreateProfileData();
-  void UpdateUnusedTabs();
-
-  void SetTabDeclutterController(
-      tabs::TabDeclutterController* tab_declutter_controller);
 
   // Adds recently closed tabs and tab groups.
   void AddRecentlyClosedEntries(
@@ -258,11 +206,9 @@
       std::set<tab_groups::TabGroupId>& tab_group_ids,
       std::vector<tab_search::mojom::TabGroupPtr>& tab_groups);
 
-  tab_search::mojom::TabPtr GetTab(
-      const TabStripModel* tab_strip_model,
-      content::WebContents* contents,
-      int index,
-      std::string custom_last_active_text = "") const;
+  tab_search::mojom::TabPtr GetTab(const TabStripModel* tab_strip_model,
+                                   content::WebContents* contents,
+                                   int index) const;
   tab_search::mojom::RecentlyClosedTabPtr GetRecentlyClosedTab(
       sessions::tab_restore::Tab* tab,
       const base::Time& close_time);
@@ -279,51 +225,11 @@
 
   void NotifyTabIndexPrefChanged(const Profile* profile);
 
-  void NotifyOrganizationFeaturePrefChanged(const Profile* profile);
-
   void NotifyShowFREPrefChanged(const Profile* profile);
 
-  mojo::StructPtr<tab_search::mojom::UnusedTabInfo> GetMojoUnusedTabs();
-  std::vector<mojo::StructPtr<tab_search::mojom::Tab>> GetMojoStaleTabs();
-  base::flat_map<std::string,
-                 std::vector<mojo::StructPtr<tab_search::mojom::Tab>>>
-  GetMojoDuplicateTabs();
-
-  void UnregisterTabCallbacks();
-  void RegisterInactiveTabDeclutterCallbacks(tabs::TabInterface* tab);
-  void RegisterDuplicateTabDeclutterCallbacks(tabs::TabInterface* tab);
-
-  void OnStaleTabDidEnterForeground(tabs::TabInterface* tab);
-  void OnDuplicateTabWillDiscardWebContents(tabs::TabInterface* tab,
-                                            content::WebContents* old_content,
-                                            content::WebContents* new_content);
-
-  void OnUnusedTabWillDetach(tabs::TabInterface* tab,
-                             tabs::TabInterface::DetachReason reason,
-                             UnusedTabType type);
-  void OnUnusedTabPinnedStateChanged(tabs::TabInterface* tab,
-                                     bool new_pinned_state,
-                                     UnusedTabType type);
-  void OnUnusedTabGroupChanged(tabs::TabInterface* tab,
-                               std::optional<tab_groups::TabGroupId> new_group,
-                               UnusedTabType type);
-
-  void RemoveStaleTab(tabs::TabInterface* tab);
-
-  // Removes a tab from the duplicate tab list, along with its associated
-  // subscriptions and observations. If the duplicate list for the tab's URL
-  // contains only one remaining tab after removal, that tab is also removed,
-  // and the list is erased from the map. If the tab is not found, the method
-  // exits without performing any action.
-  void RemoveDuplicateTab(tabs::TabInterface* tab);
-
   // Called when the browser window context for this WebUI has changed.
   void BrowserWindowInterfaceChanged();
 
-  std::vector<tabs::TabInterface*> FilterDuplicateTabsFromStaleTabs(
-      std::vector<tabs::TabInterface*> stale_tabs,
-      std::map<GURL, std::vector<tabs::TabInterface*>> duplicate_tabs);
-
   mojo::Receiver<tab_search::mojom::PageHandler> receiver_;
   mojo::Remote<tab_search::mojom::Page> page_;
   const raw_ptr<content::WebUI> web_ui_;
@@ -335,7 +241,6 @@
   raw_ptr<TabOrganizationService> organization_service_;
   PrefChangeRegistrar pref_change_registrar_;
   raw_ptr<OptimizationGuideKeyedService> optimization_guide_keyed_service_;
-  raw_ptr<tabs::TabDeclutterController> tab_declutter_controller_;
 
   // Tracks how many times |CloseTab()| has been evoked for the currently open
   // instance of Tab Search for logging in UMA.
@@ -366,23 +271,8 @@
   std::vector<raw_ptr<TabOrganizationSession, VectorExperimental>>
       listened_sessions_;
 
-  std::vector<tabs::TabInterface*> stale_tabs_;
-  std::map<GURL, std::vector<tabs::TabInterface*>> duplicate_tabs_;
-
-  std::map<tabs::TabInterface*, std::vector<base::CallbackListSubscription>>
-      inactive_tab_subscriptions_map_;
-
-  std::map<tabs::TabInterface*, std::vector<base::CallbackListSubscription>>
-      duplicate_tab_subscriptions_map_;
-
-  std::map<tabs::TabInterface*, std::unique_ptr<DuplicateTabsObserver>>
-      duplicate_tab_webcontents_observers_;
-
   base::ScopedObservation<TabOrganizationService, TabOrganizationObserver>
       tab_organization_observation_{this};
-
-  base::ScopedObservation<tabs::TabDeclutterController, TabDeclutterObserver>
-      tab_declutter_observation_{this};
 };
 
 #endif  // CHROME_BROWSER_UI_WEBUI_TAB_SEARCH_TAB_SEARCH_PAGE_HANDLER_H_
diff --git a/chrome/browser/ui/webui/tab_search/tab_search_page_handler_unittest.cc b/chrome/browser/ui/webui/tab_search/tab_search_page_handler_unittest.cc
index 1c629b1..c8ce149 100644
--- a/chrome/browser/ui/webui/tab_search/tab_search_page_handler_unittest.cc
+++ b/chrome/browser/ui/webui/tab_search/tab_search_page_handler_unittest.cc
@@ -29,7 +29,6 @@
 #include "chrome/browser/ui/tab_ui_helper.h"
 #include "chrome/browser/ui/tabs/alert/tab_alert.h"
 #include "chrome/browser/ui/tabs/alert/tab_alert_controller.h"
-#include "chrome/browser/ui/tabs/organization/tab_declutter_controller.h"
 #include "chrome/browser/ui/tabs/public/tab_features.h"
 #include "chrome/browser/ui/tabs/saved_tab_groups/tab_group_sync_service_initialized_observer.h"
 #include "chrome/browser/ui/tabs/split_tab_metrics.h"
@@ -93,18 +92,6 @@
 constexpr char kTabName5[] = "Tab 5";
 constexpr char kTabName6[] = "Tab 6";
 
-class MockTabDeclutterController : public tabs::TabDeclutterController {
- public:
-  explicit MockTabDeclutterController(BrowserWindowInterface* browser)
-      : TabDeclutterController(browser) {}
-
-  MOCK_METHOD(std::vector<tabs::TabInterface*>, GetStaleTabs, (), (override));
-  MOCK_METHOD((std::map<GURL, std::vector<tabs::TabInterface*>>),
-              GetDuplicateTabs,
-              (),
-              (override));
-};
-
 class MockPage : public tab_search::mojom::Page {
  public:
   MockPage() = default;
@@ -129,12 +116,8 @@
   MOCK_METHOD(void,
               TabSearchSectionChanged,
               (tab_search::mojom::TabSearchSection));
-  MOCK_METHOD(void,
-              TabOrganizationFeatureChanged,
-              (tab_search::mojom::TabOrganizationFeature));
   MOCK_METHOD(void, ShowFREChanged, (bool));
   MOCK_METHOD(void, TabOrganizationEnabledChanged, (bool));
-  MOCK_METHOD(void, UnusedTabsChanged, (tab_search::mojom::UnusedTabInfoPtr));
   MOCK_METHOD(void, TabUnsplit, ());
 };
 
@@ -215,17 +198,9 @@
     browser1()->DidBecomeActive();
     webui_controller_ = std::make_unique<TabSearchUI>(web_ui());
 
-    // Handle any mock calls that might occur during the instantiation of the
-    // TabSearchPageHandler. Tests are able to override these calls with more
-    // specific EXPECT_CALLS.
-    EXPECT_CALL(page_, UnusedTabsChanged(testing::_))
-        .WillRepeatedly(testing::Return());
-
     handler_ = std::make_unique<TestTabSearchPageHandler>(
         page_.BindAndGetRemote(), web_ui(), webui_controller_.get());
     EXPECT_CALL(page_, HostWindowChanged()).Times(1);
-    feature_list_.InitWithFeatures(
-        {features::kTabstripDeclutter, features::kTabstripDedupe}, {});
 
     // Wait for the TabGroupSyncService to properly initialize before making any
     // changes to tab groups.
@@ -1025,337 +1000,6 @@
   session.reset();
 }
 
-class TabSearchPageHandlerDeclutterTest : public TabSearchPageHandlerTest {
- public:
-  TabSearchPageHandlerDeclutterTest() = default;
-  ~TabSearchPageHandlerDeclutterTest() override = default;
-
-  void SetUp() override {
-    TabSearchPageHandlerTest::SetUp();
-
-    testing_profile_ = std::make_unique<TestingProfile>();
-    tab_strip_model_delegate_ =
-        std::make_unique<TabSearchTabStripModelDelegate>();
-    tab_strip_model_ = std::make_unique<TabStripModel>(
-        tab_strip_model_delegate_.get(), testing_profile_.get());
-
-    browser_window_interface_ = std::make_unique<MockBrowserWindowInterface>();
-    ON_CALL(*browser_window_interface_, GetUnownedUserDataHost())
-        .WillByDefault(::testing::ReturnRef(user_data_host_));
-    ON_CALL(*browser_window_interface_, GetTabStripModel())
-        .WillByDefault(::testing::Return(tab_strip_model_.get()));
-
-    tab_declutter_controller_ = std::make_unique<MockTabDeclutterController>(
-        browser_window_interface_.get());
-    tab_declutter_controller_->DidBecomeActive(browser_window_interface_.get());
-    handler()->SetTabDeclutterControllerForTesting(
-        tab_declutter_controller_.get());
-  }
-
-  void TearDown() override {
-    // Remove the tab declutter observation first.
-    handler()->SetTabDeclutterControllerForTesting(nullptr);
-
-    tab_declutter_controller_.reset();
-    tab_strip_model_.reset();
-    tab_strip_model_delegate_.reset();
-    testing_profile_.reset();
-    TabSearchPageHandlerTest::TearDown();
-  }
-
-  MockTabDeclutterController* tab_declutter_controller() {
-    return tab_declutter_controller_.get();
-  }
-  TabStripModel* fake_tab_strip_model() { return tab_strip_model_.get(); }
-  Profile* testing_profile() { return testing_profile_.get(); }
-
-  void CloseTab(int index) {
-    fake_tab_strip_model()->CloseWebContentsAt(index,
-                                               TabCloseTypes::CLOSE_NONE);
-  }
-
-  tabs::TabInterface* AppendBackgroundTab() {
-    std::unique_ptr<tabs::TabModel> tab_model =
-        std::make_unique<tabs::TabModel>(
-            content::WebContents::Create(
-                content::WebContents::CreateParams(testing_profile())),
-            fake_tab_strip_model());
-    tabs::TabFeatures* const tab_features = tab_model->GetTabFeatures();
-    tabs::TabInterface* const tab_interface = tab_model.get();
-    std::unique_ptr<TabUIHelper> tab_ui_helper =
-        tabs::TabFeatures::GetUserDataFactoryForTesting()
-            .CreateInstance<TabUIHelper>(*tab_interface, *tab_interface);
-    tab_features->SetTabUIHelperForTesting(std::move(tab_ui_helper));
-    std::unique_ptr<tabs::TabAlertController> tab_alert_controller =
-        tabs::TabFeatures::GetUserDataFactoryForTesting()
-            .CreateInstance<tabs::TabAlertController>(*tab_interface,
-                                                      *tab_interface);
-    tab_features->SetTabAlertControllerForTesting(
-        std::move(tab_alert_controller));
-    fake_tab_strip_model()->AppendTab(std::move(tab_model), false);
-    return tab_interface;
-  }
-
- private:
-  ui::UnownedUserDataHost user_data_host_;
-  std::unique_ptr<TestingProfile> testing_profile_;
-  std::unique_ptr<TabSearchTabStripModelDelegate> tab_strip_model_delegate_;
-  std::unique_ptr<TabStripModel> tab_strip_model_;
-  std::unique_ptr<MockTabDeclutterController> tab_declutter_controller_;
-  std::unique_ptr<MockBrowserWindowInterface> browser_window_interface_;
-  const tabs::TabModel::PreventFeatureInitializationForTesting prevent_;
-};
-
-TEST_F(TabSearchPageHandlerDeclutterTest, TabDeclutterFindUnusedTabs) {
-  EXPECT_CALL(page_, UnusedTabsChanged(_)).Times(1);
-
-  // Create stale tabs.
-  std::vector<tabs::TabInterface*> stale_tabs_raw_ptr;
-  for (int i = 0; i < 4; ++i) {
-    stale_tabs_raw_ptr.push_back(AppendBackgroundTab());
-  }
-
-  // Create duplicate tabs.
-  std::map<GURL, std::vector<tabs::TabInterface*>> duplicate_tabs;
-  GURL duplicate_tabs_url("https://duplicate_url.com");
-  for (int i = 0; i < 2; ++i) {
-    duplicate_tabs[duplicate_tabs_url].push_back(AppendBackgroundTab());
-  }
-
-  EXPECT_CALL(*tab_declutter_controller(), GetStaleTabs())
-      .WillOnce(testing::Return(stale_tabs_raw_ptr));
-
-  EXPECT_CALL(*tab_declutter_controller(), GetDuplicateTabs())
-      .WillOnce(testing::Return(duplicate_tabs));
-
-  tab_search::mojom::PageHandler::GetUnusedTabsCallback callback =
-      base::BindLambdaForTesting(
-          [&](tab_search::mojom::UnusedTabInfoPtr unused_tabs) {
-            // Verify stale tabs.
-            EXPECT_EQ(4u, unused_tabs->stale_tabs.size());
-
-            // Verify duplicate tabs.
-            auto it =
-                unused_tabs->duplicate_tabs.find(duplicate_tabs_url.spec());
-            ASSERT_NE(it, unused_tabs->duplicate_tabs.end());
-            EXPECT_EQ(2u, it->second.size());
-          });
-
-  handler()->GetUnusedTabs(std::move(callback));
-}
-
-TEST_F(TabSearchPageHandlerDeclutterTest, TabDeclutterExcludeTabs) {
-  EXPECT_CALL(page_, UnusedTabsChanged(_)).Times(2);
-
-  // Create stale tabs.
-  std::vector<tabs::TabInterface*> stale_tabs_raw_ptr;
-  for (int i = 0; i < 4; ++i) {
-    stale_tabs_raw_ptr.push_back(AppendBackgroundTab());
-  }
-
-  // Create duplicate tabs.
-  std::map<GURL, std::vector<tabs::TabInterface*>> duplicate_tabs;
-  GURL duplicate_tabs_url("https://duplicate_url.com");
-  for (int i = 0; i < 2; ++i) {
-    tabs::TabInterface* const tab_interface = AppendBackgroundTab();
-    auto navigation_simulator =
-        content::NavigationSimulator::CreateBrowserInitiated(
-            duplicate_tabs_url, tab_interface->GetContents());
-    navigation_simulator->Commit();
-    duplicate_tabs[duplicate_tabs_url].push_back(tab_interface);
-  }
-
-  EXPECT_CALL(*tab_declutter_controller(), GetStaleTabs())
-      .WillOnce(testing::Return(stale_tabs_raw_ptr));
-
-  EXPECT_CALL(*tab_declutter_controller(), GetDuplicateTabs())
-      .WillOnce(testing::Return(duplicate_tabs));
-
-  tab_search::mojom::PageHandler::GetUnusedTabsCallback callback =
-      base::BindLambdaForTesting(
-          [&](tab_search::mojom::UnusedTabInfoPtr unused_tabs) {
-            // Verify stale tabs.
-            EXPECT_EQ(4u, unused_tabs->stale_tabs.size());
-
-            // Verify duplicate tabs.
-            auto it =
-                unused_tabs->duplicate_tabs.find(duplicate_tabs_url.spec());
-            ASSERT_NE(it, unused_tabs->duplicate_tabs.end());
-            EXPECT_EQ(2u, it->second.size());
-          });
-
-  handler()->GetUnusedTabs(std::move(callback));
-
-  handler()->ExcludeFromDuplicateTabs(duplicate_tabs_url.GetWithoutRef());
-  EXPECT_TRUE(handler()->duplicate_tabs_for_testing().empty());
-}
-
-TEST_F(TabSearchPageHandlerDeclutterTest, TabDeclutterObserverTest) {
-  EXPECT_CALL(page_, UnusedTabsChanged(_)).Times(2);
-  std::vector<tabs::TabInterface*> stale_tabs_raw_ptr;
-
-  for (int i = 0; i < 4; ++i) {
-    stale_tabs_raw_ptr.push_back(AppendBackgroundTab());
-  }
-
-  std::map<GURL, std::vector<tabs::TabInterface*>> duplicate_tabs;
-  GURL duplicate_tabs_url("https://duplicate_url.com");
-  for (int i = 0; i < 2; ++i) {
-    duplicate_tabs[duplicate_tabs_url].push_back(AppendBackgroundTab());
-  }
-
-  EXPECT_CALL(*tab_declutter_controller(), GetStaleTabs())
-      .WillRepeatedly(testing::Return(stale_tabs_raw_ptr));
-
-  EXPECT_CALL(*tab_declutter_controller(), GetDuplicateTabs())
-      .WillRepeatedly(testing::Return(duplicate_tabs));
-
-  auto task_runner = base::MakeRefCounted<base::TestMockTimeTaskRunner>();
-  base::TestMockTimeTaskRunner::ScopedContext scoped_context(task_runner);
-  tab_declutter_controller()->SetTimerForTesting(
-      task_runner->GetMockTickClock(), task_runner);
-
-  task_runner->FastForwardBy(
-      tab_declutter_controller()->declutter_timer_interval());
-}
-
-TEST_F(TabSearchPageHandlerDeclutterTest, TabDeclutterUnusedTabChanges) {
-  EXPECT_CALL(page_, UnusedTabsChanged(_)).Times(::testing::AtLeast(1));
-  std::vector<tabs::TabInterface*> stale_tabs_raw_ptr;
-
-  // Create 10 stale tabs.
-  for (int i = 0; i < 10; ++i) {
-    stale_tabs_raw_ptr.push_back(AppendBackgroundTab());
-  }
-
-  // Create duplicate tabs.
-  std::map<GURL, std::vector<tabs::TabInterface*>> duplicate_tabs;
-  GURL duplicate_tabs_url("https://duplicate_url.com");
-  for (int i = 0; i < 5; ++i) {
-    tabs::TabInterface* const tab_interface = AppendBackgroundTab();
-    auto navigation_simulator =
-        content::NavigationSimulator::CreateBrowserInitiated(
-            duplicate_tabs_url, tab_interface->GetContents());
-    navigation_simulator->Commit();
-    duplicate_tabs[duplicate_tabs_url].push_back(tab_interface);
-  }
-
-  EXPECT_CALL(*tab_declutter_controller(), GetStaleTabs())
-      .WillRepeatedly(testing::Return(stale_tabs_raw_ptr));
-
-  EXPECT_CALL(*tab_declutter_controller(), GetDuplicateTabs())
-      .WillOnce(testing::Return(duplicate_tabs));
-
-  tab_search::mojom::PageHandler::GetUnusedTabsCallback callback =
-      base::BindLambdaForTesting(
-          [&](tab_search::mojom::UnusedTabInfoPtr unused_tabs) {
-            // Verify stale tabs.
-            EXPECT_EQ(10u, unused_tabs->stale_tabs.size());
-
-            // Verify duplicate tabs.
-            auto it =
-                unused_tabs->duplicate_tabs.find(duplicate_tabs_url.spec());
-            ASSERT_NE(it, unused_tabs->duplicate_tabs.end());
-            EXPECT_EQ(5u, it->second.size());
-          });
-
-  handler()->GetUnusedTabs(std::move(callback));
-
-  // Make a stale tab a part of a group. It should remove it from the internal
-  // stale tab list.
-  fake_tab_strip_model()->AddToNewGroup({1});
-  EXPECT_EQ(handler()->stale_tabs_for_testing().size(), 9u);
-
-  // Make a stale tab pinned. It should remove it from the internal stale tab
-  // list.
-  fake_tab_strip_model()->SetTabPinned(2, true);
-  EXPECT_EQ(handler()->stale_tabs_for_testing().size(), 8u);
-
-  // Activate a stale tab. It should remove it from the internal stale tab list.
-  fake_tab_strip_model()->ActivateTabAt(3);
-  EXPECT_EQ(handler()->stale_tabs_for_testing().size(), 7u);
-
-  // Detach a stale tab. It should remove it from the internal stale tab list.
-  CloseTab(4);
-  EXPECT_EQ(handler()->stale_tabs_for_testing().size(), 6u);
-
-  fake_tab_strip_model()->AddToNewGroup({fake_tab_strip_model()->count() - 1});
-  EXPECT_EQ(handler()->duplicate_tabs_for_testing()[duplicate_tabs_url].size(),
-            4u);
-
-  fake_tab_strip_model()->SetTabPinned(fake_tab_strip_model()->count() - 2,
-                                       true);
-  EXPECT_EQ(handler()->duplicate_tabs_for_testing()[duplicate_tabs_url].size(),
-            3u);
-
-  CloseTab(fake_tab_strip_model()->count() - 2);
-  EXPECT_EQ(handler()->duplicate_tabs_for_testing()[duplicate_tabs_url].size(),
-            2u);
-}
-
-TEST_F(TabSearchPageHandlerDeclutterTest, TabDuplicateURLChanges) {
-  EXPECT_CALL(page_, UnusedTabsChanged(_)).Times(::testing::AtLeast(1));
-  std::vector<tabs::TabInterface*> stale_tabs_raw_ptr;
-
-  // Create 10 stale tabs.
-  for (int i = 0; i < 10; ++i) {
-    stale_tabs_raw_ptr.push_back(AppendBackgroundTab());
-  }
-
-  // Create duplicate tabs.
-  std::map<GURL, std::vector<tabs::TabInterface*>> duplicate_tabs;
-  GURL duplicate_tabs_url("https://duplicate_url.com");
-  for (int i = 0; i < 5; ++i) {
-    tabs::TabInterface* const tab_interface = AppendBackgroundTab();
-
-    auto navigation_simulator =
-        content::NavigationSimulator::CreateBrowserInitiated(
-            duplicate_tabs_url, tab_interface->GetContents());
-    navigation_simulator->Commit();
-    duplicate_tabs[duplicate_tabs_url].push_back(tab_interface);
-  }
-
-  EXPECT_CALL(*tab_declutter_controller(), GetStaleTabs())
-      .WillRepeatedly(testing::Return(stale_tabs_raw_ptr));
-
-  EXPECT_CALL(*tab_declutter_controller(), GetDuplicateTabs())
-      .WillOnce(testing::Return(duplicate_tabs));
-
-  tab_search::mojom::PageHandler::GetUnusedTabsCallback callback =
-      base::BindLambdaForTesting(
-          [&](tab_search::mojom::UnusedTabInfoPtr unused_tabs) {
-            // Verify stale tabs.
-            EXPECT_EQ(10u, unused_tabs->stale_tabs.size());
-
-            // Verify duplicate tabs.
-            auto it =
-                unused_tabs->duplicate_tabs.find(duplicate_tabs_url.spec());
-            ASSERT_NE(it, unused_tabs->duplicate_tabs.end());
-            EXPECT_EQ(5u, it->second.size());
-          });
-
-  handler()->GetUnusedTabs(std::move(callback));
-
-  GURL new_url("https://duplicate_url_two.com");
-
-  auto navigation_simulator =
-      content::NavigationSimulator::CreateBrowserInitiated(
-          new_url, fake_tab_strip_model()
-                       ->GetTabAtIndex(fake_tab_strip_model()->count() - 1)
-                       ->GetContents());
-  navigation_simulator->Commit();
-
-  EXPECT_EQ(handler()->duplicate_tabs_for_testing()[duplicate_tabs_url].size(),
-            4u);
-
-  fake_tab_strip_model()->DiscardWebContentsAt(
-      fake_tab_strip_model()->count() - 2,
-      content::WebContents::Create(
-          content::WebContents::CreateParams(testing_profile())));
-  EXPECT_EQ(handler()->duplicate_tabs_for_testing()[duplicate_tabs_url].size(),
-            3u);
-}
-
 TEST_F(TabSearchPageHandlerTest, ReplaceActiveSplitTab) {
   std::unique_ptr<content::WebContents> test_web_contents(
       content::WebContentsTester::CreateTestWebContents(
diff --git a/chrome/browser/ui/webui/tab_search/tab_search_prefs.cc b/chrome/browser/ui/webui/tab_search/tab_search_prefs.cc
index d32342d..6af251b4 100644
--- a/chrome/browser/ui/webui/tab_search/tab_search_prefs.cc
+++ b/chrome/browser/ui/webui/tab_search/tab_search_prefs.cc
@@ -25,10 +25,6 @@
 // has been activated or closed).
 const char kTabSearchUsed[] = "tab_search.used";
 
-// Integer pref indicating which organization feature, if any, the Tab
-// Organization Selector should open to when shown.
-const char kTabOrganizationFeature[] = "tab_organization.feature";
-
 // Boolean pref indicating whether the user should see the first run experience
 // when interacting with the Tab Organization UI.
 const char kTabOrganizationShowFRE[] = "tab_organization.show_fre_2";
@@ -46,10 +42,6 @@
       kTabSearchTabIndex,
       GetIntFromTabSearchSection(tab_search::mojom::TabSearchSection::kSearch));
   registry->RegisterBooleanPref(kTabSearchUsed, false);
-  registry->RegisterIntegerPref(
-      kTabOrganizationFeature,
-      GetIntFromTabOrganizationFeature(
-          tab_search::mojom::TabOrganizationFeature::kSelector));
   registry->RegisterBooleanPref(kTabOrganizationShowFRE, true);
   registry->RegisterIntegerPref(kTabOrganizationModelStrategy, 0);
   registry->RegisterIntegerPref(kTabDeclutterUsageCount, 0);
@@ -66,15 +58,4 @@
   return std::to_underlying(section);
 }
 
-tab_search::mojom::TabOrganizationFeature GetTabOrganizationFeatureFromInt(
-    const int feature) {
-  return ToKnownEnumValue(
-      static_cast<tab_search::mojom::TabOrganizationFeature>(feature));
-}
-
-int GetIntFromTabOrganizationFeature(
-    const tab_search::mojom::TabOrganizationFeature feature) {
-  return std::to_underlying(feature);
-}
-
 }  // namespace tab_search_prefs
diff --git a/chrome/browser/ui/webui/tab_search/tab_search_prefs.h b/chrome/browser/ui/webui/tab_search/tab_search_prefs.h
index 174f9f0..684afe2 100644
--- a/chrome/browser/ui/webui/tab_search/tab_search_prefs.h
+++ b/chrome/browser/ui/webui/tab_search/tab_search_prefs.h
@@ -21,8 +21,6 @@
 
 extern const char kTabSearchUsed[];
 
-extern const char kTabOrganizationFeature[];
-
 extern const char kTabOrganizationShowFRE[];
 
 extern const char kTabOrganizationModelStrategy[];
@@ -37,12 +35,6 @@
 int GetIntFromTabSearchSection(
     const tab_search::mojom::TabSearchSection section);
 
-tab_search::mojom::TabOrganizationFeature GetTabOrganizationFeatureFromInt(
-    const int feature);
-
-int GetIntFromTabOrganizationFeature(
-    const tab_search::mojom::TabOrganizationFeature feature);
-
 }  // namespace tab_search_prefs
 
 #endif  // CHROME_BROWSER_UI_WEBUI_TAB_SEARCH_TAB_SEARCH_PREFS_H_
diff --git a/chrome/browser/ui/webui_browser/webui_browser_window.cc b/chrome/browser/ui/webui_browser/webui_browser_window.cc
index 397d7b2..c98a596f 100644
--- a/chrome/browser/ui/webui_browser/webui_browser_window.cc
+++ b/chrome/browser/ui/webui_browser/webui_browser_window.cc
@@ -1092,8 +1092,7 @@
 }
 
 void WebUIBrowserWindow::CreateTabSearchBubble(
-    tab_search::mojom::TabSearchSection section,
-    tab_search::mojom::TabOrganizationFeature organization_feature) {
+    tab_search::mojom::TabSearchSection section) {
   NOTIMPLEMENTED_LOG_ONCE();
 }
 
diff --git a/chrome/browser/ui/webui_browser/webui_browser_window.h b/chrome/browser/ui/webui_browser/webui_browser_window.h
index 86416b2..71da448 100644
--- a/chrome/browser/ui/webui_browser/webui_browser_window.h
+++ b/chrome/browser/ui/webui_browser/webui_browser_window.h
@@ -202,8 +202,8 @@
       content::EyeDropperListener* listener) override;
   void ShowCaretBrowsingDialog() override;
   void CreateTabSearchBubble(
-      tab_search::mojom::TabSearchSection section,
-      tab_search::mojom::TabOrganizationFeature organization_feature) override;
+      tab_search::mojom::TabSearchSection section =
+          tab_search::mojom::TabSearchSection::kSearch) override;
   void CloseTabSearchBubble() override;
   void ShowIncognitoClearBrowsingDataDialog() override;
   void ShowIncognitoHistoryDisclaimerDialog() override;
diff --git a/chrome/browser/upgrade_detector/BUILD.gn b/chrome/browser/upgrade_detector/BUILD.gn
index 0461e0fb..a9b07bf0 100644
--- a/chrome/browser/upgrade_detector/BUILD.gn
+++ b/chrome/browser/upgrade_detector/BUILD.gn
@@ -100,6 +100,7 @@
         "//base:build_time",
         "//chrome/browser:buildflags",
         "//chrome/browser/google",
+        "//chrome/browser/obsolete_system",
         "//content/public/browser",
       ]
     } else {
diff --git a/chrome/browser/usb/web_usb_detector_browsertest.cc b/chrome/browser/usb/web_usb_detector_browsertest.cc
index ee6bfad..2c1d7499 100644
--- a/chrome/browser/usb/web_usb_detector_browsertest.cc
+++ b/chrome/browser/usb/web_usb_detector_browsertest.cc
@@ -62,7 +62,7 @@
 
   void SetUpOnMainThread() override {
     InProcessBrowserTest::SetUpOnMainThread();
-    BrowserList::SetLastActive(browser());
+    ui_test_utils::DeprecatedFakeActivateBrowser(browser());
     display_service_ = std::make_unique<NotificationDisplayServiceTester>(
         /*profile=*/nullptr);
 
diff --git a/chrome/browser/web_applications/BUILD.gn b/chrome/browser/web_applications/BUILD.gn
index c49ef95..a4f818e 100644
--- a/chrome/browser/web_applications/BUILD.gn
+++ b/chrome/browser/web_applications/BUILD.gn
@@ -257,6 +257,8 @@
     "model/localized_text.h",
     "model/migration_behavior.cc",
     "model/migration_behavior.h",
+    "model/migration_source.cc",
+    "model/migration_source.h",
     "model/pending_migration_info.cc",
     "model/pending_migration_info.h",
     "model/web_app_comparison.cc",
diff --git a/chrome/browser/web_applications/commands/apply_manifest_migration_command.cc b/chrome/browser/web_applications/commands/apply_manifest_migration_command.cc
index 611012d..5078ab16 100644
--- a/chrome/browser/web_applications/commands/apply_manifest_migration_command.cc
+++ b/chrome/browser/web_applications/commands/apply_manifest_migration_command.cc
@@ -57,10 +57,9 @@
   const WebApp* destination_app =
       all_apps_lock.registrar().GetAppById(destination_id);
   CHECK(destination_app);
-  for (const auto& migration_sources :
+  for (const auto& migration_source :
        destination_app->validated_migration_sources()) {
-    CHECK(migration_sources.has_manifest_id());
-    if (migration_sources.manifest_id() == source_manifest_id) {
+    if (migration_source.manifest_id() == source_manifest_id) {
       return true;
     }
   }
diff --git a/chrome/browser/web_applications/commands/apply_manifest_migration_command_unittest.cc b/chrome/browser/web_applications/commands/apply_manifest_migration_command_unittest.cc
index 1de547a..675aa9d 100644
--- a/chrome/browser/web_applications/commands/apply_manifest_migration_command_unittest.cc
+++ b/chrome/browser/web_applications/commands/apply_manifest_migration_command_unittest.cc
@@ -131,7 +131,9 @@
       source.set_manifest_id("https://app.source.com/");
       source.set_behavior(
           proto::WebAppMigrationBehavior::WEB_APP_MIGRATION_BEHAVIOR_SUGGEST);
-      info->migration_sources.push_back(std::move(source));
+      info->migration_sources.emplace_back(
+          webapps::ManifestId(GURL("https://app.source.com/")),
+          MigrationBehavior::kSuggest);
     }
 
     fake_provider().scheduler().InstallFromInfoWithParams(
diff --git a/chrome/browser/web_applications/commands/install_app_locally_command_unittest.cc b/chrome/browser/web_applications/commands/install_app_locally_command_unittest.cc
index d55ba7c..af05db0 100644
--- a/chrome/browser/web_applications/commands/install_app_locally_command_unittest.cc
+++ b/chrome/browser/web_applications/commands/install_app_locally_command_unittest.cc
@@ -82,9 +82,9 @@
     info->user_display_mode = mojom::UserDisplayMode::kStandalone;
     info->icon_bitmaps.any = std::move(icon_map);
     if (install_state == proto::InstallState::SUGGESTED_FROM_MIGRATION) {
-      web_app::proto::WebAppMigrationSource source;
-      source.set_manifest_id("https://migration.example.com/start.html");
-      info->migration_sources.push_back(std::move(source));
+      info->migration_sources.emplace_back(
+          webapps::ManifestId(GURL("https://migration.example.com/start.html")),
+          MigrationBehavior::kSuggest);
     }
     base::test::TestFuture<const webapps::AppId&, webapps::InstallResultCode>
         result;
diff --git a/chrome/browser/web_applications/commands/launch_web_app_command_browsertest.cc b/chrome/browser/web_applications/commands/launch_web_app_command_browsertest.cc
index e58fe19..6ff84c0d 100644
--- a/chrome/browser/web_applications/commands/launch_web_app_command_browsertest.cc
+++ b/chrome/browser/web_applications/commands/launch_web_app_command_browsertest.cc
@@ -24,6 +24,7 @@
 #include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
 #include "chrome/browser/ui/web_applications/web_app_browsertest_base.h"
 #include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
+#include "chrome/browser/web_applications/model/migration_behavior.h"
 #include "chrome/browser/web_applications/proto/web_app_install_state.pb.h"
 #include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
 #include "chrome/browser/web_applications/test/web_app_test_utils.h"
@@ -305,9 +306,9 @@
   web_app_info->title = u"Name";
   web_app_info->user_display_mode = mojom::UserDisplayMode::kStandalone;
 
-  web_app::proto::WebAppMigrationSource source;
-  source.set_manifest_id("https://migration.example.com/start.html");
-  web_app_info->migration_sources.push_back(std::move(source));
+  web_app_info->migration_sources.emplace_back(
+      webapps::ManifestId(GURL("https://migration.example.com/start.html")),
+      MigrationBehavior::kSuggest);
 
   // Install & bypass os integration.
   base::test::TestFuture<const webapps::AppId&, webapps::InstallResultCode>
diff --git a/chrome/browser/web_applications/commands/os_integration_synchronize_command_unittest.cc b/chrome/browser/web_applications/commands/os_integration_synchronize_command_unittest.cc
index 2ae6ea7..2441edb 100644
--- a/chrome/browser/web_applications/commands/os_integration_synchronize_command_unittest.cc
+++ b/chrome/browser/web_applications/commands/os_integration_synchronize_command_unittest.cc
@@ -492,9 +492,9 @@
   install_info->user_display_mode =
       web_app::mojom::UserDisplayMode::kStandalone;
 
-  web_app::proto::WebAppMigrationSource source;
-  source.set_manifest_id("https://migration.example.com/start.html");
-  install_info->migration_sources.push_back(std::move(source));
+  install_info->migration_sources.emplace_back(
+      webapps::ManifestId(GURL("https://migration.example.com/start.html")),
+      MigrationBehavior::kSuggest);
 
   WebAppInstallParams params;
   params.install_state = proto::InstallState::SUGGESTED_FROM_MIGRATION;
diff --git a/chrome/browser/web_applications/commands/resolve_web_app_pending_migration_info_command.cc b/chrome/browser/web_applications/commands/resolve_web_app_pending_migration_info_command.cc
index 50ebd07..2707436 100644
--- a/chrome/browser/web_applications/commands/resolve_web_app_pending_migration_info_command.cc
+++ b/chrome/browser/web_applications/commands/resolve_web_app_pending_migration_info_command.cc
@@ -51,13 +51,8 @@
     // If 'app' claims to be migrated from 'source', then 'source' should know
     // about 'app' via PendingMigrationInfo.
     for (const auto& source : app.validated_migration_sources()) {
-      CHECK(source.has_manifest_id());
-      CHECK(source.has_behavior());
-      CHECK(IsValidProtoMigrationBehavior(source.behavior()));
-      PendingMigrationInfo info(app.manifest_id(),
-                                FromProtoMigrationBehavior(source.behavior()));
-      pending_migrations[webapps::ManifestId(source.manifest_id())].push_back(
-          std::move(info));
+      pending_migrations[source.manifest_id()].emplace_back(app.manifest_id(),
+                                                            source.behavior());
     }
   }
 
diff --git a/chrome/browser/web_applications/commands/resolve_web_app_pending_migration_info_command_unittest.cc b/chrome/browser/web_applications/commands/resolve_web_app_pending_migration_info_command_unittest.cc
index 26291de..23298c4 100644
--- a/chrome/browser/web_applications/commands/resolve_web_app_pending_migration_info_command_unittest.cc
+++ b/chrome/browser/web_applications/commands/resolve_web_app_pending_migration_info_command_unittest.cc
@@ -65,13 +65,11 @@
     ScopedRegistryUpdate update =
         provider()->sync_bridge_unsafe().BeginUpdate();
     WebApp* app_target = update->UpdateApp(app_id_target);
-    std::vector<proto::WebAppMigrationSource> sources;
-    proto::WebAppMigrationSource source;
-    source.set_manifest_id(
-        GenerateManifestIdFromStartUrlOnly(GURL(kSourceAppUrl)).spec());
-    source.set_behavior(proto::WEB_APP_MIGRATION_BEHAVIOR_FORCE);
-    sources.push_back(source);
-    app_target->SetValidatedMigrationSources(sources);
+    std::vector<MigrationSource> sources;
+    sources.emplace_back(
+        GenerateManifestIdFromStartUrlOnly(GURL(kSourceAppUrl)),
+        MigrationBehavior::kForce, std::nullopt);
+    app_target->SetValidatedMigrationSources(std::move(sources));
   }
 
   RunCommand();
diff --git a/chrome/browser/web_applications/commands/set_user_display_mode_command_unittest.cc b/chrome/browser/web_applications/commands/set_user_display_mode_command_unittest.cc
index 242966b..2a139eb 100644
--- a/chrome/browser/web_applications/commands/set_user_display_mode_command_unittest.cc
+++ b/chrome/browser/web_applications/commands/set_user_display_mode_command_unittest.cc
@@ -101,9 +101,9 @@
   install_info->user_display_mode =
       web_app::mojom::UserDisplayMode::kStandalone;
 
-  web_app::proto::WebAppMigrationSource source;
-  source.set_manifest_id("https://migration.example.com/start.html");
-  install_info->migration_sources.push_back(std::move(source));
+  install_info->migration_sources.emplace_back(
+      webapps::ManifestId(GURL("https://migration.example.com/start.html")),
+      MigrationBehavior::kSuggest);
 
   WebAppInstallParams params;
   params.install_state = proto::InstallState::SUGGESTED_FROM_MIGRATION;
diff --git a/chrome/browser/web_applications/install_element_browsertest.cc b/chrome/browser/web_applications/install_element_browsertest.cc
index 46a85c1..23435c21f 100644
--- a/chrome/browser/web_applications/install_element_browsertest.cc
+++ b/chrome/browser/web_applications/install_element_browsertest.cc
@@ -665,4 +665,80 @@
   WaitForDismissEvent(kInstallElementId);
 }
 
+///////////////////////////////////////////////////////////////////////////////
+// Regression tests for interactions between <install> element and JS API.
+///////////////////////////////////////////////////////////////////////////////
+
+// Test fixture that enables both the <install> element and the
+// navigator.install() JS API, so we can test interactions between them on the
+// same document (same WebInstallServiceImpl instance).
+class InstallElementAndApiInteractionBrowserTest
+    : public InstallElementBrowserTest {
+ public:
+  InstallElementAndApiInteractionBrowserTest() {
+    additional_features_.InitWithFeatures(
+        {blink::features::kWebAppInstallation}, {});
+  }
+
+ private:
+  base::test::ScopedFeatureList additional_features_;
+};
+
+// Regression test for crbug.com/487568011: triggered_from_element was set by
+// InstallFromElement() but never reset, causing subsequent Install() calls via
+// navigator.install() on the same document to bypass the permissions-policy and
+// permission prompt checks.
+IN_PROC_BROWSER_TEST_F(InstallElementAndApiInteractionBrowserTest,
+                       InstallApiRespectsPermissionsAfterElementInstall) {
+  // Navigate to a page with <install> elements.
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), https_server()->GetURL(kInstallElementPageStartUrl)));
+
+  auto auto_accept = SetAutoAcceptPWAInstallConfirmationForTesting();
+
+  // Set the <install> element's installurl to a another test page.
+  const GURL element_install_url =
+      https_server()->GetURL(kCustomIdPageInstallUrl);
+  ASSERT_TRUE(SetButtonInstallUrl(element_install_url));
+
+  // Step 1: Click the <install> element. This calls InstallFromElement() on the
+  // browser-side WebInstallServiceImpl instance for this document, which passes
+  // triggered_from_element = true.
+  {
+    ui_test_utils::BrowserCreatedObserver browser_created_observer;
+    ASSERT_TRUE(ClickElementWithId(kInstallElementId));
+    Browser* web_app_browser = browser_created_observer.Wait();
+    WaitForPromptActionEvent(kInstallElementId);
+    ASSERT_TRUE(AppBrowserController::IsWebApp(web_app_browser));
+  }
+
+  // Step 2: From the SAME page (same WebInstallServiceImpl instance), call
+  // navigator.install(url, manifest_id) via JS. This uses the Install() Mojo
+  // method, NOT InstallFromElement(). The permission check should NOT be
+  // bypassed by the sticky triggered_from_element_ flag from step 1.
+  const GURL api_install_url =
+      https_server()->GetURL(kNoCustomIdPageInstallUrl);
+  const GURL api_manifest_id = https_server()->GetURL(kNoCustomIdPageId);
+
+  // Block the WEB_APP_INSTALLATION permission so that if the logic to prompt
+  // for permission is correctly reached, it will be denied. With the sticky
+  // state bug, the permission check would've been skipped entirely and the
+  // install would've succeeded.
+  BlockWebInstallPermission(api_install_url);
+
+  auto result = content::EvalJs(
+      web_contents(), "navigator.install('" + api_install_url.spec() + "', '" +
+                          api_manifest_id.spec() +
+                          "')"
+                          ".then(result => 'success')"
+                          ".catch(error => error.name)");
+
+  // The Install API call should fail because the WEB_APP_INSTALLATION
+  // permission was blocked. With the sticky state bug,
+  // triggered_from_element_ would still be true from step 1, causing
+  // Install() to skip all permission checks and auto-grant, resulting in
+  // 'success' instead.
+  EXPECT_EQ("AbortError", result.ExtractString());
+}
+
 }  // namespace web_app
diff --git a/chrome/browser/web_applications/jobs/finalize_install_job.cc b/chrome/browser/web_applications/jobs/finalize_install_job.cc
index 9d47d55..9b303304 100644
--- a/chrome/browser/web_applications/jobs/finalize_install_job.cc
+++ b/chrome/browser/web_applications/jobs/finalize_install_job.cc
@@ -666,7 +666,7 @@
   if (base::FeatureList::IsEnabled(blink::features::kWebAppMigrationApi)) {
     auto old_sources = existing_app
                            ? existing_app->validated_migration_sources()
-                           : std::vector<proto::WebAppMigrationSource>{};
+                           : std::vector<MigrationSource>{};
     if (old_sources != web_app.validated_migration_sources()) {
       provider.scheduler().ScheduleResolveWebAppPendingMigrationInfo(
           base::DoNothing());
diff --git a/chrome/browser/web_applications/jobs/manifest_to_web_app_install_info_job.cc b/chrome/browser/web_applications/jobs/manifest_to_web_app_install_info_job.cc
index e45e2e8..2cda084 100644
--- a/chrome/browser/web_applications/jobs/manifest_to_web_app_install_info_job.cc
+++ b/chrome/browser/web_applications/jobs/manifest_to_web_app_install_info_job.cc
@@ -32,6 +32,7 @@
 #include "chrome/browser/global_features.h"
 #include "chrome/browser/web_applications/icons/trusted_icon_filter.h"
 #include "chrome/browser/web_applications/model/display_override.h"
+#include "chrome/browser/web_applications/model/migration_source.h"
 #include "chrome/browser/web_applications/scope_extension_info.h"
 #include "chrome/browser/web_applications/web_app_constants.h"
 #include "chrome/browser/web_applications/web_app_icon_operations.h"
@@ -464,24 +465,10 @@
   return apps_scope_extensions;
 }
 
-proto::WebAppMigrationSource ToWebAppMigrationSource(
+MigrationSource ToMigrationSource(
     const blink::mojom::ManifestMigrateFrom& migrate_from) {
-  proto::WebAppMigrationSource result;
-  result.set_manifest_id(migrate_from.id.spec());
-  if (migrate_from.install_url && migrate_from.install_url->is_valid()) {
-    result.set_install_url(migrate_from.install_url->spec());
-  }
-  switch (migrate_from.behavior) {
-    case blink::mojom::ManifestMigrationBehavior::kSuggest:
-      result.set_behavior(
-          proto::WebAppMigrationBehavior::WEB_APP_MIGRATION_BEHAVIOR_SUGGEST);
-      break;
-    case blink::mojom::ManifestMigrationBehavior::kForce:
-      result.set_behavior(
-          proto::WebAppMigrationBehavior::WEB_APP_MIGRATION_BEHAVIOR_FORCE);
-      break;
-  }
-  return result;
+  return MigrationSource(migrate_from.id, migrate_from.behavior,
+                         migrate_from.install_url);
 }
 
 base::flat_map<std::string, blink::Manifest::TranslationItem>
@@ -869,7 +856,7 @@
 
   for (const auto& migrate_from : manifest_->migrate_from) {
     install_info().migration_sources.push_back(
-        ToWebAppMigrationSource(*migrate_from));
+        ToMigrationSource(*migrate_from));
   }
 
   if (manifest_->manifest_url.is_valid()) {
diff --git a/chrome/browser/web_applications/model/migration_source.cc b/chrome/browser/web_applications/model/migration_source.cc
new file mode 100644
index 0000000..f16fd78a
--- /dev/null
+++ b/chrome/browser/web_applications/model/migration_source.cc
@@ -0,0 +1,89 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/web_applications/model/migration_source.h"
+
+#include <utility>
+
+#include "base/check.h"
+#include "base/strings/to_string.h"
+#include "base/values.h"
+#include "chrome/browser/web_applications/model/migration_behavior.h"
+#include "chrome/browser/web_applications/proto/web_app.pb.h"
+#include "url/origin.h"
+
+namespace web_app {
+
+MigrationSource::MigrationSource(webapps::ManifestId manifest_id,
+                                 MigrationBehavior behavior,
+                                 std::optional<GURL> install_url)
+    : manifest_id_(std::move(manifest_id)),
+      behavior_(behavior),
+      install_url_(std::move(install_url)) {
+  CHECK(manifest_id_.is_valid());
+  CHECK(!url::Origin::Create(manifest_id_).opaque());
+  if (install_url_.has_value()) {
+    CHECK(install_url_->is_valid());
+    CHECK(url::IsSameOriginWith(manifest_id_, *install_url_));
+  }
+}
+
+MigrationSource::MigrationSource(const MigrationSource&) = default;
+MigrationSource& MigrationSource::operator=(const MigrationSource&) = default;
+MigrationSource::~MigrationSource() = default;
+
+std::optional<MigrationSource> MigrationSource::ParseAndCreate(
+    const proto::WebAppMigrationSource& proto) {
+  // Exit early if either field is missing, as both fields are required for a
+  // valid `MigrationSource`.
+  if (!proto.has_manifest_id() || !proto.has_behavior()) {
+    return std::nullopt;
+  }
+
+  if (!IsValidProtoMigrationBehavior(proto.behavior())) {
+    return std::nullopt;
+  }
+
+  // The `manifest_id` for the destination app should be valid, otherwise this
+  // is an incorrect state for the app to exist as.
+  webapps::ManifestId manifest_id(proto.manifest_id());
+  if (!manifest_id.is_valid() || url::Origin::Create(manifest_id).opaque()) {
+    return std::nullopt;
+  }
+
+  std::optional<GURL> install_url;
+  if (proto.has_install_url()) {
+    install_url = GURL(proto.install_url());
+    if (!install_url->is_valid() ||
+        !url::IsSameOriginWith(manifest_id, *install_url)) {
+      return std::nullopt;
+    }
+  }
+
+  return MigrationSource(std::move(manifest_id),
+                         FromProtoMigrationBehavior(proto.behavior()),
+                         std::move(install_url));
+}
+
+proto::WebAppMigrationSource MigrationSource::ToProto() const {
+  proto::WebAppMigrationSource proto;
+  proto.set_manifest_id(manifest_id_.spec());
+  proto.set_behavior(ToProtoMigrationBehavior(behavior_));
+  if (install_url_.has_value()) {
+    proto.set_install_url(install_url_->spec());
+  }
+  return proto;
+}
+
+base::Value MigrationSource::AsDebugValue() const {
+  base::DictValue root;
+  root.Set("manifest_id", manifest_id_.possibly_invalid_spec());
+  root.Set("behavior", base::ToString(behavior_));
+  if (install_url_.has_value()) {
+    root.Set("install_url", install_url_->possibly_invalid_spec());
+  }
+  return base::Value(std::move(root));
+}
+
+}  // namespace web_app
diff --git a/chrome/browser/web_applications/model/migration_source.h b/chrome/browser/web_applications/model/migration_source.h
new file mode 100644
index 0000000..10586a5
--- /dev/null
+++ b/chrome/browser/web_applications/model/migration_source.h
@@ -0,0 +1,60 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_WEB_APPLICATIONS_MODEL_MIGRATION_SOURCE_H_
+#define CHROME_BROWSER_WEB_APPLICATIONS_MODEL_MIGRATION_SOURCE_H_
+
+#include <optional>
+
+#include "base/values.h"
+#include "chrome/browser/web_applications/model/migration_behavior.h"
+#include "components/webapps/common/web_app_id.h"
+#include "url/gurl.h"
+
+namespace web_app {
+
+namespace proto {
+class WebAppMigrationSource;
+}  // namespace proto
+
+// Represents a migration source specified in the 'migration_from' field of a
+// web app manifest.
+// This class abstracts the behavior of proto::WebAppMigrationSource to avoid
+// including heavy Protobuf headers in other files, leading to faster
+// compilation times.
+class MigrationSource {
+ public:
+  MigrationSource(webapps::ManifestId manifest_id,
+                  MigrationBehavior behavior,
+                  std::optional<GURL> install_url = std::nullopt);
+  MigrationSource(const MigrationSource&);
+  MigrationSource& operator=(const MigrationSource&);
+  ~MigrationSource();
+
+  bool operator==(const MigrationSource&) const = default;
+
+  // Provides a single point of validation when loading migration data from the
+  // database. This ensures that the in-memory model only ever contains
+  // well-formed migration sources.
+  static std::optional<MigrationSource> ParseAndCreate(
+      const proto::WebAppMigrationSource& proto);
+
+  // Serializes this MigrationSource to a profo::WebAppMigrationSource.
+  proto::WebAppMigrationSource ToProto() const;
+
+  base::Value AsDebugValue() const;
+
+  const webapps::ManifestId& manifest_id() const { return manifest_id_; }
+  MigrationBehavior behavior() const { return behavior_; }
+  const std::optional<GURL>& install_url() const { return install_url_; }
+
+ private:
+  webapps::ManifestId manifest_id_;
+  MigrationBehavior behavior_;
+  std::optional<GURL> install_url_;
+};
+
+}  // namespace web_app
+
+#endif  // CHROME_BROWSER_WEB_APPLICATIONS_MODEL_MIGRATION_SOURCE_H_
diff --git a/chrome/browser/web_applications/replace_migration_suggested_app_browsertest.cc b/chrome/browser/web_applications/replace_migration_suggested_app_browsertest.cc
index ceef038..5c5e4d4 100644
--- a/chrome/browser/web_applications/replace_migration_suggested_app_browsertest.cc
+++ b/chrome/browser/web_applications/replace_migration_suggested_app_browsertest.cc
@@ -9,6 +9,7 @@
 #include "chrome/browser/ui/web_applications/web_app_browsertest_base.h"
 #include "chrome/browser/web_applications/external_install_options.h"
 #include "chrome/browser/web_applications/externally_managed_app_manager.h"
+#include "chrome/browser/web_applications/model/migration_behavior.h"
 #include "chrome/browser/web_applications/proto/web_app_install_state.pb.h"
 #include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
 #include "chrome/browser/web_applications/test/web_app_test_utils.h"
@@ -60,9 +61,9 @@
     web_app_info->title = u"Test App";
     web_app_info->user_display_mode = mojom::UserDisplayMode::kStandalone;
 
-    web_app::proto::WebAppMigrationSource source;
-    source.set_manifest_id(start_url.GetWithoutFilename().spec());
-    web_app_info->migration_sources.push_back(std::move(source));
+    web_app_info->migration_sources.emplace_back(
+        webapps::ManifestId(GURL(start_url.GetWithoutFilename().spec())),
+        MigrationBehavior::kSuggest);
 
     base::test::TestFuture<const webapps::AppId&, webapps::InstallResultCode>
         install_future;
diff --git a/chrome/browser/web_applications/test/web_app_test_utils.cc b/chrome/browser/web_applications/test/web_app_test_utils.cc
index a7de671..4b4d41e4f 100644
--- a/chrome/browser/web_applications/test/web_app_test_utils.cc
+++ b/chrome/browser/web_applications/test/web_app_test_utils.cc
@@ -55,6 +55,7 @@
 #include "chrome/browser/web_applications/model/app_installed_by.h"
 #include "chrome/browser/web_applications/model/display_override.h"
 #include "chrome/browser/web_applications/model/migration_behavior.h"
+#include "chrome/browser/web_applications/model/migration_source.h"
 #include "chrome/browser/web_applications/model/pending_migration_info.h"
 #include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
 #include "chrome/browser/web_applications/proto/web_app.pb.h"
@@ -663,22 +664,21 @@
   return icons;
 }
 
-std::vector<proto::WebAppMigrationSource> CreateRandomMigrationSources(
+std::vector<MigrationSource> CreateRandomMigrationSources(
     RandomHelper& random) {
-  std::vector<proto::WebAppMigrationSource> sources;
+  std::vector<MigrationSource> sources;
   int num_sources = random.next_uint(3);
   for (int i = 0; i < num_sources; ++i) {
-    proto::WebAppMigrationSource source;
-    source.set_manifest_id("https://example.com/manifest_id_" +
-                           base::NumberToString(random.next_uint()));
-    source.set_behavior(random.next_bool()
-                            ? proto::WEB_APP_MIGRATION_BEHAVIOR_FORCE
-                            : proto::WEB_APP_MIGRATION_BEHAVIOR_SUGGEST);
+    GURL manifest_id("https://example.com/manifest_id_" +
+                     base::NumberToString(random.next_uint()));
+    MigrationBehavior behavior = random.next_enum<MigrationBehavior>();
+    std::optional<GURL> install_url;
     if (random.next_bool()) {
-      source.set_install_url("https://example.com/install_url_" +
-                             base::NumberToString(random.next_uint()));
+      install_url = GURL("https://example.com/install_url_" +
+                         base::NumberToString(random.next_uint()));
     }
-    sources.push_back(std::move(source));
+    sources.emplace_back(std::move(manifest_id), behavior,
+                         std::move(install_url));
   }
   return sources;
 }
@@ -1334,12 +1334,11 @@
   }
 
   app->SetUnvalidatedMigrationSources(CreateRandomMigrationSources(random));
-  std::vector<proto::WebAppMigrationSource> validated_sources;
-  std::ranges::copy_if(app->unvalidated_migration_sources(),
-                       std::back_inserter(validated_sources),
-                       [&random](const proto::WebAppMigrationSource&) {
-                         return random.next_bool();
-                       });
+  std::vector<MigrationSource> validated_sources;
+  std::ranges::copy_if(
+      app->unvalidated_migration_sources(),
+      std::back_inserter(validated_sources),
+      [&random](const MigrationSource&) { return random.next_bool(); });
   app->SetValidatedMigrationSources(std::move(validated_sources));
   app->SetPendingMigrationInfo(CreateRandomPendingMigrationInfos(random));
 
diff --git a/chrome/browser/web_applications/web_app.cc b/chrome/browser/web_applications/web_app.cc
index 4f6621f..23e212c 100644
--- a/chrome/browser/web_applications/web_app.cc
+++ b/chrome/browser/web_applications/web_app.cc
@@ -893,31 +893,13 @@
   }
 }
 
-namespace {
-void ValidateMigrationSources(
-    const std::vector<proto::WebAppMigrationSource>& sources) {
-  for (const auto& source : sources) {
-    GURL manifest_id(source.manifest_id());
-    CHECK(manifest_id.is_valid());
-    CHECK(!url::Origin::Create(manifest_id).opaque());
-    if (source.has_install_url()) {
-      GURL install_url(source.install_url());
-      CHECK(install_url.is_valid());
-      CHECK(url::IsSameOriginWith(manifest_id, install_url));
-    }
-  }
-}
-}  // namespace
-
 void WebApp::SetUnvalidatedMigrationSources(
-    std::vector<proto::WebAppMigrationSource> sources) {
-  ValidateMigrationSources(sources);
+    std::vector<MigrationSource> sources) {
   unvalidated_migration_sources_ = std::move(sources);
 }
 
 void WebApp::SetValidatedMigrationSources(
-    std::vector<proto::WebAppMigrationSource> sources) {
-  ValidateMigrationSources(sources);
+    std::vector<MigrationSource> sources) {
   validated_migration_sources_ = std::move(sources);
 }
 
@@ -1416,15 +1398,9 @@
   root.Set("installed_by", std::move(installed_by_list));
 
   root.Set("unvalidated_migration_sources",
-           base::ToValueList(unvalidated_migration_sources_,
-                             [](const proto::WebAppMigrationSource& source) {
-                               return proto::ToValue(source);
-                             }));
+           ConvertDebugValueList(unvalidated_migration_sources_));
   root.Set("validated_migration_sources",
-           base::ToValueList(validated_migration_sources_,
-                             [](const proto::WebAppMigrationSource& source) {
-                               return proto::ToValue(source);
-                             }));
+           ConvertDebugValueList(validated_migration_sources_));
   root.Set("pending_migration_info",
            OptionalAsDebugValue(pending_migration_info_));
 
diff --git a/chrome/browser/web_applications/web_app.h b/chrome/browser/web_applications/web_app.h
index 414e874..56642a9 100644
--- a/chrome/browser/web_applications/web_app.h
+++ b/chrome/browser/web_applications/web_app.h
@@ -25,6 +25,7 @@
 #include "chrome/browser/web_applications/isolated_web_apps/isolation_data.h"
 #include "chrome/browser/web_applications/model/app_installed_by.h"
 #include "chrome/browser/web_applications/model/display_override.h"
+#include "chrome/browser/web_applications/model/migration_source.h"
 #include "chrome/browser/web_applications/model/pending_migration_info.h"
 #include "chrome/browser/web_applications/mojom/user_display_mode.mojom-forward.h"
 #include "chrome/browser/web_applications/proto/web_app.pb.h"
@@ -572,22 +573,18 @@
 
   void SetStoredTrustedIconSizes(IconPurpose purpose, SortedSizesPx sizes);
 
-  const std::vector<proto::WebAppMigrationSource>&
-  unvalidated_migration_sources() const {
+  const std::vector<MigrationSource>& unvalidated_migration_sources() const {
     return unvalidated_migration_sources_;
   }
-  const std::vector<proto::WebAppMigrationSource>& validated_migration_sources()
-      const {
+  const std::vector<MigrationSource>& validated_migration_sources() const {
     return validated_migration_sources_;
   }
   const std::optional<PendingMigrationInfo>& pending_migration_info() const {
     return pending_migration_info_;
   }
 
-  void SetUnvalidatedMigrationSources(
-      std::vector<proto::WebAppMigrationSource> sources);
-  void SetValidatedMigrationSources(
-      std::vector<proto::WebAppMigrationSource> sources);
+  void SetUnvalidatedMigrationSources(std::vector<MigrationSource> sources);
+  void SetValidatedMigrationSources(std::vector<MigrationSource> sources);
   void SetPendingMigrationInfo(std::optional<PendingMigrationInfo> info);
 
   void SetInstalledBy(InstalledByPassKey,
@@ -750,8 +747,8 @@
 
   std::deque<AppInstalledBy> installed_by_;
 
-  std::vector<proto::WebAppMigrationSource> unvalidated_migration_sources_;
-  std::vector<proto::WebAppMigrationSource> validated_migration_sources_;
+  std::vector<MigrationSource> unvalidated_migration_sources_;
+  std::vector<MigrationSource> validated_migration_sources_;
   std::optional<PendingMigrationInfo> pending_migration_info_;
   // LINT.ThenChange(//chrome/browser/web_applications/proto/web_app.proto)
 
diff --git a/chrome/browser/web_applications/web_app_database_serialization.cc b/chrome/browser/web_applications/web_app_database_serialization.cc
index da50e6c..1dda132 100644
--- a/chrome/browser/web_applications/web_app_database_serialization.cc
+++ b/chrome/browser/web_applications/web_app_database_serialization.cc
@@ -1480,48 +1480,30 @@
   }
   web_app->SetInstalledBy(InstalledByPassKey(), std::move(installed_by_data));
 
-  auto is_valid_migration_source =
-      [](const proto::WebAppMigrationSource& source) {
-        if (!source.has_manifest_id() || !source.has_behavior()) {
-          return false;
-        }
-        GURL manifest_id(source.manifest_id());
-        if (!manifest_id.is_valid() ||
-            url::Origin::Create(manifest_id).opaque()) {
-          return false;
-        }
-        if (source.has_install_url()) {
-          GURL install_url(source.install_url());
-          if (!install_url.is_valid() ||
-              !url::IsSameOriginWith(manifest_id, install_url)) {
-            return false;
-          }
-        }
-        return true;
-      };
-
-  std::vector<proto::WebAppMigrationSource> unvalidated_migration_sources;
+  std::vector<MigrationSource> unvalidated_migration_sources;
   for (const auto& source_proto : proto.unvalidated_migration_sources()) {
-    if (!is_valid_migration_source(source_proto)) {
+    std::optional<MigrationSource> source =
+        MigrationSource::ParseAndCreate(source_proto);
+    if (!source) {
       RecordProtoParseResult(
           ProtoParseResult::kInvalidWebAppUnvalidatedMigrationSource);
-      DLOG(ERROR) << "WebApp proto Unvalidated MigrationSource parse error";
       return nullptr;
     }
-    unvalidated_migration_sources.push_back(source_proto);
+    unvalidated_migration_sources.push_back(std::move(*source));
   }
   web_app->SetUnvalidatedMigrationSources(
       std::move(unvalidated_migration_sources));
 
-  std::vector<proto::WebAppMigrationSource> validated_migration_sources;
+  std::vector<MigrationSource> validated_migration_sources;
   for (const auto& source_proto : proto.validated_migration_sources()) {
-    if (!is_valid_migration_source(source_proto)) {
+    std::optional<MigrationSource> source =
+        MigrationSource::ParseAndCreate(source_proto);
+    if (!source) {
       RecordProtoParseResult(
           ProtoParseResult::kInvalidWebAppValidatedMigrationSource);
-      DLOG(ERROR) << "WebApp proto Validated MigrationSource parse error";
       return nullptr;
     }
-    validated_migration_sources.push_back(source_proto);
+    validated_migration_sources.push_back(std::move(*source));
   }
   web_app->SetValidatedMigrationSources(std::move(validated_migration_sources));
 
@@ -2072,11 +2054,11 @@
   }
 
   for (const auto& source : web_app.unvalidated_migration_sources()) {
-    *local_data->add_unvalidated_migration_sources() = source;
+    *local_data->add_unvalidated_migration_sources() = source.ToProto();
   }
 
   for (const auto& source : web_app.validated_migration_sources()) {
-    *local_data->add_validated_migration_sources() = source;
+    *local_data->add_validated_migration_sources() = source.ToProto();
   }
 
   if (web_app.pending_migration_info().has_value()) {
diff --git a/chrome/browser/web_applications/web_app_install_finalizer_unittest.cc b/chrome/browser/web_applications/web_app_install_finalizer_unittest.cc
index db81bb2..9f9ac970 100644
--- a/chrome/browser/web_applications/web_app_install_finalizer_unittest.cc
+++ b/chrome/browser/web_applications/web_app_install_finalizer_unittest.cc
@@ -654,11 +654,9 @@
   info->title = u"Foo Title";
   FinalizeJobOptions options(webapps::WebappInstallSource::INTERNAL_DEFAULT);
 
-  proto::WebAppMigrationSource source;
-  source.set_manifest_id("https://migration.foo.example/");
-  source.set_behavior(
-      proto::WebAppMigrationBehavior::WEB_APP_MIGRATION_BEHAVIOR_SUGGEST);
-  info->migration_sources = {source};
+  info->migration_sources = {MigrationSource(
+      webapps::ManifestId(GURL("https://migration.foo.example/")),
+      MigrationBehavior::kSuggest)};
 
   // Set data such that migration source will be returned in validated data.
   static_cast<FakeWebAppOriginAssociationManager&>(
@@ -673,14 +671,14 @@
 
   EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
   const WebApp* installed_app = registrar().GetAppById(result.installed_app_id);
-  EXPECT_THAT(installed_app->unvalidated_migration_sources(),
-              testing::ElementsAre(
-                  testing::Property(&proto::WebAppMigrationSource::manifest_id,
-                                    "https://migration.foo.example/")));
-  EXPECT_THAT(installed_app->validated_migration_sources(),
-              testing::ElementsAre(
-                  testing::Property(&proto::WebAppMigrationSource::manifest_id,
-                                    "https://migration.foo.example/")));
+  EXPECT_THAT(
+      installed_app->unvalidated_migration_sources(),
+      testing::ElementsAre(testing::Property(
+          &MigrationSource::manifest_id, "https://migration.foo.example/")));
+  EXPECT_THAT(
+      installed_app->validated_migration_sources(),
+      testing::ElementsAre(testing::Property(
+          &MigrationSource::manifest_id, "https://migration.foo.example/")));
 }
 
 TEST_F(WebAppInstallFinalizerUnitTest,
@@ -694,11 +692,9 @@
   options.add_to_desktop = false;
   options.add_to_quick_launch_bar = false;
 
-  proto::WebAppMigrationSource source;
-  source.set_manifest_id("https://migration.foo.example/");
-  source.set_behavior(
-      proto::WebAppMigrationBehavior::WEB_APP_MIGRATION_BEHAVIOR_SUGGEST);
-  info->migration_sources = {source};
+  info->migration_sources = {MigrationSource(
+      webapps::ManifestId(GURL("https://migration.foo.example/")),
+      MigrationBehavior::kSuggest)};
 
   // Set data such that migration source will NOT be returned in validated data.
   static_cast<FakeWebAppOriginAssociationManager&>(
@@ -711,10 +707,10 @@
   const WebApp* installed_app = registrar().GetAppById(result.installed_app_id);
   EXPECT_EQ(proto::InstallState::SUGGESTED_FROM_MIGRATION,
             installed_app->install_state());
-  EXPECT_THAT(installed_app->unvalidated_migration_sources(),
-              testing::ElementsAre(
-                  testing::Property(&proto::WebAppMigrationSource::manifest_id,
-                                    "https://migration.foo.example/")));
+  EXPECT_THAT(
+      installed_app->unvalidated_migration_sources(),
+      testing::ElementsAre(testing::Property(
+          &MigrationSource::manifest_id, "https://migration.foo.example/")));
   EXPECT_TRUE(installed_app->validated_migration_sources().empty());
 }
 
@@ -755,11 +751,9 @@
       .WillOnce(base::test::RunOnceClosure<0>());
 
   // 3. Finalize update with migration sources.
-  proto::WebAppMigrationSource source;
-  source.set_manifest_id("https://migration.foo.example/");
-  source.set_behavior(
-      proto::WebAppMigrationBehavior::WEB_APP_MIGRATION_BEHAVIOR_SUGGEST);
-  info->migration_sources = {source};
+  info->migration_sources = {MigrationSource(
+      webapps::ManifestId(GURL("https://migration.foo.example/")),
+      MigrationBehavior::kSuggest)};
 
   base::test::TestFuture<const webapps::AppId&, webapps::InstallResultCode>
       update_future;
diff --git a/chrome/browser/web_applications/web_app_install_info.h b/chrome/browser/web_applications/web_app_install_info.h
index df92d74..7281f5ec 100644
--- a/chrome/browser/web_applications/web_app_install_info.h
+++ b/chrome/browser/web_applications/web_app_install_info.h
@@ -22,6 +22,7 @@
 #include "build/build_config.h"
 #include "chrome/browser/web_applications/model/display_override.h"
 #include "chrome/browser/web_applications/model/localized_text.h"
+#include "chrome/browser/web_applications/model/migration_source.h"
 #include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
 #include "chrome/browser/web_applications/proto/web_app.pb.h"
 #include "chrome/browser/web_applications/scope_extension_info.h"
@@ -508,7 +509,7 @@
   std::optional<GURL> iwa_update_manifest_url;
 
   // Migration sources to be associated with the app.
-  std::vector<proto::WebAppMigrationSource> migration_sources;
+  std::vector<MigrationSource> migration_sources;
 
  private:
   // Used this method in Clone() method. Use Clone() to deep copy explicitly.
diff --git a/chrome/browser/web_applications/web_app_origin_association_manager.h b/chrome/browser/web_applications/web_app_origin_association_manager.h
index 79311ca..4c8beed 100644
--- a/chrome/browser/web_applications/web_app_origin_association_manager.h
+++ b/chrome/browser/web_applications/web_app_origin_association_manager.h
@@ -11,6 +11,7 @@
 
 #include "base/gtest_prod_util.h"
 #include "base/memory/weak_ptr.h"
+#include "chrome/browser/web_applications/model/migration_source.h"
 #include "chrome/browser/web_applications/proto/web_app.pb.h"
 #include "chrome/browser/web_applications/scope_extension_info.h"
 #include "components/webapps/services/web_app_origin_association/web_app_origin_association_fetcher.h"
@@ -32,7 +33,7 @@
   bool operator==(const OriginAssociations&) const;
 
   ScopeExtensions scope_extensions;
-  std::vector<web_app::proto::WebAppMigrationSource> migration_sources;
+  std::vector<MigrationSource> migration_sources;
 };
 
 // Callback type that sends back the valid |origin_associations|.
diff --git a/chrome/browser/web_applications/web_app_origin_association_manager_browsertest.cc b/chrome/browser/web_applications/web_app_origin_association_manager_browsertest.cc
index b7961e4..7071a19 100644
--- a/chrome/browser/web_applications/web_app_origin_association_manager_browsertest.cc
+++ b/chrome/browser/web_applications/web_app_origin_association_manager_browsertest.cc
@@ -215,11 +215,8 @@
                        InvalidMigrationSource) {
   base::test::TestFuture<OriginAssociations> future;
   OriginAssociations origin_associations;
-  web_app::proto::WebAppMigrationSource migration_source;
-  migration_source.set_manifest_id(kInvalidFileUrl);
-  migration_source.set_behavior(
-      web_app::proto::WEB_APP_MIGRATION_BEHAVIOR_SUGGEST);
-  origin_associations.migration_sources.push_back(std::move(migration_source));
+  origin_associations.migration_sources.emplace_back(
+      GURL(kInvalidFileUrl), MigrationBehavior::kSuggest);
 
   manager_->GetWebAppOriginAssociations(GURL(kWebAppIdentity),
                                         std::move(origin_associations),
@@ -235,12 +232,9 @@
   {
     base::test::TestFuture<OriginAssociations> future;
     OriginAssociations origin_associations;
-    web_app::proto::WebAppMigrationSource migration_source;
-    migration_source.set_manifest_id(kAppWithMultipleMigrationCasesUrl);
-    migration_source.set_behavior(
-        web_app::proto::WEB_APP_MIGRATION_BEHAVIOR_SUGGEST);
-    origin_associations.migration_sources.push_back(
-        std::move(migration_source));
+    origin_associations.migration_sources.emplace_back(
+        webapps::ManifestId(GURL(kAppWithMultipleMigrationCasesUrl)),
+        MigrationBehavior::kSuggest);
     manager_->GetWebAppOriginAssociations(
         GURL("https://foo.com/index_no_migration"),
         std::move(origin_associations), future.GetCallback());
@@ -252,12 +246,9 @@
   {
     base::test::TestFuture<OriginAssociations> future;
     OriginAssociations origin_associations;
-    web_app::proto::WebAppMigrationSource migration_source;
-    migration_source.set_manifest_id(kAppWithMultipleMigrationCasesUrl);
-    migration_source.set_behavior(
-        web_app::proto::WEB_APP_MIGRATION_BEHAVIOR_SUGGEST);
-    origin_associations.migration_sources.push_back(
-        std::move(migration_source));
+    origin_associations.migration_sources.emplace_back(
+        webapps::ManifestId(GURL(kAppWithMultipleMigrationCasesUrl)),
+        MigrationBehavior::kSuggest);
     manager_->GetWebAppOriginAssociations(
         GURL("https://foo.com/index_migration_true"),
         std::move(origin_associations), future.GetCallback());
@@ -275,8 +266,9 @@
     migration_source.set_manifest_id(kAppWithMultipleMigrationCasesUrl);
     migration_source.set_behavior(
         web_app::proto::WEB_APP_MIGRATION_BEHAVIOR_SUGGEST);
-    origin_associations.migration_sources.push_back(
-        std::move(migration_source));
+    origin_associations.migration_sources.emplace_back(
+        webapps::ManifestId(GURL(kAppWithMultipleMigrationCasesUrl)),
+        MigrationBehavior::kSuggest);
     manager_->GetWebAppOriginAssociations(
         GURL("https://foo.com/index_migration_false"),
         std::move(origin_associations), future.GetCallback());
@@ -295,7 +287,9 @@
   migration_source.set_manifest_id(same_origin_manifest_id);
   migration_source.set_behavior(
       web_app::proto::WEB_APP_MIGRATION_BEHAVIOR_SUGGEST);
-  origin_associations.migration_sources.push_back(std::move(migration_source));
+  origin_associations.migration_sources.emplace_back(
+      webapps::ManifestId(GURL(same_origin_manifest_id)),
+      MigrationBehavior::kSuggest);
 
   // The fetcher's data does NOT include any entry for foo.com.
   // GetWebAppOriginAssociations should allow this without fetching.
diff --git a/chrome/browser/web_applications/web_app_origin_association_task.cc b/chrome/browser/web_applications/web_app_origin_association_task.cc
index 0d3cee7..99ee0d0a6 100644
--- a/chrome/browser/web_applications/web_app_origin_association_task.cc
+++ b/chrome/browser/web_applications/web_app_origin_association_task.cc
@@ -37,8 +37,7 @@
       unique_origins.insert(scope_extension.origin);
     }
   }
-  for (const proto::WebAppMigrationSource& migration_source :
-       migration_sources_input_) {
+  for (const MigrationSource& migration_source : migration_sources_input_) {
     url::Origin origin =
         url::Origin::Create(GURL(migration_source.manifest_id()));
     CHECK(!origin.opaque());
@@ -120,10 +119,9 @@
     result_.scope_extensions.insert(scope_extension);
   }
 
-  for (const proto::WebAppMigrationSource& migration_source :
-       migration_sources_input_) {
+  for (const MigrationSource& migration_source : migration_sources_input_) {
     url::Origin origin_to_check =
-        url::Origin::Create(GURL(migration_source.manifest_id()));
+        url::Origin::Create(migration_source.manifest_id());
     if (origin_to_check.opaque()) {
       continue;
     }
diff --git a/chrome/browser/web_applications/web_app_origin_association_task.h b/chrome/browser/web_applications/web_app_origin_association_task.h
index 6813e863..85d56684 100644
--- a/chrome/browser/web_applications/web_app_origin_association_task.h
+++ b/chrome/browser/web_applications/web_app_origin_association_task.h
@@ -13,6 +13,7 @@
 
 #include "base/memory/raw_ref.h"
 #include "base/memory/weak_ptr.h"
+#include "chrome/browser/web_applications/model/migration_source.h"
 #include "chrome/browser/web_applications/web_app_origin_association_manager.h"
 #include "components/webapps/services/web_app_origin_association/web_app_origin_association_parser.h"
 
@@ -48,7 +49,7 @@
   std::deque<url::Origin> pending_origins_;
   std::map<url::Origin, webapps::AssociatedWebApp> fetched_associations_;
   ScopeExtensions scope_extensions_input_;
-  std::vector<web_app::proto::WebAppMigrationSource> migration_sources_input_;
+  std::vector<MigrationSource> migration_sources_input_;
   // The manager that owns this task.
   const raw_ref<WebAppOriginAssociationManager> owner_;
   // Callback to send the result back.
diff --git a/chrome/browser/web_applications/web_install_service_impl.cc b/chrome/browser/web_applications/web_install_service_impl.cc
index 3ef74d6c..faadccb 100644
--- a/chrome/browser/web_applications/web_install_service_impl.cc
+++ b/chrome/browser/web_applications/web_install_service_impl.cc
@@ -190,6 +190,21 @@
 
 void WebInstallServiceImpl::Install(blink::mojom::InstallOptionsPtr options,
                                     InstallCallback callback) {
+  InstallInternal(std::move(options), std::move(callback),
+                  /*triggered_from_element=*/false);
+}
+
+void WebInstallServiceImpl::InstallFromElement(
+    blink::mojom::InstallOptionsPtr options,
+    InstallCallback callback) {
+  InstallInternal(std::move(options), std::move(callback),
+                  /*triggered_from_element=*/true);
+}
+
+void WebInstallServiceImpl::InstallInternal(
+    blink::mojom::InstallOptionsPtr options,
+    InstallCallback callback,
+    bool triggered_from_element) {
   // Create source ids for UKM logging.
   ukm::SourceId requesting_page_source_id =
       options ? render_frame_host().GetPageUkmSourceId()
@@ -251,18 +266,18 @@
         }
         std::move(callback).Run(install_result, manifest_id_result);
       },
-      std::move(callback), triggered_from_element_, requesting_page_source_id,
+      std::move(callback), triggered_from_element, requesting_page_source_id,
       installed_app_source_id);
 
   web_app::WebInstallServiceType install_type =
       options ? web_app::WebInstallServiceType::kBackgroundDocument
               : web_app::WebInstallServiceType::kCurrentDocument;
   base::UmaHistogramEnumeration(
-      triggered_from_element_ ? kInstallElementTypeUma : kInstallApiTypeUma,
+      triggered_from_element ? kInstallElementTypeUma : kInstallApiTypeUma,
       install_type);
   base::UmaHistogramEnumeration(
       base::StrCat({"WebApp.WebInstallService.",
-                    triggered_from_element_ ? "Element" : "Api",
+                    triggered_from_element ? "Element" : "Api",
                     ".InstallType"}),
       install_type);
 
@@ -314,10 +329,10 @@
 
   // Skip requesting permission in two cases:
   // 1. The install URL matches the current document URL (user is installing
-  //    the page they're on, even if using background install syntax).
-  // 2. Install triggered from the <install> element (permission is handled
-  //    by the element itself). In both cases, the install dialog is shown.
-  if (triggered_from_element_ || install_target == last_committed_url_) {
+  //    the page they're currently on, just using background install syntax).
+  // 2. Install triggered from the <install> element.
+  // In both cases, the install dialog is always shown.
+  if (triggered_from_element || install_target == last_committed_url_) {
     OnPermissionDecided(
         std::move(callback_with_metrics),
         std::vector<content::PermissionResult>({content::PermissionResult(
@@ -341,13 +356,6 @@
       weak_ptr_factory_.GetWeakPtr(), std::move(callback_with_metrics)));
 }
 
-void WebInstallServiceImpl::InstallFromElement(
-    blink::mojom::InstallOptionsPtr options,
-    InstallCallback callback) {
-  triggered_from_element_ = true;
-  Install(std::move(options), std::move(callback));
-}
-
 void WebInstallServiceImpl::OnInstallNotSupportedDialogClosed(
     InstallCallbackWithMetrics callback_with_metrics) {
   std::move(callback_with_metrics)
diff --git a/chrome/browser/web_applications/web_install_service_impl.h b/chrome/browser/web_applications/web_install_service_impl.h
index 6164059..b8325df 100644
--- a/chrome/browser/web_applications/web_install_service_impl.h
+++ b/chrome/browser/web_applications/web_install_service_impl.h
@@ -98,6 +98,13 @@
                           InstallCallback callback) override;
 
  private:
+  // Shared implementation for Install() and InstallFromElement().
+  // `triggered_from_element` controls metrics routing and whether the
+  // permission prompt is bypassed (the <install> element handles permission
+  // via its own UI).
+  void InstallInternal(blink::mojom::InstallOptionsPtr options,
+                       InstallCallback callback,
+                       bool triggered_from_element);
   WebInstallServiceImpl(
       content::RenderFrameHost& render_frame_host,
       mojo::PendingReceiver<blink::mojom::WebInstallService> receiver);
@@ -169,8 +176,6 @@
   blink::mojom::InstallOptionsPtr install_options_;
   const content::GlobalRenderFrameHostId frame_routing_id_;
   GURL last_committed_url_;
-  // True if install was triggered from <install> element rather than JS API.
-  bool triggered_from_element_ = false;
   // Active data retrievers. They are destroyed when this service is destroyed
   // or when their callback completes.
   absl::flat_hash_set<std::unique_ptr<WebAppDataRetriever>> data_retrievers_;
diff --git a/chrome/browser/win/isolated_browser_support.cc b/chrome/browser/win/isolated_browser_support.cc
index 5a72514..5005fc7 100644
--- a/chrome/browser/win/isolated_browser_support.cc
+++ b/chrome/browser/win/isolated_browser_support.cc
@@ -9,7 +9,6 @@
 #include <windows.h>
 
 #include "base/command_line.h"
-#include "base/logging.h"
 #include "base/memory/ptr_util.h"
 #include "base/process/process.h"
 #include "base/strings/strcat.h"
@@ -30,13 +29,16 @@
 
 IsolatedBrowser::~IsolatedBrowser() = default;
 
+IsolatedBrowser::IsolatedBrowser(IsolatedBrowser&& other) = default;
+IsolatedBrowser& IsolatedBrowser::operator=(IsolatedBrowser&& other) = default;
+
 IsolatedBrowser::IsolatedBrowser(base::Process process,
                                  base::win::ScopedHandle job)
     : job_(std::move(job)), process_(std::move(process)) {}
 
 // static
-base::expected<std::unique_ptr<IsolatedBrowser>, HRESULT>
-IsolatedBrowser::Launch(const base::CommandLine& command_line) {
+base::expected<IsolatedBrowser, HRESULT> IsolatedBrowser::Launch(
+    const base::CommandLine& command_line) {
   base::win::ScopedCOMInitializer com_init;
 
   base::win::AssertComInitialized();
@@ -47,7 +49,6 @@
       install_static::GetElevatorIid(), IID_PPV_ARGS_Helper(&elevator));
 
   if (FAILED(hr)) {
-    PLOG(ERROR) << "Failed to create instance.";
     return base::unexpected(hr);
   }
 
@@ -56,7 +57,6 @@
       COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
       RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING);
   if (FAILED(hr)) {
-    PLOG(ERROR) << "Failed to create security blanket.";
     return base::unexpected(hr);
   }
 
@@ -64,7 +64,6 @@
 
   job.Set(::CreateJobObjectW(nullptr, nullptr));
   if (!job.is_valid()) {
-    PLOG(ERROR) << "Failed to create job object.";
     return base::unexpected(HRESULT_FROM_WIN32(::GetLastError()));
   }
 
@@ -75,12 +74,10 @@
   if (!::SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation,
                                  &limit_information,
                                  sizeof(limit_information))) {
-    PLOG(ERROR) << "Failed to set extended job limit information.";
     return base::unexpected(HRESULT_FROM_WIN32(::GetLastError()));
   }
 
   if (!::AssignProcessToJobObject(job.get(), ::GetCurrentProcess())) {
-    PLOG(ERROR) << "Failed to place current process in job.";
     return base::unexpected(HRESULT_FROM_WIN32(::GetLastError()));
   }
 
@@ -91,19 +88,20 @@
       /*flags=*/0, command_line.GetCommandLineString().c_str(),
       /*log=*/log.Receive(), &proc_handle, &last_error);
   if (FAILED(hr)) {
-    PLOG(ERROR) << "Failed to launch isolated browser.";
     return base::unexpected(hr);
   }
 
-  return base::WrapUnique<IsolatedBrowser>(new IsolatedBrowser(
+  return IsolatedBrowser(
       base::Process(reinterpret_cast<base::ProcessHandle>(proc_handle)),
-      std::move(job)));
+      std::move(job));
 }
 
-int IsolatedBrowser::WaitForExit() const {
-  int exit_code;
-  process_.WaitForExit(&exit_code);
-  return exit_code;
+std::optional<int> IsolatedBrowser::WaitForExit() const {
+  int exit_code = 0;
+  if (process_.WaitForExit(&exit_code)) {
+    return exit_code;
+  }
+  return std::nullopt;
 }
 
 bool IsIsolationEnabled(const base::CommandLine& command_line) {
diff --git a/chrome/browser/win/isolated_browser_support.h b/chrome/browser/win/isolated_browser_support.h
index 1d837e42..aa27c6c 100644
--- a/chrome/browser/win/isolated_browser_support.h
+++ b/chrome/browser/win/isolated_browser_support.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_WIN_ISOLATED_BROWSER_SUPPORT_H_
 
 #include <memory>
+#include <optional>
 
 #include "base/process/process.h"
 #include "base/types/expected.h"
@@ -25,20 +26,20 @@
   // Attempt to launch an isolated browser process with the command line
   // `command_line`. If successful, an `IsolatedBrowser` is returned, if not
   // then an HRESULT containing the error launching the process.
-  static base::expected<std::unique_ptr<IsolatedBrowser>, HRESULT> Launch(
+  static base::expected<IsolatedBrowser, HRESULT> Launch(
       const base::CommandLine& command_line);
 
-  IsolatedBrowser(IsolatedBrowser&& other) = delete;
-  IsolatedBrowser& operator=(IsolatedBrowser&& other) = delete;
+  IsolatedBrowser(IsolatedBrowser&& other);
+  IsolatedBrowser& operator=(IsolatedBrowser&& other);
   IsolatedBrowser(const IsolatedBrowser&) = delete;
   IsolatedBrowser& operator=(const IsolatedBrowser&) = delete;
 
   ~IsolatedBrowser();
 
-  // Wait for exit of the isolated browser process, and return its exit code.
-  // This should typically be then returned as the exit code of the calling
-  // process.
-  int WaitForExit() const;
+  // Wait for exit of the isolated browser process, and return its `exit_code`,
+  // if it was possible to obtain it. This should typically be then returned as
+  // the exit code of the calling process.
+  std::optional<int> WaitForExit() const;
 
  private:
   IsolatedBrowser(base::Process process, base::win::ScopedHandle job);
@@ -47,10 +48,10 @@
   // process, ensuring that if the launcher terminates then any isolated browser
   // also does. This means that the isolated browser can guarantee that its
   // lifetime always exceeds the lifetime of its parent process.
-  const base::win::ScopedHandle job_;
+  base::win::ScopedHandle job_;
 
   // Handle to the isolated browser process, returned from the elevated service.
-  const base::Process process_;
+  base::Process process_;
 };
 
 // Returns true if the platform configuration indicates that the browser should
diff --git a/chrome/build/android-arm32.pgo.txt b/chrome/build/android-arm32.pgo.txt
index 63f6af3..388c5e9 100644
--- a/chrome/build/android-arm32.pgo.txt
+++ b/chrome/build/android-arm32.pgo.txt
@@ -1 +1 @@
-chrome-android32-main-1772517141-d9e749a3d541b1df3748b10a98630b626fbf992c-42c876b060144139de0fd47355de466d53606f5d.profdata
+chrome-android32-main-1772560800-55a7e4b9bd19318cda43568c72acff2f6ac45e10-0913a3e4401c7882501d3dabf0043505dfa01c1c.profdata
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 51aea1d..20e10f92 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1772553506-e54b1d6e53a53c376ae9c003aa33d6ad00437045-5e443287fdb8001f6d615db8fded684e084bceec.profdata
+chrome-mac-arm-main-1772575194-59281c3790ca770e5220fede14803572aec8d55b-597494522be362d528a6bd90d0b0f34233aae433.profdata
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index 58158f7..2e47154 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-main-1772541087-b7f9b6db84ae084c8d15e872ec0f0dc976cab05e-3f5271784dd1ad4ca38c94cc9031869d661361f5.profdata
+chrome-mac-main-1772560800-252811e0572541a15b5ce3bd72a49d1bf7f9ce86-0913a3e4401c7882501d3dabf0043505dfa01c1c.profdata
diff --git a/chrome/build/win-arm64.pgo.txt b/chrome/build/win-arm64.pgo.txt
index 32e46a2..0746822 100644
--- a/chrome/build/win-arm64.pgo.txt
+++ b/chrome/build/win-arm64.pgo.txt
@@ -1 +1 @@
-chrome-win-arm64-main-1772541087-5e05a5b88d87c2ff68c97eb99ade285d2890b9e3-3f5271784dd1ad4ca38c94cc9031869d661361f5.profdata
+chrome-win-arm64-main-1772560800-1953e4e7093be349067c1dbce1bbe3773fb8d570-0913a3e4401c7882501d3dabf0043505dfa01c1c.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 0bbd390..04e9c57 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1772541087-8cdb7b3adda7c5e5b8bf830ec4333abfa5cb5a0a-3f5271784dd1ad4ca38c94cc9031869d661361f5.profdata
+chrome-win32-main-1772560777-ad03072a25e3f64f14540fe16159e0f054e3b5ca-e23301da2cf7a935d7bc9643beca388c6d0f56e1.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 1937fc9c..371269c 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1772528232-193aff9f5a6ca86b727ef3591ce4c2650031c8b2-ac0c268901ed0db351857bc43ecbed53d6ce06a6.profdata
+chrome-win64-main-1772560777-63ac50abcede3e6897613c60f6cc057d3d3d026d-e23301da2cf7a935d7bc9643beca388c6d0f56e1.profdata
diff --git a/chrome/common/chrome_result_codes.h b/chrome/common/chrome_result_codes.h
index c851ebf..2fec819 100644
--- a/chrome/common/chrome_result_codes.h
+++ b/chrome/common/chrome_result_codes.h
@@ -135,11 +135,15 @@
       partition_alloc::kTerminateOnCommitFailureExitCode,
   // LINT.ThenChange(/base/allocator/partition_allocator/src/partition_alloc/page_allocator.h:CHROME_RESULT_CODE_TERMINATED_BY_OTHER_PROCESS_ON_COMMIT_FAILURE)
 
+  // The isolated browser process launched but it was not possible to wait on
+  // the exit of the process, so the browser must exit. This should not happen.
+  CHROME_RESULT_CODE_INVALID_ISOLATED_BROWSER_PROCESS,
+
   // Last return code (keep this last).
   CHROME_RESULT_CODE_CHROME_LAST_CODE
 };
 
-static_assert(CHROME_RESULT_CODE_CHROME_LAST_CODE == 40,
+static_assert(CHROME_RESULT_CODE_CHROME_LAST_CODE == 41,
               "Please make sure the enum values are in sync with enums.xml");
 
 // Returns true if the result code should be treated as a normal exit code i.e.
diff --git a/chrome/installer/linux/BUILD.gn b/chrome/installer/linux/BUILD.gn
index 85f93c41..20ab87d 100644
--- a/chrome/installer/linux/BUILD.gn
+++ b/chrome/installer/linux/BUILD.gn
@@ -11,6 +11,7 @@
 import("//build/util/lastchange.gni")
 import("//build/util/process_version.gni")
 import("//chrome/installer/installers.gni")
+import("//chrome/installer/linux/common/key.gni")
 import("//chrome/version.gni")
 import("//components/optimization_guide/features.gni")
 import("//third_party/angle/gni/angle.gni")
@@ -270,10 +271,6 @@
     package = "chromium-browser-repo"
   }
 
-  # Assume PGP_KEY_VERSION=1 for now.
-  # Ideally this would be extracted via exec_script or passed from GN.
-  pgp_key_version = "1"
-
   outputs = [ "$root_out_dir/${package}_${pgp_key_version}_all.deb" ]
 
   args = [
diff --git a/chrome/installer/linux/common/key.gni b/chrome/installer/linux/common/key.gni
new file mode 100644
index 0000000..d746b659
--- /dev/null
+++ b/chrome/installer/linux/common/key.gni
@@ -0,0 +1,8 @@
+# Copyright 2026 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This file is automatically generated by update_key_include.py
+# Do not edit this file directly.
+
+pgp_key_version = 2
diff --git a/chrome/installer/linux/common/key.include b/chrome/installer/linux/common/key.include
index 9cfd916..acea900 100644
--- a/chrome/installer/linux/common/key.include
+++ b/chrome/installer/linux/common/key.include
@@ -1,18 +1,13 @@
-# This must increment by 1 every time PGP_KEY_DATA is updated.
+# This file is automatically generated by update_key_include.py
+# Do not edit this file directly.
+
 # This is used as a priority value for the key file, so newer
 # keyrings should always take priority.
-PGP_KEY_VERSION=1
+PGP_KEY_VERSION=2
 
-PGP_KEY_CHECKSUM="=+PQZ"
-
-# Be sure to update PGP_KEY_CHECKSUM and increment PGP_KEY_VERSION
-# when updating PGP_KEY_DATA.
-#
 # pub   rsa4096 2016-04-12 [SC]
 #       EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796
-# uid           Google Inc. (Linux Packages Signing Authority)
-#                           <linux-packages-keymaster@google.com>
-# sub   rsa4096 2023-02-15 [S] [expires: 2026-02-14]
+# uid           [ unknown] Google Inc. (Linux Packages Signing Authority) <linux-packages-keymaster@google.com>
 # sub   rsa4096 2024-01-30 [S] [expires: 2027-01-29]
 # sub   rsa4096 2025-01-07 [S] [expires: 2028-01-07]
 PGP_KEY_DATA=$(cat <<KEYDATA
@@ -41,109 +36,77 @@
 pmB0QXNxr/x737I9Q8FCZasSlNqocaiKF6gKBxFOKfiKx5DRZ63EZ07Z3HE6y+w3
 +97UIJhjxVrONgb7ZX9paE8NtLG/X0ZldUzqWngfnFVasnCDiQC+ls2Tu9Oa+yMJ
 rMe3VM4EcZTjYoESUjKzEHP72hn+GoAk7saWWVK6xYUJPM18Ua1mGx8xwoXt/t95
-W40b92HbJrkCDQRj7PlYARAAym4Cy/rwGmyldqxkg4GPiLbUwLYPcPVKK9fkPwiF
-hsYvyUMu5e10yo5ktML3cFPvX/3hrI8YoG7wHErFdbM8in1UEBU9pvSoQ6wajQgL
-0OU4OmUHTXudaB8I4iENYu8/EKE9tlbnHU2KnDCwB3voNGjaiy0kliwIluM/p3q3
-JYp44k0QsP2lmSUdaM0HdnisAMOq5NfWx3IoV6NhNCtRA5nR3DQPMrcqccFllwX7
-QmEVl4uSdNhnmWs8Zsfw1C5xYMtieBtFC06hlrG3/7Qrdto6oMl/rxY/7CT5/pdy
-CaqcjWOcgRuhnHo3j/b0aEK2qRqh5HMft+39r48JqY+eePCSOdihAtcmXhcKfB4x
-i6fsxmo6Z6JKyneFyR54lvfmzy6u2KezzZ+uTGmL2VI7+XpyneOXxSuryd2LP38e
-jwVyigbPX5USIHVzikr7VfmxiBtCP6fyRGf8D8UYMRzwyuY23COigVZt1JPghxQu
-CAQLc3Uoeh+GX8NRB93UGTC1QQf0o9+FEIZcADpQF7WR975LPyqXJVivIJ6s94vB
-zdHs73J0JUYkTvCKpTffz5hamXjU3q5JU+07dI16oqKSxy9BV0di7J3XjkX8QxNa
-4VTlMZaHriLGPMeoDvIdmxoONWGqUTo2MWWRHGpIPTXIeFwJcXqgeCErbX24lcXi
-9g0AEQEAAYkEWwQYAQgAJgIbAhYhBOtMG/1PBC9t3czskXch9jvTi0eWBQJj7PlY
-BQkFo5qAAinBXSAEGQEIAAYFAmPs+VgACgkQ6Il5+5swrPJG5Q/+PMhN1qYugsPE
-Qc6trsy3ZLql4evdcxulYR1GUDW/OXsBoxg7vw9ubtiRa4QHJpczq8YILy+GvFmr
-T10Gj6g2WkoeNXpTNWGtAu3DUKu8TVQNKXDeW0Pil12TLkGgPPQQpU0lyE8+o+Du
-Kb4QBvMvENhPTL+1GGrNDoQ4M1SK8trNaNj5pdao5W/Y3LTvXK0VIher/UbvWkJI
-Bh2LeLsj9x8yg36Dbs1/1l9ztBZvDTaZyZOqmbCysIO7pFHSTiBCGyyzS1PWWJsr
-N8DbQyjH5uE+/Wm0jcDSJ+HXeYWqR/QQLgyZ5OFpxTmqfQEGT4CV9llygtg10GXk
-l9VV6SN66+xUm0nnPHeW4rcO7NtF1skAdvmaHrUcTYEddOBiIfy2o7WrSyhXPTZz
-/UpoXsvJ68VWRceh7l7Jxjj5G47IhWDLMbT1WJzu9pwQ0wz+GXoyzmmstirQm/KS
-ZAh/FNILqrgxlXfktNl8feO3r8rx6hreVdMlRTw+7gLuwOUAWF77XLc6vd0tY2Qy
-KDD/dznvFaVK1wQX4s8x1cT+lVJsTPeyBPoI1UajfT7jK6dg/chAVBpOOH0Fuc8r
-rqJmGnOzKcdn51oBgPwJfboNrr0uKCM1MixCcaXOjPEWJbmnEiIxYAooLnEbL0wc
-upaGxtRTL50Ms3uvnwHim26yvOTrgNQJEHch9jvTi0eWzxkQAJoEooabuFEvyaFp
-0f2nohX/bqaG11Q5wZ6jgF4jFGhXkvoVLoeRFlIQyyFmL114T2nL26VDpccC7CyH
-T0UBhkqdf66oVUZ5lrCd+A6ACsRuxJavBAKyv6Rfr+MElDHoIwDyUHryHC75vN8/
-ox5m5NQBHoqAWE6uOUW85R5si5hiv809dypwVFhN7BZBAqHKPrzJYvKD3i/iTH4j
-ID29rw7PufGJR6uVtuqXtPAcBs2OS0DOybedqMbKoFxF/zfeUKoEnLHOtucAiBPP
-0KOaV09EypPuVYhaI7NhIt5oFxwVxYCEnQLVgRJqjfUxEqjz6x835xZPbepj4Na+
-Tbd+yCju6E87u/0l6yZVzyEPfTZauhzv5jFXWI21hQT8PPjRlRnpkHITjg2bJLLx
-yRleIIzKVtRQt+zETbImotVDK2lcc7KwrXuP6KqWu22PFXVsOqeZr33a6C5MB1tn
-EtpYAvH7e3uJ6Yh11ywCIm/rBR3KyJGbtLicRgiTpFMJGg6wBSls2WB3NmFK1uVz
-ewjQaP33vdK9Vvf+HrJ+fUjNpkzGq61J9X4hMcBYlHIuFPt/+1OCIlYjXjaGdidf
-oasbnZcdTk+wHtloOHSwEqBB2jCm8uPiVVYnAPI3ZaHKwm6RL9YVVeO4cIinPlU0
-BrwmarPHk/qW58NUXnHddyfTcu2zuQINBGW5WnYBEADEptUD7cowK13TNYtmOuN/
-SXPwCct8pFY8U2g4up+c+5YEIqWkAUqVm8Lp+DqdFuX7NbfK2BNojwPyiqKlaBAN
-6Nx7bO9bjSvlbbZBrVJ3mL+k7xrFdTGLeDSSTlEBesj7FXK1zK7SW9AV3n45lmOL
-AXTC13VQ3K0SEXb+69FwXr31/i54NgTfM/1LcJZhIoR6MutHJCSYKuO6ZHxYaOdf
-j48BGSWj7RIEbF59rIEzDR7pBk61fLm/2illTQTxdMGGBeSwNjR4FadePJ0tdReD
-oIjaQpAGZiUAj5cqeHpmH8ZIoY4fk2SsOinK8cDgse2HnHiiusFr6xx68IycZdp4
-WrQyGDSJZ1ZKk5QaeAPYE3QsZnImdcV2/kJZS8nAWDFoLtpaNSpONDTn1ZYyCi2r
-WPPF8JiVMkmCxsl+ZHjyNvZHPslRsnGB7EoKDpcjP1cPhl37o/wUYpyiLYyE8W+m
-DNucH4YHLVHq/zQGqO3V6axTA1Ds+gu9tHV/3+yErIqpou19VOfPKJjYC7yfzvID
-58jLZNYFq8IRWQ5VWCOQJWMcdzMKB57S2Zb5vIhJkfl/S5ISMGXDXb32nyKVfvC5
-TmiMbfrszc3DLAxwhqJlAH/xmF2yPP8dYh6KKWSIffVGTm38scs9kkm1bVBAR18i
-lx6dGxVjNSM9i30MMXjUUQARAQABiQRyBBgBCgAmFiEE60wb/U8EL23dzOyRdyH2
-O9OLR5YFAmW5WnYCGwIFCQWjmoACQAkQdyH2O9OLR5bBdCAEGQEKAB0WIQQPBv+G
-vur05xhm7lIy7lNVprxuQgUCZbladgAKCRAy7lNVprxuQpgeD/9UZ1yh68qd1JWj
-gKv1ABdmChUvTQIIFcB/D4bBXOp2aa7nPghVNbOY8ArlvWoloEPk4wRb0NIf2xy2
-7o5U0pn8ssqPyI/uL0sUc5ZlGvJRz6sqr+3yLEPNgALPFhP5lfA4n9uIbMSUmRB9
-d9A05EarZ10tBQerDkDxo+RCgBbd79Lzf2dUEV9ni5mXNozu0H5HUbLa7xd26K2+
-8XbduvzzPIPEYGGhvn6iCfzbRkBdFyPllMbkPprURhMlaS+Kp8MJ6JMsY7DMbCb7
-xiGj1CykdQmoRiQ+LQJchNw7zYpDESNkg6I2hsoeXMNuJiSVWZseOu0N7ROHcJgL
-vKQhgpXVEARunqa0y/1mrsiXJpCa9arEiu4MsflMvJsFxEmP7OW8A5M4Zc2idFow
-cal36BxSBJRyX4S0tHn6iK+jRZj1LuHASiSqGaBQcEz00xJls+7RNpg4FTvWekPX
-6uwsGUuifxnIYnkIAMt0cZuQYswWbForGNwUOCV9cOsB9AmnuK2Anm5LrRRVgYOy
-QN49p5IH87A95GB2H+QZZS9slefPXRKHDYz4qLOerz4uZIPVEDhTtUml+BH7tXgK
-zXDbXgcSn+aR/KJA3II6o/cl2UbOnyNlJPc889FC+t/okUqko/Cr+onqSwszqYdQ
-x52NrRYIaUhWKpWsoXaazCVZOxpixaImD/9Z0+sKvJP0Q0t/1uxxASfEbcbNby+d
-Z1NHuSu44G1E/ZGhtigZpZ0W4JBC477tJV+syxYsj3HOSZLxNgMn2e7eXCH2pzku
-IXNFvQuukUKNnL1MZJ9oLQqzOygk3NeiMHv7jAtkTRJ3jS4gnrcHOQ4aY2BMUefb
-M80PTacd9aXn7JpEsCnbrRM/Fvepdvch2ICA6C2Ft2+p7gUQX6eJwF0NYFqnxJhT
-RZSmQWtqT7CjZRYKXIQvhIjEP3W9RJVTclt/CuyDseoTRqRAScGz7hzXn/VlN59g
-yCIDAR0xz72zR4gjU37jjNfvwTG5iufUZluuk0tFGsWbLMBxy2be7zTe0is17L3k
-/fgNXesGZVaMvGN4MpASpwRxkWhjtM2l3Mx7eGLFAOC94rOpQI2yKKVOTN49hdUR
-Ska/efh2U/zVzXhmxb7HSprz+BC7QwLmFTIGEfBqoyOcj/3HFTLIv5oU/nFMTvxe
-/u54scNRqOt+yaq/zReZI46wQ/BIhsvEKxpurgUdzRmndAZzFimtmKGChdZtxbP3
-rwZHMpkfoUsBc5F5ulkjm/IcySGshWupAHO7kiskeYhtNKf53om26jNWlAEAwcFe
-4PcD5nHUSIyUtjLStSVx2QkdYFSm/hDm3LekSTlcPLOfBEuTvcBmZm1bSGfiR027
-h/rYNBKETXAm87kCDQRnfVvtARAAwRYzSDUoojETurwkrtBuUqibv741if3YTey4
-e/dbGrOuFFP21g0NJqg5rNA/6Fo0DYGqR0VwHsbiMp20rZUsVztEGlxHaudAQlaC
-l4/TcMet1lgV2kf3MAGgWyi2BlErDBM6jF2bpe1X7jssRj3IB58+u3WA8lDiHVIr
-BYgVJE8KFDVxOEHPZAPYto8Aa2NbOvr7TF/SebjFMO/JrQRt1Okw6+3IMJSnup7j
-W7fhF6UbGijT6uagv/RxMkYKvYvfFaKOW9YIwuGnZ2BWe9Eim+j605JKX+m9MIuA
-TScWM1AVs2HcPVCj8AeETPczNtD0lsTLswX5+LDzdhQtEo3WRnrvdnWeDXAsDSbj
-4RauH8+kw+nWtphBPR9KpSSIYGjbkx8iN2F4C5OBR//hocpTu+LjIXDGvwNZwqvG
-K/uDrHFsW8jen7/CbINstcEGHZyKrBB9b8ffZOV8v5xeq1qkH3cvXFHgg9NG8ych
-UkED4noj+OWrHpGo8AF6Ye50W8vawAm73WVCMvkbESaBU0vuX5QoazL6HyaDhKie
-fDcwjVzUqu+8iEmWROmqIKoKdoYEGNlBaS6LJUlKrxUserYvGVYl32e3vcyk4uCP
-v3KUPkbs5ARWAYFu8rCp1Fi8qKTxRNhi7uxOiU0VU0y9CgIgnGuik7/GTXuorRrg
-u1pnyhUAEQEAAYkEcgQYAQoAJhYhBOtMG/1PBC9t3czskXch9jvTi0eWBQJnfVvt
-AhsCBQkFo5qAAkAJEHch9jvTi0eWwXQgBBkBCgAdFiEEDiJZF0FGcPRELCUN/VM8
-B8JkZI8FAmd9W+0ACgkQ/VM8B8JkZI+mGQ/+IdaOdi4RcLu4rO37O+yqOQaQaRdU
-CleBkab2HX+6OVpvQUr71UDUmVW7OrD1IFi7BTCQZfRNVUIJzkoDENrqxgfYiCBw
-L503hmMIyJSsmUQc1UTp/sWODn1JNJyv6NY5yQqc/mnbHjGYfSRlf+uRn73SV+eI
-9S9W3+Dlq6egRAO+0w5kGfc6mDHjM5bGori8Yy0XVuEVSJ4y2DkHcTmOU143CaQN
-hXcwca76gxjLVMUwDc5C5M7Pl14PzrETveEr0dDcMGgRM8i7OVhc/SqhCzBUoEjj
-PseNEuZ8FBqBcJeP7rgsRRq6YBmVZZXfPPFmcCTiTMm/nX68RLFoybouXlUbXwwu
-vO/dmLgTPgEyvs6NsCfF9yCuwhMOXCvB4JgLtGr19bDlRLKT3eHSQ0nwBVPoIbZr
-n8uwLO19biOSDssUNZaQwhKg3+sJpHawl+/LZqKNnffIBZScSlZkbr7zzXVgAV2f
-2DVue5YsfH1nurxWNqXMmFUV71/wyHCTYrvrhVQvumc01jGXgqPShMCpVUYNmgxY
-cJwTufGoZvOOfwf+6dNjS+0rmTwE/K5S3QO16Xx+ymOUOXmjlVt4FCjpwUETNlJ3
-mttq9ea2LGEvsIYqoJZodsno5hNrzlbXid1SZEnmo90txhqR2XXpSP8OsW4oc4+u
-6WABuz6GaCbWr+dh2A//dRQyBqrFTp5+yJaLczenP2dO7SZwjySknO9+i/tYg2Ms
-u7b7JVHJFtL3zjvdmvopLj0EWo5c1AGa1mXvttzPmyF+E/Ggot3xglVrK+cFCuMI
-gTUIAK+YkFkYNlI1jS8Rrgb6fpZPcPARqfqmVnf/Ezqzhxpl8/jdlmQSK7UzWy9C
-2DMX3wcXECFfmgvIw/DHxHrVfRe0rGaNVs2tMHBF8l/8VEOaa/mpLGg7aDWClsJy
-bZUagYG+gPCz3ka88U2VZR6zjFAxyUijTUm7KckYvI5oSbX1Ne7hPfq6UJM79vWU
-Yg4uL7bm+kUqXAGsPrKrjKu3zW3q8a+0jWlmu3VZybuFx7wIUuvN/OjYPbzIc8ix
-PRi8rsCfBQr9IwohXURQ2sshGGdMwGc/4Fo4XQfFwBieVEtLBJw/HakBOCUeYi81
-xGQKx8hlA5eOfyW8VezuWeQ/kpQXn/5xywmPyQR3NyxhpzNB7fBjY3fqJupE8589
-5WY8UAo9CxR2DHjtwA8pObo/EinvcjFx4ZduQ5cntbWWpU8qLBCHT1GTBaky0kDY
-dJ1uYifEGITvW0mpLFCmnYR81c6BZNOuzjqWS5bf0jt2vQ04AaiZ5o2c657EyaVN
-nLZF/vsF+jNFkhn5EBiVm7n9bUR3ZfiJHyLvpARPogIWaDEAMrMHhael4FoGoCs=
+W40b92HbJrkCDQRluVp2ARAAxKbVA+3KMCtd0zWLZjrjf0lz8AnLfKRWPFNoOLqf
+nPuWBCKlpAFKlZvC6fg6nRbl+zW3ytgTaI8D8oqipWgQDejce2zvW40r5W22Qa1S
+d5i/pO8axXUxi3g0kk5RAXrI+xVytcyu0lvQFd5+OZZjiwF0wtd1UNytEhF2/uvR
+cF699f4ueDYE3zP9S3CWYSKEejLrRyQkmCrjumR8WGjnX4+PARklo+0SBGxefayB
+Mw0e6QZOtXy5v9opZU0E8XTBhgXksDY0eBWnXjydLXUXg6CI2kKQBmYlAI+XKnh6
+Zh/GSKGOH5NkrDopyvHA4LHth5x4orrBa+scevCMnGXaeFq0Mhg0iWdWSpOUGngD
+2BN0LGZyJnXFdv5CWUvJwFgxaC7aWjUqTjQ059WWMgotq1jzxfCYlTJJgsbJfmR4
+8jb2Rz7JUbJxgexKCg6XIz9XD4Zd+6P8FGKcoi2MhPFvpgzbnB+GBy1R6v80Bqjt
+1emsUwNQ7PoLvbR1f9/shKyKqaLtfVTnzyiY2Au8n87yA+fIy2TWBavCEVkOVVgj
+kCVjHHczCgee0tmW+byISZH5f0uSEjBlw1299p8ilX7wuU5ojG367M3NwywMcIai
+ZQB/8Zhdsjz/HWIeiilkiH31Rk5t/LHLPZJJtW1QQEdfIpcenRsVYzUjPYt9DDF4
+1FEAEQEAAYkEcgQYAQoAJhYhBOtMG/1PBC9t3czskXch9jvTi0eWBQJluVp2AhsC
+BQkFo5qAAkAJEHch9jvTi0eWwXQgBBkBCgAdFiEEDwb/hr7q9OcYZu5SMu5TVaa8
+bkIFAmW5WnYACgkQMu5TVaa8bkKYHg//VGdcoevKndSVo4Cr9QAXZgoVL00CCBXA
+fw+GwVzqdmmu5z4IVTWzmPAK5b1qJaBD5OMEW9DSH9sctu6OVNKZ/LLKj8iP7i9L
+FHOWZRryUc+rKq/t8ixDzYACzxYT+ZXwOJ/biGzElJkQfXfQNORGq2ddLQUHqw5A
+8aPkQoAW3e/S839nVBFfZ4uZlzaM7tB+R1Gy2u8XduitvvF23br88zyDxGBhob5+
+ogn820ZAXRcj5ZTG5D6a1EYTJWkviqfDCeiTLGOwzGwm+8Yho9QspHUJqEYkPi0C
+XITcO82KQxEjZIOiNobKHlzDbiYklVmbHjrtDe0Th3CYC7ykIYKV1RAEbp6mtMv9
+Zq7IlyaQmvWqxIruDLH5TLybBcRJj+zlvAOTOGXNonRaMHGpd+gcUgSUcl+EtLR5
++oivo0WY9S7hwEokqhmgUHBM9NMSZbPu0TaYOBU71npD1+rsLBlLon8ZyGJ5CADL
+dHGbkGLMFmxaKxjcFDglfXDrAfQJp7itgJ5uS60UVYGDskDePaeSB/OwPeRgdh/k
+GWUvbJXnz10Shw2M+Kiznq8+LmSD1RA4U7VJpfgR+7V4Cs1w214HEp/mkfyiQNyC
+OqP3JdlGzp8jZST3PPPRQvrf6JFKpKPwq/qJ6ksLM6mHUMedja0WCGlIViqVrKF2
+mswlWTsaYsWiJg//WdPrCryT9ENLf9bscQEnxG3GzW8vnWdTR7kruOBtRP2RobYo
+GaWdFuCQQuO+7SVfrMsWLI9xzkmS8TYDJ9nu3lwh9qc5LiFzRb0LrpFCjZy9TGSf
+aC0KszsoJNzXojB7+4wLZE0Sd40uIJ63BzkOGmNgTFHn2zPND02nHfWl5+yaRLAp
+260TPxb3qXb3IdiAgOgthbdvqe4FEF+nicBdDWBap8SYU0WUpkFrak+wo2UWClyE
+L4SIxD91vUSVU3Jbfwrsg7HqE0akQEnBs+4c15/1ZTefYMgiAwEdMc+9s0eII1N+
+44zX78ExuYrn1GZbrpNLRRrFmyzAcctm3u803tIrNey95P34DV3rBmVWjLxjeDKQ
+EqcEcZFoY7TNpdzMe3hixQDgveKzqUCNsiilTkzePYXVEUpGv3n4dlP81c14ZsW+
+x0qa8/gQu0MC5hUyBhHwaqMjnI/9xxUyyL+aFP5xTE78Xv7ueLHDUajrfsmqv80X
+mSOOsEPwSIbLxCsabq4FHc0Zp3QGcxYprZihgoXWbcWz968GRzKZH6FLAXORebpZ
+I5vyHMkhrIVrqQBzu5IrJHmIbTSn+d6JtuozVpQBAMHBXuD3A+Zx1EiMlLYy0rUl
+cdkJHWBUpv4Q5ty3pEk5XDyznwRLk73AZmZtW0hn4kdNu4f62DQShE1wJvO5Ag0E
+Z31b7QEQAMEWM0g1KKIxE7q8JK7QblKom7++NYn92E3suHv3WxqzrhRT9tYNDSao
+OazQP+haNA2BqkdFcB7G4jKdtK2VLFc7RBpcR2rnQEJWgpeP03DHrdZYFdpH9zAB
+oFsotgZRKwwTOoxdm6XtV+47LEY9yAefPrt1gPJQ4h1SKwWIFSRPChQ1cThBz2QD
+2LaPAGtjWzr6+0xf0nm4xTDvya0EbdTpMOvtyDCUp7qe41u34RelGxoo0+rmoL/0
+cTJGCr2L3xWijlvWCMLhp2dgVnvRIpvo+tOSSl/pvTCLgE0nFjNQFbNh3D1Qo/AH
+hEz3MzbQ9JbEy7MF+fiw83YULRKN1kZ673Z1ng1wLA0m4+EWrh/PpMPp1raYQT0f
+SqUkiGBo25MfIjdheAuTgUf/4aHKU7vi4yFwxr8DWcKrxiv7g6xxbFvI3p+/wmyD
+bLXBBh2ciqwQfW/H32TlfL+cXqtapB93L1xR4IPTRvMnIVJBA+J6I/jlqx6RqPAB
+emHudFvL2sAJu91lQjL5GxEmgVNL7l+UKGsy+h8mg4Sonnw3MI1c1KrvvIhJlkTp
+qiCqCnaGBBjZQWkuiyVJSq8VLHq2LxlWJd9nt73MpOLgj79ylD5G7OQEVgGBbvKw
+qdRYvKik8UTYYu7sTolNFVNMvQoCIJxropO/xk17qK0a4LtaZ8oVABEBAAGJBHIE
+GAEKACYWIQTrTBv9TwQvbd3M7JF3IfY704tHlgUCZ31b7QIbAgUJBaOagAJACRB3
+IfY704tHlsF0IAQZAQoAHRYhBA4iWRdBRnD0RCwlDf1TPAfCZGSPBQJnfVvtAAoJ
+EP1TPAfCZGSPphkP/iHWjnYuEXC7uKzt+zvsqjkGkGkXVApXgZGm9h1/ujlab0FK
++9VA1JlVuzqw9SBYuwUwkGX0TVVCCc5KAxDa6sYH2IggcC+dN4ZjCMiUrJlEHNVE
+6f7Fjg59STScr+jWOckKnP5p2x4xmH0kZX/rkZ+90lfniPUvVt/g5aunoEQDvtMO
+ZBn3Opgx4zOWxqK4vGMtF1bhFUieMtg5B3E5jlNeNwmkDYV3MHGu+oMYy1TFMA3O
+QuTOz5deD86xE73hK9HQ3DBoETPIuzlYXP0qoQswVKBI4z7HjRLmfBQagXCXj+64
+LEUaumAZlWWV3zzxZnAk4kzJv51+vESxaMm6Ll5VG18MLrzv3Zi4Ez4BMr7OjbAn
+xfcgrsITDlwrweCYC7Rq9fWw5USyk93h0kNJ8AVT6CG2a5/LsCztfW4jkg7LFDWW
+kMISoN/rCaR2sJfvy2aijZ33yAWUnEpWZG6+8811YAFdn9g1bnuWLHx9Z7q8Vjal
+zJhVFe9f8Mhwk2K764VUL7pnNNYxl4Kj0oTAqVVGDZoMWHCcE7nxqGbzjn8H/unT
+Y0vtK5k8BPyuUt0Dtel8fspjlDl5o5VbeBQo6cFBEzZSd5rbavXmtixhL7CGKqCW
+aHbJ6OYTa85W14ndUmRJ5qPdLcYakdl16Uj/DrFuKHOPrulgAbs+hmgm1q/nYdgP
+/3UUMgaqxU6efsiWi3M3pz9nTu0mcI8kpJzvfov7WINjLLu2+yVRyRbS98473Zr6
+KS49BFqOXNQBmtZl77bcz5shfhPxoKLd8YJVayvnBQrjCIE1CACvmJBZGDZSNY0v
+Ea4G+n6WT3DwEan6plZ3/xM6s4caZfP43ZZkEiu1M1svQtgzF98HFxAhX5oLyMPw
+x8R61X0XtKxmjVbNrTBwRfJf/FRDmmv5qSxoO2g1gpbCcm2VGoGBvoDws95GvPFN
+lWUes4xQMclIo01JuynJGLyOaEm19TXu4T36ulCTO/b1lGIOLi+25vpFKlwBrD6y
+q4yrt81t6vGvtI1pZrt1Wcm7hce8CFLrzfzo2D28yHPIsT0YvK7AnwUK/SMKIV1E
+UNrLIRhnTMBnP+BaOF0HxcAYnlRLSwScPx2pATglHmIvNcRkCsfIZQOXjn8lvFXs
+7lnkP5KUF5/+ccsJj8kEdzcsYaczQe3wY2N36ibqRPOfPeVmPFAKPQsUdgx47cAP
+KTm6PxIp73IxceGXbkOXJ7W1lqVPKiwQh09RkwWpMtJA2HSdbmInxBiE71tJqSxQ
+pp2EfNXOgWTTrs46lkuW39I7dr0NOAGomeaNnOuexMmlTZy2Rf77BfozRZIZ+RAY
+lZu5/W1Ed2X4iR8i76QET6ICFmgxADKzB4WnpeBaBqAr
 KEYDATA
 )
+
+PGP_KEY_CHECKSUM="=AcRC"
diff --git a/chrome/installer/linux/common/update_key_include.py b/chrome/installer/linux/common/update_key_include.py
index c50cc36b..6f82ac0 100755
--- a/chrome/installer/linux/common/update_key_include.py
+++ b/chrome/installer/linux/common/update_key_include.py
@@ -31,6 +31,17 @@
 PGP_KEY_CHECKSUM="{checksum}"
 """
 
+KEY_GNI_TEMPLATE = """\
+# Copyright 2026 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This file is automatically generated by update_key_include.py
+# Do not edit this file directly.
+
+pgp_key_version = {version}
+"""
+
 
 def crc24(data):
     crc = 0x00B704CE
@@ -52,6 +63,7 @@
 def main():
     script_dir = os.path.dirname(os.path.realpath(__file__))
     key_include_path = os.path.join(script_dir, "key.include")
+    key_gni_path = os.path.join(script_dir, "key.gni")
 
     # Read current key.include
     with open(key_include_path, "r") as f:
@@ -145,7 +157,11 @@
     with open(key_include_path, "w") as f:
         f.write(output)
 
-    print(f"Updated key.include to version {new_version}")
+    gni_output = KEY_GNI_TEMPLATE.format(version=new_version)
+    with open(key_gni_path, "w") as f:
+        f.write(gni_output)
+
+    print(f"Updated key.include and key.gni to version {new_version}")
 
 
 if __name__ == "__main__":
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index dac04a6..f9555f1 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -2588,6 +2588,7 @@
       "//chrome/browser/device_api:browser_tests",
       "//chrome/browser/devtools",
       "//chrome/browser/devtools:test_support",
+      "//chrome/browser/digital_credentials",
       "//chrome/browser/dom_distiller:browser_tests",
       "//chrome/browser/domain_reliability:browser_tests",
       "//chrome/browser/enterprise/browser_management:management_identity",
@@ -6629,7 +6630,6 @@
     "../browser/data_sharing/data_sharing_navigation_throttle_unittest.cc",
     "../browser/data_sharing/data_sharing_service_factory_unittest.cc",
     "../browser/data_sharing/personal_collaboration_data/personal_collaboration_data_service_factory_unittest.cc",
-    "../browser/digital_credentials/digital_identity_low_risk_origins_unittest.cc",
     "../browser/download/chrome_download_manager_delegate_unittest.cc",
     "../browser/download/deferred_client_wrapper_unittest.cc",
     "../browser/download/download_core_service_impl_unittest.cc",
@@ -7141,6 +7141,7 @@
     "//chrome/browser/data_sharing",
     "//chrome/browser/devtools",
     "//chrome/browser/diagnostics:unit_tests",
+    "//chrome/browser/digital_credentials:unit_tests",
     "//chrome/browser/domain_reliability",
     "//chrome/browser/download",
     "//chrome/browser/download:status_text_builder_utils",
@@ -8435,6 +8436,7 @@
       "../browser/ui/intent_picker_tab_helper_unittest.cc",
       "../browser/ui/recently_audible_helper_unittest.cc",
       "../browser/ui/side_panel/side_panel_entry_key_unittest.cc",
+      "../browser/ui/side_panel/side_panel_metrics_unittest.cc",
       "../browser/ui/url_identity_unittest.cc",
       "../browser/ui/views/accessibility/theme_tracking_non_accessible_image_view_unittest.cc",
       "../browser/ui/views/autofill/address_editor_view_unittest.cc",
@@ -8455,7 +8457,6 @@
       "../browser/ui/views/passwords/password_save_update_view_unittest.cc",
       "../browser/ui/views/passwords/post_save_compromised_bubble_view_unittest.cc",
       "../browser/ui/views/passwords/shared_passwords_notification_view_unittest.cc",
-      "../browser/ui/views/side_panel/side_panel_util_unittest.cc",
       "../browser/ui/views/tab_sharing/tab_sharing_infobar_unittest.cc",
       "../browser/ui/views/tab_sharing/tab_sharing_status_message_view_unittest.cc",
       "../browser/ui/views/tab_sharing/tab_sharing_test_utils.cc",
@@ -9766,7 +9767,6 @@
       "../browser/extensions/api/socket/tls_socket_unittest.cc",
       "../browser/extensions/api/socket/udp_socket_unittest.cc",
       "../browser/extensions/api/sockets_tcp_server/sockets_tcp_server_api_unittest.cc",
-      "../browser/extensions/api/streams_private/streams_private_manifest_unittest.cc",
       "../browser/extensions/api/tabs/tabs_api_unittest.cc",
       "../browser/extensions/api/tabs/windows_util_unittest.cc",
       "../browser/extensions/app_tab_helper_unittest.cc",
diff --git a/chrome/test/base/test_browser_window.cc b/chrome/test/base/test_browser_window.cc
index d2a861297..349e3e0 100644
--- a/chrome/test/base/test_browser_window.cc
+++ b/chrome/test/base/test_browser_window.cc
@@ -9,9 +9,9 @@
 #include "base/feature_list.h"
 #include "base/values.h"
 #include "build/build_config.h"
-#include "chrome/browser/ui/browser_list.h"
-#include "chrome/browser/ui/browser_list_observer.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_features.h"
+#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
+#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
 #include "chrome/browser/ui/find_bar/find_bar.h"
 #include "chrome/browser/ui/user_education/browser_user_education_interface.h"
 #include "chrome/browser/ui/views/bubble_anchor_util_views.h"
@@ -129,7 +129,8 @@
   // TestBrowserWindow will always be instantiated before its Browser.
   // TODO(crbug.com/413168662): This can be removed once Browser is updated to
   // always own its BrowserWindow.
-  browser_list_observer_.Observe(BrowserList::GetInstance());
+  browser_collection_observation_.Observe(
+      GlobalBrowserCollection::GetInstance());
 }
 
 TestBrowserWindow::~TestBrowserWindow() {
@@ -435,9 +436,10 @@
   is_tab_modal_popup_deprecated_ = is_tab_modal_popup_deprecated;
 }
 
-void TestBrowserWindow::OnBrowserAdded(Browser* browser) {
-  if (browser->create_params().window == this) {
-    browser_ = browser;
-    browser_list_observer_.Reset();
+void TestBrowserWindow::OnBrowserCreated(BrowserWindowInterface* browser) {
+  Browser* current_browser = browser->GetBrowserForMigrationOnly();
+  if (current_browser->create_params().window == this) {
+    browser_ = current_browser;
+    browser_collection_observation_.Reset();
   }
 }
diff --git a/chrome/test/base/test_browser_window.h b/chrome/test/base/test_browser_window.h
index 023990d..b1a1e0a 100644
--- a/chrome/test/base/test_browser_window.h
+++ b/chrome/test/base/test_browser_window.h
@@ -14,9 +14,8 @@
 #include "build/build_config.h"
 #include "chrome/browser/ui/autofill/test/test_autofill_bubble_handler.h"
 #include "chrome/browser/ui/browser.h"
-#include "chrome/browser/ui/browser_list.h"
-#include "chrome/browser/ui/browser_list_observer.h"
 #include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/browser_window/public/browser_collection_observer.h"
 #include "chrome/browser/ui/dialogs/browser_dialogs.h"
 #include "chrome/browser/ui/location_bar/location_bar.h"
 #include "chrome/browser/ui/translate/partial_translate_bubble_model.h"
@@ -31,6 +30,7 @@
 #endif  //  !BUILDFLAG(IS_ANDROID)
 
 class LocationBarTesting;
+class GlobalBrowserCollection;
 class OmniboxView;
 
 namespace qrcode_generator {
@@ -55,7 +55,8 @@
 // contains a valid LocationBar, all other getters return NULL.
 // However, some of them can be preset to a specific value.
 // See BrowserWithTestWindowTest for an example of using this class.
-class TestBrowserWindow : public BrowserWindow, public BrowserListObserver {
+class TestBrowserWindow : public BrowserWindow,
+                          public BrowserCollectionObserver {
  public:
   TestBrowserWindow();
   TestBrowserWindow(const TestBrowserWindow&) = delete;
@@ -246,9 +247,7 @@
 
   void CreateTabSearchBubble(
       tab_search::mojom::TabSearchSection section =
-          tab_search::mojom::TabSearchSection::kSearch,
-      tab_search::mojom::TabOrganizationFeature feature =
-          tab_search::mojom::TabOrganizationFeature::kNone) override {}
+          tab_search::mojom::TabSearchSection::kSearch) override {}
   void CloseTabSearchBubble() override {}
 
   bool IsTabModalPopupDeprecated() const override;
@@ -309,8 +308,8 @@
     bool HasSecurityStateChanged() override;
   };
 
-  // BrowserListObserver:
-  void OnBrowserAdded(Browser* browser) override;
+  // BrowserCollectionObserver:
+  void OnBrowserCreated(BrowserWindowInterface* browser) override;
 
   autofill::TestAutofillBubbleHandler autofill_bubble_handler_;
   TestLocationBar location_bar_;
@@ -324,8 +323,8 @@
   bool is_tab_strip_editable_ = true;
   bool is_tab_modal_popup_deprecated_ = false;
 
-  base::ScopedObservation<BrowserList, BrowserListObserver>
-      browser_list_observer_{this};
+  base::ScopedObservation<GlobalBrowserCollection, BrowserCollectionObserver>
+      browser_collection_observation_{this};
   raw_ptr<Browser> browser_;
   base::OnceClosure close_callback_;
 };
diff --git a/chrome/test/base/ui_test_utils.cc b/chrome/test/base/ui_test_utils.cc
index 291b5806..9aa39dc 100644
--- a/chrome/test/base/ui_test_utils.cc
+++ b/chrome/test/base/ui_test_utils.cc
@@ -650,6 +650,18 @@
   waiter.Wait();
 }
 
+void DeprecatedFakeActivateBrowser(BrowserWindowInterface* browser) {
+  CHECK(browser);
+
+  // We must deactivate the currently active browser first.
+  GetLastActiveBrowserWindowInterfaceWithAnyProfile()
+      ->GetBrowserForMigrationOnly()
+      ->DidBecomeInactive();
+
+  // Fake activation of the target browser.
+  browser->GetBrowserForMigrationOnly()->DidBecomeActive();
+}
+
 void SendToOmniboxAndSubmit(BrowserWindowInterface* browser,
                             std::string_view input,
                             base::TimeTicks match_selection_timestamp,
diff --git a/chrome/test/base/ui_test_utils.h b/chrome/test/base/ui_test_utils.h
index 85f30eb1..86924d1 100644
--- a/chrome/test/base/ui_test_utils.h
+++ b/chrome/test/base/ui_test_utils.h
@@ -361,6 +361,16 @@
     BrowserWindowInterface* browser,
     bool wait_for_set_last_active_observed = false);
 
+// DEPRECATED - DO NOT USE. This function exists only to assist with deprecation
+// of existing tests incorrectly manipulating browser activation state. If you
+// want to write tests that handle browser activation, please create an
+// interactive ui test and activate the browser's ui::BaseWindow.
+//
+// This function fakes the activation state managed by `browser`. It does not
+// change the activation state of the underlying ui::BaseWindow. This creates
+// inconsistencies in tests and may yield unexpected results.
+void DeprecatedFakeActivateBrowser(BrowserWindowInterface* browser);
+
 // Send the given text to the omnibox and wait until it's updated.
 void SendToOmniboxAndSubmit(
     BrowserWindowInterface* browser,
diff --git a/chrome/test/data/pdf/accessibility/paragraphs-and-heading-untagged-nonsense.pdf b/chrome/test/data/pdf/accessibility/paragraphs-and-heading-untagged-nonsense.pdf
new file mode 100644
index 0000000..7c81df1
--- /dev/null
+++ b/chrome/test/data/pdf/accessibility/paragraphs-and-heading-untagged-nonsense.pdf
@@ -0,0 +1,110 @@
+%PDF-1.7
+%���
+%@BLINK-ALLOW:hierarchicalLevel*
+1 0 obj <<
+  /Type /Catalog
+  /Pages 2 0 R
+>>
+endobj
+2 0 obj <<
+  /Type /Pages
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+% Page number 0
+3 0 obj <<
+  /Type /Page
+  /Parent 2 0 R
+  /Contents 4 0 R
+  /Resources <<
+    /Font <<
+      /F1 5 0 R
+    >>
+    /ProcSet [/PDF /Text]
+  >>
+  /MediaBox [0 0 612 792]
+>>
+endobj
+4 0 obj <<
+  /Length 1024
+>>
+stream
+2 J
+BT
+0 0 0 rg
+/F1 0027 Tf
+69 723 Td
+(Heading) Tj
+ET
+BT
+/F1 0010 Tf
+69 688 Td
+(This is a small pdf file:) Tj
+ET
+BT
+/F1 0010 Tf
+69 664 Td
+(!!!!! ????? !5 5!mply dummy text of the pr!nt!ng and type5ett!ng !ndu5try.) Tj
+ET
+BT
+/F1 0010 Tf
+69 652 Td
+(!!!!! ????? ha5 been the !ndu5try'5 5tandard dummy text ever 5!nce the 15005.) Tj
+ET
+BT
+/F1 0010 Tf
+69 628 Td
+(!t ha5 5urv!ved not only f!ve centur!e5, but al5o the leap !nto electron!c type5ett!ng,) Tj
+ET
+BT
+/F1 0010 Tf
+69 616 Td
+(rema!n!ng e55ent!ally unchanged.  !t wa5 popular!5ed !n the 19605 w!th the relea5e of) Tj
+ET
+BT
+/F1 0010 Tf
+69 604 Td
+(Letra5et 5heet5 conta!n!ng !!!!! ????? pa55age5, and more recently w!th de5ktop) Tj
+ET
+BT
+/F1 0010 Tf
+69 592 Td
+(publ!5h!ng 5oftware l!ke Aldu5 PageMaker !nclud!ng ver5!on5 of !!!!! ?????.) Tj
+ET
+BT
+/F1 0010 Tf
+69 569 Td
+(Contrary to popular bel!ef, !!!!! ????? !5 not 5!mply random text. !t ha5 root5) Tj
+ET
+BT
+/F1 0010 Tf
+69 557 Td
+(!n a p!ece of cla55!cal Lat!n l!terature from 45 BC, mak!ng !t over 2000 year5 old.) Tj
+ET
+endstream
+endobj
+5 0 obj <<
+  /Type /Font
+  /SubType /Type1
+  /Name /F1
+  /BaseFont /Helvetica
+  /Encoding /WinAnsiEncoding
+>>
+endobj
+
+xref
+0 6
+0000000000 65535 f
+0000000048 00000 n
+0000000101 00000 n
+0000000180 00000 n
+0000000358 00000 n
+0000001435 00000 n
+trailer <<
+  /Root 1 0 R
+  /Size 6
+>>
+startxref
+1553
+%EOF
diff --git a/chrome/test/data/web_apps/sample_web_app.json b/chrome/test/data/web_apps/sample_web_app.json
index 582572e..124cfc1 100644
--- a/chrome/test/data/web_apps/sample_web_app.json
+++ b/chrome/test/data/web_apps/sample_web_app.json
@@ -261,7 +261,7 @@
    "parent_app_id": null,
    "pending_migration_info": {
       "behavior": "kForce",
-      "manifest_id": "https://example.com/manifest_id_771601322"
+      "manifest_id": "https://example.com/manifest_id_1548098189"
    },
    "pending_update_info": {
       "downloaded_manifest_icons": [ {
@@ -541,14 +541,14 @@
       "url": "https://example.com/icons1481177892"
    } ],
    "unvalidated_migration_sources": [ {
-      "behavior": "WEB_APP_MIGRATION_BEHAVIOR_FORCE",
-      "install_url": "https://example.com/install_url_554811270",
+      "behavior": "kForce",
+      "install_url": "https://example.com/install_url_771601322",
       "manifest_id": "https://example.com/manifest_id_2069295071"
    } ],
    "user_link_capturing_preference": "1",
    "validated_migration_sources": [ {
-      "behavior": "WEB_APP_MIGRATION_BEHAVIOR_FORCE",
-      "install_url": "https://example.com/install_url_554811270",
+      "behavior": "kForce",
+      "install_url": "https://example.com/install_url_771601322",
       "manifest_id": "https://example.com/manifest_id_2069295071"
    } ],
    "was_shortcut_app": true,
diff --git a/chrome/test/data/webui/contextual_tasks/composebox_test.ts b/chrome/test/data/webui/contextual_tasks/composebox_test.ts
index 1bd4231..10f7ca8 100644
--- a/chrome/test/data/webui/contextual_tasks/composebox_test.ts
+++ b/chrome/test/data/webui/contextual_tasks/composebox_test.ts
@@ -94,6 +94,10 @@
       enableBasicModeZOrder: true,
       composeboxShowContextMenu: true,
       composeboxHintTextLensOverlay: 'Test Lens Hint',
+      composeboxHintTextAskAboutThese: 'Ask about these',
+      composeboxHintTextAskAboutThisTab: 'Ask about this tab',
+      composeboxHintTextAskAboutThisImage: 'Ask about this image',
+      composeboxHintTextAskAboutThisDoc: 'Ask about this doc',
     });
 
     testProxy = new TestContextualTasksBrowserProxy('https://google.com');
@@ -1315,6 +1319,27 @@
   });
 
   test(
+      'Injected input with icon can be added, then deleted from AIM',
+      async () => {
+        composebox.injectInput('title', '', FAKE_TOKEN_STRING, 'quoteFilled');
+        await composebox.updateComplete;
+        await microtasksFinished();
+
+        // Avoid using $.carousel since may be cached.
+        const carousel = composebox.shadowRoot.querySelector('#carousel');
+        assertTrue(!!carousel, 'Carousel should be in the DOM');
+        const files = carousel.files;
+        assertEquals(1, files.length);
+
+        composebox.deleteFile(FAKE_TOKEN_STRING);
+        await composebox.updateComplete;
+        await microtasksFinished();
+        assertFalse(
+            !!composebox.shadowRoot.querySelector('#carousel'),
+            'Carousel should be removed from the DOM');
+      });
+
+  test(
       'Injected input can be added, then deleted from composebox', async () => {
         composebox.injectInput('title', 'thumbnail.jpg', FAKE_TOKEN_STRING);
         await composebox.updateComplete;
@@ -1334,4 +1359,91 @@
             !!composebox.shadowRoot.querySelector('#carousel'),
             'Carousel should be removed from the DOM');
       });
+
+  test('Multiple files updates placeholder', async () => {
+    const contextualComposebox = contextualTasksApp.$.composebox;
+    const innerComposebox = contextualComposebox.$.composebox;
+
+    const token1 = {high: 0n, low: 1n} as any;
+    const token2 = {high: 0n, low: 2n} as any;
+    innerComposebox.addFileContextForTesting(
+        {type: 'image/png', uuid: token1} as ComposeboxFile);
+    innerComposebox.addFileContextForTesting(
+        {type: 'application/pdf', uuid: token2} as ComposeboxFile);
+    await contextualComposebox.updateComplete;
+    await innerComposebox.updateComplete;
+
+    assertEquals('Ask about these', innerComposebox.$.input.placeholder);
+  });
+
+  test('Single tab file updates placeholder', async () => {
+    const contextualComposebox = contextualTasksApp.$.composebox;
+    const innerComposebox = contextualComposebox.$.composebox;
+
+    const token = {high: 0n, low: 1n} as any;
+    innerComposebox.addFileContextForTesting(
+        {type: 'tab', uuid: token} as ComposeboxFile);
+    await contextualComposebox.updateComplete;
+    await innerComposebox.updateComplete;
+
+    assertEquals('Ask about this tab', innerComposebox.$.input.placeholder);
+  });
+
+  test('Single image file updates placeholder', async () => {
+    const contextualComposebox = contextualTasksApp.$.composebox;
+    const innerComposebox = contextualComposebox.$.composebox;
+
+    const token = {high: 0n, low: 1n} as any;
+    innerComposebox.addFileContextForTesting(
+        {type: 'image/png', uuid: token} as ComposeboxFile);
+    await contextualComposebox.updateComplete;
+    await innerComposebox.updateComplete;
+
+    assertEquals('Ask about this image', innerComposebox.$.input.placeholder);
+  });
+
+  test('Single pdf file updates placeholder', async () => {
+    const contextualComposebox = contextualTasksApp.$.composebox;
+    const innerComposebox = contextualComposebox.$.composebox;
+
+    const token = {high: 0n, low: 1n} as any;
+    innerComposebox.addFileContextForTesting(
+        {type: 'application/pdf', uuid: token} as ComposeboxFile);
+    await contextualComposebox.updateComplete;
+    await innerComposebox.updateComplete;
+
+    assertEquals('Ask about this doc', innerComposebox.$.input.placeholder);
+  });
+
+  test('Single unknown file updates placeholder', async () => {
+    const contextualComposebox = contextualTasksApp.$.composebox;
+    const innerComposebox = contextualComposebox.$.composebox;
+
+    const token = {high: 0n, low: 1n} as any;
+    innerComposebox.addFileContextForTesting(
+        {type: 'unknown/type', uuid: token} as ComposeboxFile);
+    await contextualComposebox.updateComplete;
+    await innerComposebox.updateComplete;
+
+    assertFalse(innerComposebox.$.input.placeholder.includes('Ask about'));
+  });
+
+  test('Overlay hint text overridden by file hint', async () => {
+    const contextualComposebox = contextualTasksApp.$.composebox;
+    const innerComposebox = contextualComposebox.$.composebox;
+
+    // Set overlay hint text to true.
+    contextualComposebox.maybeShowOverlayHintText = true;
+
+    // Add an image file.
+    const token = {high: 0n, low: 1n} as any;
+    innerComposebox.addFileContextForTesting(
+        {type: 'image/png', uuid: token} as ComposeboxFile);
+
+    await contextualComposebox.updateComplete;
+    await innerComposebox.updateComplete;
+
+    // File hint should take precedence over overlay hint.
+    assertEquals('Ask about this image', innerComposebox.$.input.placeholder);
+  });
 });
diff --git a/chrome/test/data/webui/contextual_tasks/contextual_tasks_pixel_interactive_ui_test.cc b/chrome/test/data/webui/contextual_tasks/contextual_tasks_pixel_interactive_ui_test.cc
index d7da880..8234fe3 100644
--- a/chrome/test/data/webui/contextual_tasks/contextual_tasks_pixel_interactive_ui_test.cc
+++ b/chrome/test/data/webui/contextual_tasks/contextual_tasks_pixel_interactive_ui_test.cc
@@ -206,7 +206,7 @@
       // Take a screenshot of the composebox.
       ScreenshotWebUi(kActiveTab, kComposebox,
                       /*screenshot_name=*/"ContextualTasksComposebox",
-                      /*baseline_cl=*/"7519825"));
+                      /*baseline_cl=*/"7620222"));
 }
 
 struct AppPixelTestParams {
@@ -317,7 +317,7 @@
       SetOnIncompatibleAction(OnIncompatibleAction::kIgnoreAndContinue,
                               "Screenshots not captured on this platform."),
       ScreenshotWebUi(kActiveTab, kApp, "ContextualTasksApp",
-                      /*baseline_cl=*/"7519825"));
+                      /*baseline_cl=*/"7620222"));
 }
 
 enum class TitleType { kNone, kShort, kLong };
diff --git a/chrome/test/data/webui/contextual_tasks/test_contextual_tasks_browser_proxy.ts b/chrome/test/data/webui/contextual_tasks/test_contextual_tasks_browser_proxy.ts
index fccced5..53a48cf9 100644
--- a/chrome/test/data/webui/contextual_tasks/test_contextual_tasks_browser_proxy.ts
+++ b/chrome/test/data/webui/contextual_tasks/test_contextual_tasks_browser_proxy.ts
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 
 import {PageCallbackRouter} from 'chrome://contextual-tasks/contextual_tasks.mojom-webui.js';
-import type {ComposeboxPosition, ContextInfo, PageHandlerInterface, PageInterface, PageRemote} from 'chrome://contextual-tasks/contextual_tasks.mojom-webui.js';
+import type {ComposeboxPosition, ContextInfo, IconType, PageHandlerInterface, PageInterface, PageRemote} from 'chrome://contextual-tasks/contextual_tasks.mojom-webui.js';
 import type {BrowserProxy} from 'chrome://contextual-tasks/contextual_tasks_browser_proxy.js';
 import type {PostMessageHandler} from 'chrome://contextual-tasks/post_message_handler.js';
 import type {PageHandler as ComposeboxPageHandler, PageHandlerFactory as ComposeboxPageHandlerFactory} from 'chrome://resources/cr_components/composebox/composebox.mojom-webui.js';
@@ -36,6 +36,7 @@
       'lockInput',
       'unlockInput',
       'injectInput',
+      'injectInputWithIcon',
       'removeInjectedInput',
     ]);
   }
@@ -131,6 +132,11 @@
     this.methodCalled('injectInput', title, thumbnail, fileToken);
   }
 
+  injectInputWithIcon(
+      title: string, iconId: IconType, fileToken: UnguessableToken) {
+    this.methodCalled('injectInputWithIcon', title, iconId, fileToken);
+  }
+
   removeInjectedInput(fileToken: UnguessableToken) {
     this.methodCalled('removeInjectedInput', fileToken);
   }
diff --git a/chrome/test/data/webui/cr_components/composebox/composebox_input_placeholder_test.ts b/chrome/test/data/webui/cr_components/composebox/composebox_input_placeholder_test.ts
index 5d5c14e5..49408a0 100644
--- a/chrome/test/data/webui/cr_components/composebox/composebox_input_placeholder_test.ts
+++ b/chrome/test/data/webui/cr_components/composebox/composebox_input_placeholder_test.ts
@@ -5,10 +5,11 @@
 import 'chrome://new-tab-page/strings.m.js';
 import 'chrome://resources/cr_components/composebox/composebox.js';
 
+import type {ComposeboxFile} from 'chrome://resources/cr_components/composebox/common.js';
 import type {ComposeboxElement} from 'chrome://resources/cr_components/composebox/composebox.js';
 import {PageCallbackRouter, PageHandlerRemote} from 'chrome://resources/cr_components/composebox/composebox.mojom-webui.js';
 import {ComposeboxProxyImpl} from 'chrome://resources/cr_components/composebox/composebox_proxy.js';
-import {ModelMode, ToolMode as ComposeboxToolMode} from 'chrome://resources/cr_components/composebox/composebox_query.mojom-webui.js';
+import {ContextUploadStatus, ModelMode, ToolMode as ComposeboxToolMode} from 'chrome://resources/cr_components/composebox/composebox_query.mojom-webui.js';
 import type {InputState} from 'chrome://resources/cr_components/composebox/composebox_query.mojom-webui.js';
 import {WindowProxy} from 'chrome://resources/cr_components/composebox/window_proxy.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
@@ -168,4 +169,90 @@
       assertEquals(defaultApiHint, composebox.$.input.placeholder);
     });
   });
+
+  test('MultipleFilesUpdatesPlaceholder', async () => {
+    loadTimeData.overrideValues({
+      composeboxHintTextAskAboutThese: 'Ask about these',
+    });
+    composebox.enableFileHint = true;
+    const token1 = {high: 0n, low: 1n} as any;
+    const token2 = {high: 0n, low: 2n} as any;
+    composebox.addFileContextForTesting({
+      type: 'image/png',
+      uuid: token1,
+      status: ContextUploadStatus.kNotUploaded,
+    } as ComposeboxFile);
+    composebox.addFileContextForTesting({
+      type: 'application/pdf',
+      uuid: token2,
+      status: ContextUploadStatus.kNotUploaded,
+    } as ComposeboxFile);
+    await composebox.updateComplete;
+
+    assertEquals('Ask about these', composebox.$.input.placeholder);
+  });
+
+  test('SingleTabFileUpdatesPlaceholder', async () => {
+    loadTimeData.overrideValues({
+      composeboxHintTextAskAboutThisTab: 'Ask about this tab',
+    });
+    composebox.enableFileHint = true;
+    const token = {high: 0n, low: 1n} as any;
+    composebox.addFileContextForTesting({
+      type: 'tab',
+      uuid: token,
+      status: ContextUploadStatus.kNotUploaded,
+    } as ComposeboxFile);
+    await composebox.updateComplete;
+
+    assertEquals('Ask about this tab', composebox.$.input.placeholder);
+  });
+
+  test('SingleImageFileUpdatesPlaceholder', async () => {
+    loadTimeData.overrideValues({
+      composeboxHintTextAskAboutThisImage: 'Ask about this image',
+    });
+    composebox.enableFileHint = true;
+    const token = {high: 0n, low: 1n} as any;
+    composebox.addFileContextForTesting({
+      type: 'image/png',
+      uuid: token,
+      status: ContextUploadStatus.kNotUploaded,
+    } as ComposeboxFile);
+    await composebox.updateComplete;
+
+    assertEquals('Ask about this image', composebox.$.input.placeholder);
+  });
+
+  test('SinglePdfFileUpdatesPlaceholder', async () => {
+    loadTimeData.overrideValues({
+      composeboxHintTextAskAboutThisDoc: 'Ask about this doc',
+    });
+    composebox.enableFileHint = true;
+    const token = {high: 0n, low: 1n} as any;
+    composebox.addFileContextForTesting({
+      type: 'application/pdf',
+      uuid: token,
+      status: ContextUploadStatus.kNotUploaded,
+    } as ComposeboxFile);
+    await composebox.updateComplete;
+
+    assertEquals('Ask about this doc', composebox.$.input.placeholder);
+  });
+
+  test('SingleUnknownFileUpdatesPlaceholder', async () => {
+    composebox.enableFileHint = true;
+    const token = {high: 0n, low: 1n} as any;
+    composebox.addFileContextForTesting({
+      type: 'unknown/type',
+      uuid: token,
+      status: ContextUploadStatus.kNotUploaded,
+    } as ComposeboxFile);
+    await composebox.updateComplete;
+
+    const placeholder = composebox.$.input.placeholder;
+    assertTrue(
+        !placeholder.includes('Ask about'),
+        `Placeholder '${placeholder}' should not include 'Ask about'`);
+  });
 });
diff --git a/chrome/test/webapps/OWNERS b/chrome/test/webapps/OWNERS
index ab78012af..1255a2c 100644
--- a/chrome/test/webapps/OWNERS
+++ b/chrome/test/webapps/OWNERS
@@ -1,4 +1 @@
-dmurph@chromium.org
-phillis@chromium.org
-msw@chromium.org
-dibyapal@chromium.org
+file://chrome/browser/web_applications/OWNERS
diff --git a/chromeos/CHROMEOS_LKGM b/chromeos/CHROMEOS_LKGM
index 694a7fb..47740c0 100644
--- a/chromeos/CHROMEOS_LKGM
+++ b/chromeos/CHROMEOS_LKGM
@@ -1 +1 @@
-16604.0.0-1075687
\ No newline at end of file
+16604.0.0-1075696
\ No newline at end of file
diff --git a/chromeos/ash/components/network/client_cert_resolver.h b/chromeos/ash/components/network/client_cert_resolver.h
index 7a58a5e1..9f7dd8c 100644
--- a/chromeos/ash/components/network/client_cert_resolver.h
+++ b/chromeos/ash/components/network/client_cert_resolver.h
@@ -15,6 +15,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
+#include "base/observer_list_types.h"
 #include "base/scoped_observation.h"
 #include "base/sequence_checker.h"
 #include "base/time/time.h"
@@ -46,7 +47,7 @@
       public NetworkCertLoader::Observer,
       public NetworkPolicyObserver {
  public:
-  class Observer {
+  class Observer : public base::CheckedObserver {
    public:
     Observer& operator=(const Observer&) = delete;
 
@@ -57,7 +58,7 @@
     virtual void ResolveRequestCompleted(bool network_properties_changed) = 0;
 
    protected:
-    virtual ~Observer() {}
+    ~Observer() override = default;
   };
 
   ClientCertResolver();
@@ -138,7 +139,7 @@
   // instead.
   base::Time Now() const;
 
-  base::ObserverList<Observer, true>::Unchecked observers_;
+  base::ObserverList<Observer, true> observers_;
 
   // Tracks which network configurations ClientCertResolver is aware of, to be
   // able to detect newly created networks for which certificate resolution may
diff --git a/chromeos/ash/components/network/network_configuration_handler.h b/chromeos/ash/components/network/network_configuration_handler.h
index 7aa002b..e153d10 100644
--- a/chromeos/ash/components/network/network_configuration_handler.h
+++ b/chromeos/ash/components/network/network_configuration_handler.h
@@ -246,7 +246,7 @@
   std::multimap<std::string, network_handler::ServiceResultCallback>
       configure_callbacks_;
 
-  base::ObserverList<NetworkConfigurationObserver, true>::Unchecked observers_;
+  base::ObserverList<NetworkConfigurationObserver, true> observers_;
 
   base::WeakPtrFactory<NetworkConfigurationHandler> weak_ptr_factory_{this};
 };
diff --git a/chromeos/ash/components/network/network_configuration_observer.h b/chromeos/ash/components/network/network_configuration_observer.h
index 0e183df..bc4d7d5d 100644
--- a/chromeos/ash/components/network/network_configuration_observer.h
+++ b/chromeos/ash/components/network/network_configuration_observer.h
@@ -8,12 +8,14 @@
 #include <string>
 
 #include "base/component_export.h"
+#include "base/observer_list_types.h"
 #include "base/values.h"
 
 namespace ash {
 
 // Observer class for network configuration events (remove only).
-class COMPONENT_EXPORT(CHROMEOS_NETWORK) NetworkConfigurationObserver {
+class COMPONENT_EXPORT(CHROMEOS_NETWORK) NetworkConfigurationObserver
+    : public base::CheckedObserver {
  public:
   NetworkConfigurationObserver& operator=(const NetworkConfigurationObserver&) =
       delete;
@@ -43,7 +45,7 @@
   virtual void OnShuttingDown();
 
  protected:
-  virtual ~NetworkConfigurationObserver();
+  ~NetworkConfigurationObserver() override;
 };
 
 }  // namespace ash
diff --git a/chromeos/ash/components/network/network_sms_handler.h b/chromeos/ash/components/network/network_sms_handler.h
index 3cbea9b..bedfd3e9 100644
--- a/chromeos/ash/components/network/network_sms_handler.h
+++ b/chromeos/ash/components/network/network_sms_handler.h
@@ -13,6 +13,7 @@
 #include "base/component_export.h"
 #include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
+#include "base/observer_list_types.h"
 #include "base/scoped_observation.h"
 #include "base/values.h"
 #include "chromeos/ash/components/dbus/shill/shill_property_changed_observer.h"
@@ -47,10 +48,8 @@
   static const char kTextKey[];
   static const char kTimestampKey[];
 
-  class Observer {
+  class Observer : public base::CheckedObserver {
    public:
-    virtual ~Observer() = default;
-
     // Called when a new message arrives. |message| contains the message which
     // is a dictionary value containing entries for kNumberKey, kTextKey, and
     // kTimestampKey.
@@ -59,6 +58,9 @@
     // Called when a new message arrives from a network with |guid|.
     virtual void MessageReceivedFromNetwork(const std::string& guid,
                                             const TextMessageData& message) {}
+
+   protected:
+    ~Observer() override = default;
   };
 
   NetworkSmsHandler(const NetworkSmsHandler&) = delete;
@@ -132,7 +134,7 @@
   // last active network accordingly.
   void OnActiveDeviceIccidChanged(const std::string& iccid);
 
-  base::ObserverList<Observer, true>::Unchecked observers_;
+  base::ObserverList<Observer, true> observers_;
   std::unique_ptr<NetworkSmsDeviceHandler> device_handler_;
   std::vector<base::DictValue> received_messages_;
   std::string cellular_device_path_;
diff --git a/chromeos/ash/components/proximity_auth/screenlock_bridge.h b/chromeos/ash/components/proximity_auth/screenlock_bridge.h
index d90e78a..b636368 100644
--- a/chromeos/ash/components/proximity_auth/screenlock_bridge.h
+++ b/chromeos/ash/components/proximity_auth/screenlock_bridge.h
@@ -11,6 +11,7 @@
 #include "base/lazy_instance.h"
 #include "base/memory/raw_ptr.h"
 #include "base/observer_list.h"
+#include "base/observer_list_types.h"
 #include "base/values.h"
 #include "chromeos/ash/components/proximity_auth/public/mojom/auth_type.mojom.h"
 #include "components/account_id/account_id.h"
@@ -66,7 +67,7 @@
     virtual ~LockHandler() {}
   };
 
-  class Observer {
+  class Observer : public base::CheckedObserver {
    public:
     // Invoked after the screen is locked.
     virtual void OnScreenDidLock() = 0;
@@ -78,7 +79,7 @@
     virtual void OnFocusedUserChanged(const AccountId& account_id) {}
 
    protected:
-    virtual ~Observer() {}
+    ~Observer() override = default;
   };
 
   static ScreenlockBridge* Get();
@@ -113,7 +114,7 @@
 
   // The last focused user's id.
   AccountId focused_account_id_;
-  base::ObserverList<Observer, true>::Unchecked observers_;
+  base::ObserverList<Observer, true> observers_;
 };
 
 }  // namespace proximity_auth
diff --git a/chromeos/ui/frame/frame_view_chromeos.cc b/chromeos/ui/frame/frame_view_chromeos.cc
index 2062c186..56388e77 100644
--- a/chromeos/ui/frame/frame_view_chromeos.cc
+++ b/chromeos/ui/frame/frame_view_chromeos.cc
@@ -20,7 +20,7 @@
 #include "ui/views/view.h"
 #include "ui/views/view_targeter.h"
 #include "ui/views/widget/widget.h"
-#include "ui/views/window/frame_view.h"
+#include "ui/views/window/native_frame_view.h"
 
 namespace chromeos {
 
@@ -61,7 +61,8 @@
 BEGIN_METADATA(FrameViewChromeOS, OverlayView)
 END_METADATA
 
-FrameViewChromeOS::FrameViewChromeOS(views::Widget* widget) : widget_(widget) {
+FrameViewChromeOS::FrameViewChromeOS(views::Widget* widget)
+    : views::NativeFrameView(widget) {
   DCHECK(widget_);
 
   auto header_view = std::make_unique<HeaderView>(widget_, this);
@@ -136,15 +137,6 @@
   return header_view_->GetFrameHeader()->GetAdjustedChildrenInZOrder(this);
 }
 
-gfx::Size FrameViewChromeOS::CalculatePreferredSize(
-    const views::SizeBounds& available_size) const {
-  gfx::Size pref = widget_->client_view()->GetPreferredSize(available_size);
-  gfx::Rect bounds(0, 0, pref.width(), pref.height());
-  return widget_->non_client_view()
-      ->GetWindowBoundsForClientBounds(bounds)
-      .size();
-}
-
 void FrameViewChromeOS::Layout(PassKey) {
   LayoutSuperclass<views::FrameView>(this);
   if (!GetFrameEnabled())
@@ -178,7 +170,7 @@
 }
 
 void FrameViewChromeOS::OnThemeChanged() {
-  FrameView::OnThemeChanged();
+  views::NativeFrameView::OnThemeChanged();
   UpdateDefaultFrameColors();
 }
 
diff --git a/chromeos/ui/frame/frame_view_chromeos.h b/chromeos/ui/frame/frame_view_chromeos.h
index adc190f..33eb5c09 100644
--- a/chromeos/ui/frame/frame_view_chromeos.h
+++ b/chromeos/ui/frame/frame_view_chromeos.h
@@ -14,7 +14,7 @@
 #include "ui/views/view.h"
 #include "ui/views/view_targeter.h"
 #include "ui/views/widget/widget.h"
-#include "ui/views/window/frame_view.h"
+#include "ui/views/window/native_frame_view.h"
 
 namespace display {
 class DisplayObserver;
@@ -24,9 +24,9 @@
 
 namespace chromeos {
 
-class FrameViewChromeOS : public views::FrameView,
+class FrameViewChromeOS : public views::NativeFrameView,
                           public display::DisplayObserver {
-  METADATA_HEADER(FrameViewChromeOS, views::FrameView)
+  METADATA_HEADER(FrameViewChromeOS, views::NativeFrameView)
 
  public:
   explicit FrameViewChromeOS(views::Widget* widget);
@@ -34,7 +34,7 @@
   FrameViewChromeOS& operator=(const FrameViewChromeOS&) = delete;
   ~FrameViewChromeOS() override;
 
-  // views::FrameView:
+  // views::NativeFrameView:
   gfx::Rect GetBoundsForClientView() const override;
   gfx::Rect GetWindowBoundsForClientBounds(
       const gfx::Rect& client_bounds) const override;
@@ -44,8 +44,6 @@
   void UpdateWindowTitle() override;
   void SizeConstraintsChanged() override;
   views::View::Views GetChildrenInZOrder() override;
-  gfx::Size CalculatePreferredSize(
-      const views::SizeBounds& available_size) const override;
   void Layout(PassKey) override;
   gfx::Size GetMinimumSize() const override;
   gfx::Size GetMaximumSize() const override;
@@ -68,7 +66,6 @@
   // views::FrameView:
   bool DoesIntersectRect(const views::View* target,
                          const gfx::Rect& rect) const override;
-  const raw_ptr<views::Widget> widget_;
 
   // View which contains the title and window controls.
   raw_ptr<HeaderView> header_view_ = nullptr;
diff --git a/clank b/clank
index 3d95afd..cb2add8 160000
--- a/clank
+++ b/clank
@@ -1 +1 @@
-Subproject commit 3d95afd50da9d85ff51c766764cdc068ad987408
+Subproject commit cb2add85959c63a84544feea0f82b211261ab582
diff --git a/components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge.cc b/components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge.cc
index 52dac810..8b30fc3 100644
--- a/components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge.cc
+++ b/components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge.cc
@@ -84,6 +84,11 @@
       std::move(batch),
       base::BindOnce(&AccessibilityAnnotationSyncBridge::OnDataTypeStoreCommit,
                      weak_ptr_factory_.GetWeakPtr()));
+  if (!entity_changes.empty()) {
+    for (Observer& observer : observers_) {
+      observer.OnAccessibilityAnnotationChanged();
+    }
+  }
   return std::nullopt;
 }
 
diff --git a/components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge.h b/components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge.h
index 0ec24d89..6ddde3b 100644
--- a/components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge.h
+++ b/components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge.h
@@ -32,12 +32,16 @@
    public:
     Observer() = default;
     ~Observer() override = default;
-    // TODO(crbug.com/486856790): Add observer methods for
-    // adding/removing/updating annotations.
+
+    // Invoked when the accessibility annotations are changed in the
+    // local DataTypeStore by the sync bridge.
+    // TODO(crbug.com/486856790): Consider more granular notifications once
+    // incremental sync is supported.
+    virtual void OnAccessibilityAnnotationChanged() {}
 
     // Invoked when the store containing the accessibility annotations is
     // loaded.
-    virtual void OnAccessibilityAnnotationSyncBridgeLoaded() = 0;
+    virtual void OnAccessibilityAnnotationSyncBridgeLoaded() {}
   };
 
   explicit AccessibilityAnnotationSyncBridge(
diff --git a/components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge_unittest.cc b/components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge_unittest.cc
index 9898581f..f6b831e 100644
--- a/components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge_unittest.cc
+++ b/components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge_unittest.cc
@@ -28,6 +28,8 @@
   ~MockObserver() override = default;
 
   MOCK_METHOD(void, OnAccessibilityAnnotationSyncBridgeLoaded, (), (override));
+
+  MOCK_METHOD(void, OnAccessibilityAnnotationChanged, (), (override));
 };
 
 class AccessibilityAnnotationSyncBridgeTest : public testing::Test {
@@ -136,6 +138,32 @@
   EXPECT_EQ(annotations[0].id(), "1");
 }
 
+TEST_F(AccessibilityAnnotationSyncBridgeTest,
+       ObserverAddedOnIncrementalChanges) {
+  testing::StrictMock<MockObserver> observer;
+  bridge()->AddObserver(&observer);
+
+  EXPECT_CALL(observer, OnAccessibilityAnnotationChanged());
+
+  ASSERT_TRUE(AddAccessibilityAnnotation("1"));
+
+  bridge()->RemoveObserver(&observer);
+}
+
+TEST_F(AccessibilityAnnotationSyncBridgeTest,
+       ObserverRemovedOnIncrementalChanges) {
+  ASSERT_TRUE(AddAccessibilityAnnotation("1"));
+
+  testing::StrictMock<MockObserver> observer;
+  bridge()->AddObserver(&observer);
+
+  EXPECT_CALL(observer, OnAccessibilityAnnotationChanged());
+
+  ASSERT_TRUE(DeleteAccessibilityAnnotation("1"));
+
+  bridge()->RemoveObserver(&observer);
+}
+
 }  // namespace
 
 }  // namespace accessibility_annotator
diff --git a/components/accessibility_annotator/core/storage/accessibility_annotator_backend.cc b/components/accessibility_annotator/core/storage/accessibility_annotator_backend.cc
index 1d38f9dd..7fa3992d 100644
--- a/components/accessibility_annotator/core/storage/accessibility_annotator_backend.cc
+++ b/components/accessibility_annotator/core/storage/accessibility_annotator_backend.cc
@@ -28,6 +28,7 @@
   accessibility_annotation_sync_bridge_ =
       std::make_unique<AccessibilityAnnotationSyncBridge>(
           std::move(processor), data_type_store_factory);
+  sync_bridge_observation_.Observe(accessibility_annotation_sync_bridge_.get());
 }
 
 AccessibilityAnnotatorBackend::~AccessibilityAnnotatorBackend() = default;
@@ -47,4 +48,13 @@
       ->GetControllerDelegate();
 }
 
+void AccessibilityAnnotatorBackend::OnAccessibilityAnnotationChanged() {
+  // TODO(crbug.com/486856790): Implement logic to handle changed annotations.
+}
+
+void AccessibilityAnnotatorBackend::
+    OnAccessibilityAnnotationSyncBridgeLoaded() {
+  // TODO(crbug.com/486856790): Implement logic to handle sync bridge loaded.
+}
+
 }  // namespace accessibility_annotator
diff --git a/components/accessibility_annotator/core/storage/accessibility_annotator_backend.h b/components/accessibility_annotator/core/storage/accessibility_annotator_backend.h
index e2ae6dc..2468bf2 100644
--- a/components/accessibility_annotator/core/storage/accessibility_annotator_backend.h
+++ b/components/accessibility_annotator/core/storage/accessibility_annotator_backend.h
@@ -9,7 +9,9 @@
 
 #include "base/files/file_path.h"
 #include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
 #include "base/threading/sequence_bound.h"
+#include "components/accessibility_annotator/core/storage/accessibility_annotation_sync_bridge.h"
 #include "components/keyed_service/core/keyed_service.h"
 #include "components/sync/model/data_type_store.h"
 
@@ -23,10 +25,11 @@
 
 namespace accessibility_annotator {
 
-class AccessibilityAnnotationSyncBridge;
 class AccessibilityAnnotatorDatabase;
 
-class AccessibilityAnnotatorBackend : public KeyedService {
+class AccessibilityAnnotatorBackend
+    : public KeyedService,
+      public AccessibilityAnnotationSyncBridge::Observer {
  public:
   AccessibilityAnnotatorBackend(
       version_info::Channel channel,
@@ -47,10 +50,18 @@
   base::WeakPtr<syncer::DataTypeControllerDelegate>
   GetAccessibilityAnnotationControllerDelegate();
 
+  // AccessibilityAnnotationSyncBridge::Observer implementation.
+  void OnAccessibilityAnnotationChanged() override;
+  void OnAccessibilityAnnotationSyncBridgeLoaded() override;
+
  private:
   base::SequenceBound<AccessibilityAnnotatorDatabase> db_;
   std::unique_ptr<AccessibilityAnnotationSyncBridge>
       accessibility_annotation_sync_bridge_;
+
+  base::ScopedObservation<AccessibilityAnnotationSyncBridge,
+                          AccessibilityAnnotationSyncBridge::Observer>
+      sync_bridge_observation_{this};
 };
 
 }  // namespace accessibility_annotator
diff --git a/components/autofill/core/browser/data_model/autofill_ai/entity_instance.cc b/components/autofill/core/browser/data_model/autofill_ai/entity_instance.cc
index 70b0ff8..3a1ae12b 100644
--- a/components/autofill/core/browser/data_model/autofill_ai/entity_instance.cc
+++ b/components/autofill/core/browser/data_model/autofill_ai/entity_instance.cc
@@ -147,6 +147,8 @@
       return false;
     case EntityInstance::RecordType::kServerWallet:
       return true;
+    case EntityInstance::RecordType::kAccessibilityAnnotator:
+      return false;
   }
   NOTREACHED();
 }
@@ -381,6 +383,9 @@
     case EntityInstance::RecordType::kServerWallet:
       os << "kServerWallet" << std::endl;
       break;
+    case EntityInstance::RecordType::kAccessibilityAnnotator:
+      os << "kAccessibilityAnnotator" << std::endl;
+      break;
   }
   return os;
 }
@@ -570,6 +575,8 @@
       return false;
     case RecordType::kServerWallet:
       return true;
+    case RecordType::kAccessibilityAnnotator:
+      return false;
   }
   NOTREACHED();
 }
@@ -692,6 +699,7 @@
                               EntityInstance::RecordType record_type) {
   switch (record_type) {
     case EntityInstance::RecordType::kLocal:
+    case EntityInstance::RecordType::kAccessibilityAnnotator:
       return false;
     case EntityInstance::RecordType::kServerWallet:
       break;
diff --git a/components/autofill/core/browser/data_model/autofill_ai/entity_instance.h b/components/autofill/core/browser/data_model/autofill_ai/entity_instance.h
index 6f858093..5646c94 100644
--- a/components/autofill/core/browser/data_model/autofill_ai/entity_instance.h
+++ b/components/autofill/core/browser/data_model/autofill_ai/entity_instance.h
@@ -258,7 +258,9 @@
     // copy. Changes happening locally or on the Wallet server are synced among
     // all local storages sharing this entity.
     kServerWallet = 1,
-    kMaxValue = kServerWallet,
+    // The entity provided by Accessibility Annotator.
+    kAccessibilityAnnotator = 2,
+    kMaxValue = kAccessibilityAnnotator,
   };
 
   // `attributes` must be non-empty and their type must be identical to `type`.
diff --git a/components/autofill/core/browser/form_import/form_data_importer_unittest.cc b/components/autofill/core/browser/form_import/form_data_importer_unittest.cc
index fcfb2e3..d038101 100644
--- a/components/autofill/core/browser/form_import/form_data_importer_unittest.cc
+++ b/components/autofill/core/browser/form_import/form_data_importer_unittest.cc
@@ -773,6 +773,8 @@
   }
 
  private:
+  base::test::ScopedFeatureList scoped_feature_list_{
+      features::kAutofillUseINAddressModel};
   base::test::SingleThreadTaskEnvironment task_environment_{
       base::test::SingleThreadTaskEnvironment::MainThreadType::UI,
       base::test::TaskEnvironment::TimeSource::MOCK_TIME};
@@ -780,8 +782,6 @@
   std::unique_ptr<PrefService> prefs_;
   syncer::TestSyncService sync_service_;
   TestAutofillClient autofill_client_;
-  base::test::ScopedFeatureList scoped_feature_list_{
-      features::kAutofillUseINAddressModel};
 };
 
 // Tests that the country is not complemented if a country is part of the form.
diff --git a/components/autofill/core/browser/integrators/autofill_ai/autofill_ai_manager.cc b/components/autofill/core/browser/integrators/autofill_ai/autofill_ai_manager.cc
index 07112ff..196eb6ba 100644
--- a/components/autofill/core/browser/integrators/autofill_ai/autofill_ai_manager.cc
+++ b/components/autofill/core/browser/integrators/autofill_ai/autofill_ai_manager.cc
@@ -739,6 +739,11 @@
       case EntityInstance::RecordType::kServerWallet:
         saved_server_entities.push_back(&entity);
         break;
+      case EntityInstance::RecordType::kAccessibilityAnnotator:
+        // kAccessibilityAnnotator entities are linked to a database in the
+        // Accessibility Annotator component. They must not be saved to the
+        // server to ensure they can be deleted if the source is removed.
+        break;
     }
   }
 
diff --git a/components/autofill/core/browser/integrators/autofill_ai/metrics/autofill_ai_metrics.cc b/components/autofill/core/browser/integrators/autofill_ai/metrics/autofill_ai_metrics.cc
index 7358e4d..a3b681a 100644
--- a/components/autofill/core/browser/integrators/autofill_ai/metrics/autofill_ai_metrics.cc
+++ b/components/autofill/core/browser/integrators/autofill_ai/metrics/autofill_ai_metrics.cc
@@ -128,6 +128,8 @@
       return "Local";
     case EntityInstance::RecordType::kServerWallet:
       return "ServerWallet";
+    case EntityInstance::RecordType::kAccessibilityAnnotator:
+      return "AccessibilityAnnotator";
   }
   NOTREACHED();
 }
diff --git a/components/autofill/core/browser/integrators/autofill_ai/metrics/autofill_ai_ukm_logger.cc b/components/autofill/core/browser/integrators/autofill_ai/metrics/autofill_ai_ukm_logger.cc
index ce8eb69f..ef090cd 100644
--- a/components/autofill/core/browser/integrators/autofill_ai/metrics/autofill_ai_ukm_logger.cc
+++ b/components/autofill/core/browser/integrators/autofill_ai/metrics/autofill_ai_ukm_logger.cc
@@ -164,6 +164,8 @@
       return optimization_guide::proto::AUTOFILL_AI_ENTITY_STORAGE_TYPE_LOCAL;
     case EntityInstance::RecordType::kServerWallet:
       return optimization_guide::proto::AUTOFILL_AI_ENTITY_STORAGE_TYPE_WALLET;
+    case EntityInstance::RecordType::kAccessibilityAnnotator:
+      return optimization_guide::proto::AUTOFILL_AI_ENTITY_STORAGE_TYPE_UNKNOWN;
   }
 }
 
diff --git a/components/autofill/core/browser/suggestions/autofill_ai/autofill_ai_suggestion_generator.cc b/components/autofill/core/browser/suggestions/autofill_ai/autofill_ai_suggestion_generator.cc
index 42b6fd6..aa17308e 100644
--- a/components/autofill/core/browser/suggestions/autofill_ai/autofill_ai_suggestion_generator.cc
+++ b/components/autofill/core/browser/suggestions/autofill_ai/autofill_ai_suggestion_generator.cc
@@ -314,6 +314,7 @@
       case EntityInstance::RecordType::kServerWallet:
         return true;
       case EntityInstance::RecordType::kLocal:
+      case EntityInstance::RecordType::kAccessibilityAnnotator:
         return false;
     }
     NOTREACHED();
diff --git a/components/autofill/core/browser/webdata/autofill_ai/entity_table.cc b/components/autofill/core/browser/webdata/autofill_ai/entity_table.cc
index dcdd0ee..b9a78d17 100644
--- a/components/autofill/core/browser/webdata/autofill_ai/entity_table.cc
+++ b/components/autofill/core/browser/webdata/autofill_ai/entity_table.cc
@@ -172,6 +172,7 @@
               static_cast<EntityInstance::RecordType>(underlying_record_type)) {
     case EntityInstance::RecordType::kLocal:
     case EntityInstance::RecordType::kServerWallet:
+    case EntityInstance::RecordType::kAccessibilityAnnotator:
       return record_type;
   }
   return std::nullopt;
diff --git a/components/autofill/core/browser/webdata/valuables/valuable_sync_bridge.cc b/components/autofill/core/browser/webdata/valuables/valuable_sync_bridge.cc
index a8f9677..d7c9afe 100644
--- a/components/autofill/core/browser/webdata/valuables/valuable_sync_bridge.cc
+++ b/components/autofill/core/browser/webdata/valuables/valuable_sync_bridge.cc
@@ -91,6 +91,9 @@
       return !IsMaskedStorageSupported(
           change.data_model().type(),
           EntityInstance::RecordType::kServerWallet);
+    case EntityInstance::RecordType::kAccessibilityAnnotator:
+      // Accessibility annotator entities are not uploaded as AUTOFILL_VALUABLE.
+      return false;
   }
   NOTREACHED();
 }
diff --git a/components/autofill_strings.grdp b/components/autofill_strings.grdp
index 916ee95..e3534b3 100644
--- a/components/autofill_strings.grdp
+++ b/components/autofill_strings.grdp
@@ -1120,7 +1120,7 @@
     Google Wallet
   </message>
   <!-- Prompt strings used on Desktop -->
-  <if expr="not is_android and not is_ios">
+  <if expr="not is_android">
     <message name="IDS_AUTOFILL_AI_SAVE_DRIVERS_LICENSE_ENTITY_DIALOG_TITLE" desc="The title of a dialog that prompts the user to save information they just filled in on a form. For example, the user is renting a car and just filled in info about their driver’s license. When they click Submit, we offer to save the data in Chrome. Depending on the form and what Chrome recognizes, there might be just one or several pieces of data saved at this moment. This is a similar moment to saving a password or credit card with Chrome.">
       Save driver's license?
     </message>
diff --git a/components/bookmarks/browser/bookmark_model.cc b/components/bookmarks/browser/bookmark_model.cc
index abd320e..94ace75 100644
--- a/components/bookmarks/browser/bookmark_model.cc
+++ b/components/bookmarks/browser/bookmark_model.cc
@@ -307,6 +307,22 @@
       encryptor, local_or_syncable_file_path,
       encrypted_local_or_syncable_file_path, account_file_path,
       encrypted_account_file_path, client_->GetLoadManagedNodeCallback(),
+      base::BindOnce(
+          [](/*arg_name=*/base::WeakPtr<BookmarkModel> model) {
+            if (!model) {
+              return;
+            }
+            model->local_or_syncable_store_->SaveBookmarksToSecondaryFile();
+          },
+          AsWeakPtr()),
+      base::BindOnce(
+          [](/*arg_name=*/base::WeakPtr<BookmarkModel> model) {
+            if (!model) {
+              return;
+            }
+            model->account_store_->SaveBookmarksToSecondaryFile();
+          },
+          AsWeakPtr()),
       base::BindOnce(&BookmarkModel::DoneLoading, AsWeakPtr()));
 }
 
diff --git a/components/bookmarks/browser/bookmark_model_unittest.cc b/components/bookmarks/browser/bookmark_model_unittest.cc
index cef4c0c0..0faa05a 100644
--- a/components/bookmarks/browser/bookmark_model_unittest.cc
+++ b/components/bookmarks/browser/bookmark_model_unittest.cc
@@ -3442,6 +3442,55 @@
       /*expected_count=*/1);
 }
 
+TEST(BookmarkModelEncryptedStorageTest, EncryptedBookmarksFileCreatedOnLoad) {
+  base::test::ScopedFeatureList features{
+      switches::kSyncEnableBookmarksInTransportMode};
+
+  base::HistogramTester histogram_tester;
+  base::ScopedTempDir tmp_dir;
+  ASSERT_TRUE(tmp_dir.CreateUniqueTempDir());
+  base::test::TaskEnvironment task_environment{
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+  auto model =
+      std::make_unique<BookmarkModel>(std::make_unique<TestBookmarkClient>());
+  model->Load(tmp_dir.GetPath());
+  test::WaitForBookmarkModelToLoad(model.get());
+
+  // Create local-or-syncable bookmarks file.
+  model->AddURL(model->bookmark_bar_node(), 0, u"Foo", GURL("http://foo.com"));
+  task_environment.FastForwardUntilNoTasksRemain();
+  // Create account bookmarks file.
+  model->CreateAccountPermanentFolders();
+  // Only clear-text files should be created.
+  task_environment.FastForwardUntilNoTasksRemain();
+  ASSERT_TRUE(base::PathExists(
+      tmp_dir.GetPath().Append(kLocalOrSyncableBookmarksFileName)));
+  ASSERT_TRUE(
+      base::PathExists(tmp_dir.GetPath().Append(kAccountBookmarksFileName)));
+  ASSERT_FALSE(base::PathExists(
+      tmp_dir.GetPath().Append(kEncryptedLocalOrSyncableBookmarksFileName)));
+  ASSERT_FALSE(base::PathExists(
+      tmp_dir.GetPath().Append(kEncryptedAccountBookmarksFileName)));
+
+  base::test::ScopedFeatureList encryption_features;
+  test::InitFeaturesForBookmarkTestEncryptionStage(
+      encryption_features, BookmarkEncryptionStage::kWriteBothReadOnlyClear);
+  model =
+      std::make_unique<BookmarkModel>(std::make_unique<TestBookmarkClient>());
+  model->Load(tmp_dir.GetPath());
+  test::WaitForBookmarkModelToLoad(model.get());
+  task_environment.FastForwardUntilNoTasksRemain();
+  // Both clear-text and encrypted files should be created.
+  AssertSameFileContent(
+      tmp_dir.GetPath().Append(kLocalOrSyncableBookmarksFileName),
+      tmp_dir.GetPath().Append(kEncryptedLocalOrSyncableBookmarksFileName),
+      model->client());
+  AssertSameFileContent(
+      tmp_dir.GetPath().Append(kAccountBookmarksFileName),
+      tmp_dir.GetPath().Append(kEncryptedAccountBookmarksFileName),
+      model->client());
+}
+
 TEST(BookmarkNodeTest, NodeMetaInfo) {
   BookmarkNode node(/*id=*/0, base::Uuid::GenerateRandomV4(), GURL());
   EXPECT_FALSE(node.GetMetaInfoMap());
diff --git a/components/bookmarks/browser/bookmark_storage.cc b/components/bookmarks/browser/bookmark_storage.cc
index 6663a89..7f3b8d0 100644
--- a/components/bookmarks/browser/bookmark_storage.cc
+++ b/components/bookmarks/browser/bookmark_storage.cc
@@ -91,6 +91,31 @@
   NOTREACHED();
 }
 
+void SaveDictionaryToSecondaryFile(
+    base::DictValue value,
+    scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
+        encryptor,
+    const base::FilePath file_path) {
+  // TODO(crbug.com/435317726): Add metrics to measure the success/failure and
+  // impact on write duration of encrypting the bookmarks file.
+  CHECK(encryptor);
+  std::string json_content;
+  if (!base::JSONWriter::WriteWithOptions(
+          value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json_content)) {
+    return;
+  }
+  std::string encrypted_json_content;
+  if (!encryptor->data.EncryptString(json_content, &encrypted_json_content)) {
+    return;
+  }
+
+  if (!base::ImportantFileWriter::WriteFileAtomically(
+          file_path, std::move(encrypted_json_content),
+          kBookmarkStorageEncryptedHistogramSuffix)) {
+    return;
+  }
+}
+
 }  // namespace
 
 // static
@@ -144,13 +169,14 @@
 base::ImportantFileWriter::BackgroundDataProducerCallback
 BookmarkStorage::GetSerializedDataProducerForBackgroundSequence() {
   base::DictValue value = EncodeModelToDict(model_, permanent_node_selection_);
-
   return base::BindOnce(
       [](base::DictValue value,
          scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
              encryptor,
-         const std::optional<base::FilePath> encrypted_file_path)
+         const base::FilePath encrypted_file_path)
           -> std::optional<std::string> {
+        // TODO(crbug.com/435317726): Add metrics to measure the success/failure
+        // and impact on write duration of encrypting the bookmarks file.
         std::string output;
         if (!base::JSONWriter::WriteWithOptions(
                 value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &output)) {
@@ -159,7 +185,6 @@
 
         if (ShouldWriteEncryptedBookmarksToDisk()) {
           CHECK(encryptor);
-          CHECK(encrypted_file_path);
           std::string encrypted;
           if (encryptor->data.EncryptString(output, &encrypted)) {
             // Also write the encrypted data to disk. Make sure this second
@@ -169,7 +194,7 @@
                 base::BindOnce(
                     base::IgnoreResult(
                         &base::ImportantFileWriter::WriteFileAtomically),
-                    encrypted_file_path.value(), std::move(encrypted),
+                    encrypted_file_path, std::move(encrypted),
                     kBookmarkStorageEncryptedHistogramSuffix));
           }
         }
@@ -193,4 +218,14 @@
   }
 }
 
+void BookmarkStorage::SaveBookmarksToSecondaryFile() {
+  CHECK(ShouldWriteEncryptedBookmarksToDisk());
+  CHECK(encryptor_);
+  base::DictValue value = EncodeModelToDict(model_, permanent_node_selection_);
+  backend_task_runner_->PostTask(
+      FROM_HERE,
+      base::BindOnce(&SaveDictionaryToSecondaryFile, std::move(value),
+                     encryptor_, encrypted_file_path_));
+}
+
 }  // namespace bookmarks
diff --git a/components/bookmarks/browser/bookmark_storage.h b/components/bookmarks/browser/bookmark_storage.h
index a86c114..18e47135 100644
--- a/components/bookmarks/browser/bookmark_storage.h
+++ b/components/bookmarks/browser/bookmark_storage.h
@@ -16,6 +16,7 @@
 #include "base/time/time.h"
 #include "components/bookmarks/browser/bookmark_node.h"
 #include "components/bookmarks/browser/titled_url_index.h"
+#include "components/bookmarks/common/bookmark_constants.h"
 #include "components/os_crypt/async/common/encryptor.h"
 
 namespace base {
@@ -85,6 +86,20 @@
   // If there is a pending write, performs it immediately.
   void SaveNowIfScheduledForTesting();
 
+  // Saves the bookmarks to the secondary file on disk right away.
+  //
+  // While transitioning from unencrypted to encrypted bookmarks, bookmarks will
+  // be saved in two files, the primary file used as source of truth and the
+  // secondary one used for verification or backup. In the first stage of the
+  // encryption ramp-up where we write both files but only read the unencrypted
+  // file to load the data, the unencrypted file will be the primary file. In
+  // following stages, the encrypted file will be the primary file
+  // (see crbug.com/435317726).
+  //
+  // The primary bookmarks file will not be touched. This write operation is
+  // scheduled on the backend task runner.
+  void SaveBookmarksToSecondaryFile();
+
  private:
   // The state of the bookmark file backup. We lazily backup this file in order
   // to reduce disk writes until absolutely necessary. Will also leave the
diff --git a/components/bookmarks/browser/bookmark_storage_unittest.cc b/components/bookmarks/browser/bookmark_storage_unittest.cc
index 459d96c..d72eeb7 100644
--- a/components/bookmarks/browser/bookmark_storage_unittest.cc
+++ b/components/bookmarks/browser/bookmark_storage_unittest.cc
@@ -368,4 +368,47 @@
   EXPECT_FALSE(base::PathExists(encrypted_backup_file_path));
 }
 
+TEST(BookmarkStorageTest,
+     ShouldOnlySaveEncryptedLocalOrSyncableBookmarksRightAway) {
+  base::test::ScopedFeatureList features;
+  test::InitFeaturesForBookmarkTestEncryptionStage(
+      features, BookmarkEncryptionStage::kWriteBothReadOnlyClear);
+  base::HistogramTester histogram_tester;
+  std::unique_ptr<BookmarkModel> model = CreateModelWithOneBookmark();
+
+  const base::FilePath bookmarks_file_path =
+      GetTestBookmarksFileNameInNewTempDir();
+  const base::FilePath encrypted_bookmarks_file_path =
+      GetTestEncryptedBookmarksFileNameInNewTempDir();
+
+  scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
+      encryptor = base::MakeRefCounted<
+          base::RefCountedData<const os_crypt_async::Encryptor>>(
+          std::in_place, os_crypt_async::GetTestEncryptorForTesting());
+  base::test::TaskEnvironment task_environment{
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+  BookmarkStorage storage(
+      model.get(), BookmarkStorage::kSelectLocalOrSyncableNodes, encryptor,
+      bookmarks_file_path, encrypted_bookmarks_file_path);
+
+  storage.SaveBookmarksToSecondaryFile();
+  // No impact on the unencrypted bookmarks file.
+  EXPECT_FALSE(storage.HasScheduledSaveForTesting());
+  task_environment.FastForwardUntilNoTasksRemain();
+
+  ASSERT_FALSE(base::PathExists(bookmarks_file_path));
+  std::optional<base::DictValue> decrypted_file_content =
+      ReadEncryptedFileToDict(encrypted_bookmarks_file_path, encryptor->data);
+  ASSERT_TRUE(decrypted_file_content.has_value());
+  BookmarkCodec codec;
+  base::DictValue expected_file_content = codec.Encode(
+      model->bookmark_bar_node(), model->other_node(), model->mobile_node(),
+      model->client()->EncodeLocalOrSyncableBookmarkSyncMetadata());
+  EXPECT_EQ(expected_file_content, *decrypted_file_content);
+  histogram_tester.ExpectTotalCount(
+      "ImportantFile.WriteDuration.BookmarkStorage", 0);
+  histogram_tester.ExpectTotalCount(
+      "ImportantFile.WriteDuration.BookmarkStorageEncrypted", 1);
+}
+
 }  // namespace bookmarks
diff --git a/components/bookmarks/browser/model_loader.cc b/components/bookmarks/browser/model_loader.cc
index df63efb4..58e2698 100644
--- a/components/bookmarks/browser/model_loader.cc
+++ b/components/bookmarks/browser/model_loader.cc
@@ -14,6 +14,7 @@
 #include "base/json/json_string_value_serializer.h"
 #include "base/numerics/clamped_math.h"
 #include "base/synchronization/waitable_event.h"
+#include "base/task/bind_post_task.h"
 #include "base/task/task_traits.h"
 #include "base/task/thread_pool.h"
 #include "components/bookmarks/browser/bookmark_codec.h"
@@ -92,7 +93,8 @@
     scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
         encryptor,
     const base::FilePath encrypted_file_path,
-    metrics::StorageFileForUma storage_file_for_uma) {
+    metrics::StorageFileForUma storage_file_for_uma,
+    base::OnceClosure save_bookmarks_to_secondary_file_callback) {
   base::expected<std::string, metrics::BookmarksFileLoadResult>
       encrypted_json_string =
           ReadFile(encryptor, encrypted_file_path, storage_file_for_uma);
@@ -100,13 +102,16 @@
       storage_file_for_uma, metrics::EncryptionTypeForUma::kEncrypted,
       encrypted_json_string.error_or(
           metrics::BookmarksFileLoadResult::kSuccess));
-  // TODO(crbug.com/435317726): If comparison isn't successful, we
-  // should save the encrypted file to disk again.
+  bool file_matches = false;
   if (encrypted_json_string.has_value()) {
     const size_t encrypted_json_string_hash =
         base::FastHash(encrypted_json_string.value());
-    metrics::RecordEncryptedBookmarksFileMatchesResult(
-        storage_file_for_uma, encrypted_json_string_hash == json_string_hash);
+    file_matches = encrypted_json_string_hash == json_string_hash;
+    metrics::RecordEncryptedBookmarksFileMatchesResult(storage_file_for_uma,
+                                                       file_matches);
+  }
+  if (!file_matches) {
+    std::move(save_bookmarks_to_secondary_file_callback).Run();
   }
 }
 
@@ -115,7 +120,8 @@
     const scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
         encryptor,
     const base::FilePath& encrypted_file_path,
-    metrics::StorageFileForUma storage_file_for_umage) {
+    metrics::StorageFileForUma storage_file_for_uma,
+    base::OnceClosure save_bookmarks_to_secondary_file_callback) {
   if (!ShouldVerifyEncryptedBookmarksDataOnLoad()) {
     return;
   }
@@ -128,7 +134,8 @@
       FROM_HERE,
       base::BindOnce(&ReadEncryptedDataAndVerifyHashOnBackgroundSequence,
                      json_string_hash, encryptor, encrypted_file_path,
-                     storage_file_for_umage));
+                     storage_file_for_uma,
+                     std::move(save_bookmarks_to_secondary_file_callback)));
 }
 
 std::unique_ptr<BookmarkLoadDetails> LoadBookmarks(
@@ -137,7 +144,9 @@
     const base::FilePath& local_or_syncable_file_path,
     const base::FilePath& encrypted_local_or_syncable_file_path,
     const base::FilePath& account_file_path,
-    const base::FilePath& encrypted_account_file_path) {
+    const base::FilePath& encrypted_account_file_path,
+    base::OnceClosure save_local_or_syncable_secondary_file_callback,
+    base::OnceClosure save_account_secondary_file_callback) {
   auto details = std::make_unique<BookmarkLoadDetails>();
 
   std::set<int64_t> ids_assigned_to_account_nodes;
@@ -207,7 +216,8 @@
           metrics::BookmarksFileLoadResult::kSuccess);
       MaybeScheduleReadEncryptedDataAndVerifyHash(
           json_string.value(), encryptor, encrypted_account_file_path,
-          metrics::StorageFileForUma::kAccount);
+          metrics::StorageFileForUma::kAccount,
+          std::move(save_account_secondary_file_callback));
     } else {
       // In the failure case, it is still possible that sync metadata was
       // decoded, which includes legit scenarios like sync metadata indicating
@@ -260,7 +270,8 @@
           metrics::BookmarksFileLoadResult::kSuccess);
       MaybeScheduleReadEncryptedDataAndVerifyHash(
           json_string.value(), encryptor, encrypted_local_or_syncable_file_path,
-          metrics::StorageFileForUma::kLocalOrSyncable);
+          metrics::StorageFileForUma::kLocalOrSyncable,
+          std::move(save_local_or_syncable_secondary_file_callback));
     } else {
       metrics::RecordBookmarksFileLoadResult(
           metrics::StorageFileForUma::kLocalOrSyncable,
@@ -357,6 +368,8 @@
     const base::FilePath& account_file_path,
     const base::FilePath& encrypted_account_file_path,
     LoadManagedNodeCallback load_managed_node_callback,
+    base::OnceClosure save_local_or_syncable_secondary_file_callback,
+    base::OnceClosure save_account_secondary_file_callback,
     LoadCallback callback) {
   CHECK(!local_or_syncable_file_path.empty());
   // Note: base::MakeRefCounted is not available here, as ModelLoader's
@@ -366,14 +379,23 @@
       base::ThreadPool::CreateSequencedTaskRunner(
           {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
            base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN});
+  auto save_local_or_syncable_secondary_file_callback_on_main_sequence =
+      base::BindPostTaskToCurrentDefault(
+          std::move(save_local_or_syncable_secondary_file_callback));
+  auto save_account_secondary_file_callback_on_main_sequence =
+      base::BindPostTaskToCurrentDefault(
+          std::move(save_account_secondary_file_callback));
 
   model_loader->backend_task_runner_->PostTaskAndReplyWithResult(
       FROM_HERE,
-      base::BindOnce(&ModelLoader::DoLoadOnBackgroundThread, model_loader,
-                     encryptor, local_or_syncable_file_path,
-                     encrypted_local_or_syncable_file_path, account_file_path,
-                     encrypted_account_file_path,
-                     std::move(load_managed_node_callback)),
+      base::BindOnce(
+          &ModelLoader::DoLoadOnBackgroundThread, model_loader, encryptor,
+          local_or_syncable_file_path, encrypted_local_or_syncable_file_path,
+          account_file_path, encrypted_account_file_path,
+          std::move(
+              save_local_or_syncable_secondary_file_callback_on_main_sequence),
+          std::move(save_account_secondary_file_callback_on_main_sequence),
+          std::move(load_managed_node_callback)),
       std::move(callback));
   return model_loader;
 }
@@ -395,11 +417,15 @@
     const base::FilePath& encrypted_local_or_syncable_file_path,
     const base::FilePath& account_file_path,
     const base::FilePath& encrypted_account_file_path,
+    base::OnceClosure save_local_or_syncable_secondary_file_callback,
+    base::OnceClosure save_account_secondary_file_callback,
     LoadManagedNodeCallback load_managed_node_callback) {
   std::unique_ptr<BookmarkLoadDetails> details =
       LoadBookmarks(encryptor, local_or_syncable_file_path,
                     encrypted_local_or_syncable_file_path, account_file_path,
-                    encrypted_account_file_path);
+                    encrypted_account_file_path,
+                    std::move(save_local_or_syncable_secondary_file_callback),
+                    std::move(save_account_secondary_file_callback));
   CHECK(details);
 
   details->PopulateNodeIdsForLocalOrSyncablePermanentNodes();
diff --git a/components/bookmarks/browser/model_loader.h b/components/bookmarks/browser/model_loader.h
index bed04cf..ece1b44a 100644
--- a/components/bookmarks/browser/model_loader.h
+++ b/components/bookmarks/browser/model_loader.h
@@ -47,6 +47,11 @@
   // `encrypted_local_or_syncable_file_path` must be non-empty and
   // `encrypted_account_file_path` should be empty only if
   // `account_file_path` is empty.
+  // `save_local_or_syncable_secondary_file_callback` and
+  // `save_account_secondary_file_callback`  will be called if bookmarks need to
+  // be saved to a secondary file. The secondary file might contain the
+  // unencrypted or encrypted bookmarks, see
+  // BookmarkStorage::SaveBookmarksToSecondaryFile for more details.
   static scoped_refptr<ModelLoader> Create(
       scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
           encryptor,
@@ -55,6 +60,8 @@
       const base::FilePath& account_file_path,
       const base::FilePath& encrypted_account_file_path,
       LoadManagedNodeCallback load_managed_node_callback,
+      base::OnceClosure save_local_or_syncable_secondary_file_callback,
+      base::OnceClosure save_account_secondary_file_callback,
       LoadCallback callback);
 
   ModelLoader(const ModelLoader&) = delete;
@@ -89,6 +96,8 @@
       const base::FilePath& encrypted_local_or_syncable_file_path,
       const base::FilePath& account_file_path,
       const base::FilePath& encrypted_account_file_path,
+      base::OnceClosure save_local_or_syncable_secondary_file_callback,
+      base::OnceClosure save_account_secondary_file_callback,
       LoadManagedNodeCallback load_managed_node_callback);
 
   scoped_refptr<base::SequencedTaskRunner> backend_task_runner_;
diff --git a/components/bookmarks/browser/model_loader_unittest.cc b/components/bookmarks/browser/model_loader_unittest.cc
index 88ad234..0170d76 100644
--- a/components/bookmarks/browser/model_loader_unittest.cc
+++ b/components/bookmarks/browser/model_loader_unittest.cc
@@ -81,7 +81,9 @@
       /*encrypted_local_or_syncable_file_path=*/base::FilePath(),
       /*account_file_path=*/base::FilePath(),
       /*encrypted_account_file_path=*/base::FilePath(),
-      /*load_managed_node_callback=*/LoadManagedNodeCallback(),
+      LoadManagedNodeCallback(),
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
       details_future.GetCallback());
 
   const std::unique_ptr<BookmarkLoadDetails>& details = details_future.Get();
@@ -158,7 +160,9 @@
       /*encrypted_local_or_syncable_file_path=*/base::FilePath(),
       /*account_file_path=*/base::FilePath(),
       /*encrypted_account_file_path=*/base::FilePath(),
-      /*load_managed_node_callback=*/LoadManagedNodeCallback(),
+      LoadManagedNodeCallback(),
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
       details_future.GetCallback());
 
   const std::unique_ptr<BookmarkLoadDetails>& details = details_future.Get();
@@ -235,7 +239,9 @@
       /*encrypted_local_or_syncable_file_path=*/base::FilePath(),
       /*account_file_path=*/base::FilePath(),
       /*encrypted_account_file_path=*/base::FilePath(),
-      /*load_managed_node_callback=*/LoadManagedNodeCallback(),
+      LoadManagedNodeCallback(),
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
       details_future.GetCallback());
 
   const std::unique_ptr<BookmarkLoadDetails>& details = details_future.Get();
@@ -312,7 +318,9 @@
       /*encrypted_local_or_syncable_file_path=*/base::FilePath(),
       /*account_file_path=*/base::FilePath(),
       /*encrypted_account_file_path=*/base::FilePath(),
-      /*load_managed_node_callback=*/LoadManagedNodeCallback(),
+      LoadManagedNodeCallback(),
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
       details_future.GetCallback());
 
   const std::unique_ptr<BookmarkLoadDetails> details = details_future.Take();
@@ -394,7 +402,9 @@
       /*encrypted_local_or_syncable_file_path=*/base::FilePath(),
       /*account_file_path=*/base::FilePath(),
       /*encrypted_account_file_path=*/base::FilePath(),
-      /*load_managed_node_callback=*/LoadManagedNodeCallback(),
+      LoadManagedNodeCallback(),
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
       details_future.GetCallback());
 
   const std::unique_ptr<BookmarkLoadDetails> details = details_future.Take();
@@ -469,7 +479,9 @@
       /*encrypted_local_or_syncable_file_path=*/base::FilePath(),
       /*account_file_path=*/test_file2,
       /*encrypted_account_file_path=*/base::FilePath(),
-      /*load_managed_node_callback=*/LoadManagedNodeCallback(),
+      LoadManagedNodeCallback(),
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
       details_future.GetCallback());
 
   const std::unique_ptr<BookmarkLoadDetails> details = details_future.Take();
@@ -573,7 +585,9 @@
       /*encrypted_local_or_syncable_file_path=*/base::FilePath(),
       /*account_file_path=*/test_file,
       /*encrypted_account_file_path=*/base::FilePath(),
-      /*load_managed_node_callback=*/LoadManagedNodeCallback(),
+      LoadManagedNodeCallback(),
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
       details_future.GetCallback());
 
   const std::unique_ptr<BookmarkLoadDetails> details = details_future.Take();
@@ -648,7 +662,9 @@
       /*encrypted_local_or_syncable_file_path=*/base::FilePath(),
       /*account_file_path=*/test_file2,
       /*encrypted_account_file_path=*/base::FilePath(),
-      /*load_managed_node_callback=*/LoadManagedNodeCallback(),
+      LoadManagedNodeCallback(),
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
       details_future.GetCallback());
 
   const std::unique_ptr<BookmarkLoadDetails> details = details_future.Take();
@@ -725,7 +741,9 @@
       /*encrypted_local_or_syncable_file_path=*/base::FilePath(),
       /*account_file_path=*/test_file2,
       /*encrypted_account_file_path=*/base::FilePath(),
-      /*load_managed_node_callback=*/LoadManagedNodeCallback(),
+      LoadManagedNodeCallback(),
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
       details_future.GetCallback());
 
   const std::unique_ptr<BookmarkLoadDetails> details = details_future.Take();
@@ -801,7 +819,9 @@
       /*encrypted_local_or_syncable_file_path=*/base::FilePath(),
       /*account_file_path=*/test_file2,
       /*encrypted_account_file_path=*/base::FilePath(),
-      /*load_managed_node_callback=*/LoadManagedNodeCallback(),
+      LoadManagedNodeCallback(),
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
       details_future.GetCallback());
 
   const std::unique_ptr<BookmarkLoadDetails> details = details_future.Take();
@@ -877,7 +897,9 @@
       /*encrypted_local_or_syncable_file_path=*/base::FilePath(),
       /*account_file_path=*/test_file2,
       /*encrypted_account_file_path=*/base::FilePath(),
-      /*load_managed_node_callback=*/LoadManagedNodeCallback(),
+      LoadManagedNodeCallback(),
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
       details_future.GetCallback());
 
   const std::unique_ptr<BookmarkLoadDetails> details = details_future.Take();
@@ -960,7 +982,9 @@
       /*encrypted_local_or_syncable_file_path=*/base::FilePath(),
       /*account_file_path=*/base::FilePath(),
       /*encrypted_account_file_path=*/base::FilePath(),
-      /*load_managed_node_callback=*/LoadManagedNodeCallback(),
+      LoadManagedNodeCallback(),
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
       details_future.GetCallback());
 
   const std::unique_ptr<BookmarkLoadDetails> details = details_future.Take();
@@ -1048,17 +1072,24 @@
   const base::FilePath encrypted_account_file_path =
       GetTestDataDir().AppendASCII("bookmarks/encrypted_missing_file_2.json");
 
+  base::test::TestFuture<void> save_local_or_syncable_secondary_file_future;
+  base::test::TestFuture<void> save_account_secondary_file_future;
   base::test::TestFuture<std::unique_ptr<BookmarkLoadDetails>> details_future;
   scoped_refptr<ModelLoader> loader = ModelLoader::Create(
       encryptor, local_or_syncable_file_path,
       encrypted_local_or_syncable_file_path, account_file_path,
       encrypted_account_file_path, LoadManagedNodeCallback(),
+      save_local_or_syncable_secondary_file_future.GetCallback(),
+      save_account_secondary_file_future.GetCallback(),
       details_future.GetCallback());
 
   task_environment.FastForwardUntilNoTasksRemain();
 
   VerifyEncryptedBookmarksFileCheckResult(
       histogram_tester, metrics::BookmarksFileLoadResult::kFileMissing);
+  // Verify that the save encrypted file callback is called for both files.
+  EXPECT_TRUE(save_local_or_syncable_secondary_file_future.IsReady());
+  EXPECT_TRUE(save_account_secondary_file_future.IsReady());
 }
 
 TEST(ModelLoaderTest, LoadEncryptedFiles_DecryptionFailed) {
@@ -1083,17 +1114,24 @@
   const base::FilePath encrypted_account_file_path =
       GetTestDataDir().AppendASCII("bookmarks/model_with_sync_metadata_2.json");
 
+  base::test::TestFuture<void> save_local_or_syncable_secondary_file_future;
+  base::test::TestFuture<void> save_account_secondary_file_future;
   base::test::TestFuture<std::unique_ptr<BookmarkLoadDetails>> details_future;
   scoped_refptr<ModelLoader> loader = ModelLoader::Create(
       encryptor, local_or_syncable_file_path,
       encrypted_local_or_syncable_file_path, account_file_path,
       encrypted_account_file_path, LoadManagedNodeCallback(),
+      save_local_or_syncable_secondary_file_future.GetCallback(),
+      save_account_secondary_file_future.GetCallback(),
       details_future.GetCallback());
 
   task_environment.FastForwardUntilNoTasksRemain();
 
   VerifyEncryptedBookmarksFileCheckResult(
       histogram_tester, metrics::BookmarksFileLoadResult::kDecryptionFailed);
+  // Verify that the save encrypted file callback is called for both files.
+  EXPECT_TRUE(save_local_or_syncable_secondary_file_future.IsReady());
+  EXPECT_TRUE(save_account_secondary_file_future.IsReady());
 }
 
 std::optional<base::FilePath> CreateTempEncryptedFile(
@@ -1142,11 +1180,15 @@
                               "TestEncryptedBookmarks2", encryptor);
   ASSERT_TRUE(encrypted_account_file_path);
 
+  base::test::TestFuture<void> save_local_or_syncable_secondary_file_future;
+  base::test::TestFuture<void> save_account_secondary_file_future;
   base::test::TestFuture<std::unique_ptr<BookmarkLoadDetails>> details_future;
   scoped_refptr<ModelLoader> loader = ModelLoader::Create(
       encryptor, local_or_syncable_file_path,
       encrypted_local_or_syncable_file_path.value(), account_file_path,
       encrypted_account_file_path.value(), LoadManagedNodeCallback(),
+      save_local_or_syncable_secondary_file_future.GetCallback(),
+      save_account_secondary_file_future.GetCallback(),
       details_future.GetCallback());
 
   task_environment.FastForwardUntilNoTasksRemain();
@@ -1171,6 +1213,9 @@
           {kEncryptedBookmarksFileMatchesResultMetricName, ".Account"}),
       false,
       /*expected_count=*/1);
+  // Verify that the save encrypted file callback is called for both files.
+  EXPECT_TRUE(save_local_or_syncable_secondary_file_future.IsReady());
+  EXPECT_TRUE(save_account_secondary_file_future.IsReady());
 }
 
 TEST(ModelLoaderTest, LoadEncryptedFiles_EncryptedFilesOk) {
@@ -1198,11 +1243,15 @@
                               encryptor);
   ASSERT_TRUE(encrypted_account_file_path);
 
+  base::test::TestFuture<void> save_local_or_syncable_secondary_file_future;
+  base::test::TestFuture<void> save_account_secondary_file_future;
   base::test::TestFuture<std::unique_ptr<BookmarkLoadDetails>> details_future;
   scoped_refptr<ModelLoader> loader = ModelLoader::Create(
       encryptor, local_or_syncable_file_path,
       encrypted_local_or_syncable_file_path.value(), account_file_path,
       encrypted_account_file_path.value(), LoadManagedNodeCallback(),
+      save_local_or_syncable_secondary_file_future.GetCallback(),
+      save_account_secondary_file_future.GetCallback(),
       details_future.GetCallback());
 
   task_environment.FastForwardUntilNoTasksRemain();
@@ -1227,6 +1276,79 @@
           {kEncryptedBookmarksFileMatchesResultMetricName, ".Account"}),
       true,
       /*expected_count=*/1);
+  // Verify that the save encrypted file callback hasn't been called.
+  EXPECT_FALSE(save_local_or_syncable_secondary_file_future.IsReady());
+  EXPECT_FALSE(save_account_secondary_file_future.IsReady());
+}
+
+TEST(ModelLoaderTest, LoadEncryptedFiles_OnlyAccountCallbackCalled) {
+  base::test::ScopedFeatureList features;
+  test::InitFeaturesForBookmarkTestEncryptionStage(
+      features, BookmarkEncryptionStage::kWriteBothReadOnlyClear);
+  base::HistogramTester histogram_tester;
+  base::test::TaskEnvironment task_environment{
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+  scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
+      encryptor = base::MakeRefCounted<
+          base::RefCountedData<const os_crypt_async::Encryptor>>(
+          std::in_place, os_crypt_async::GetTestEncryptorForTesting());
+
+  const base::FilePath local_or_syncable_file_path =
+      GetTestDataDir().AppendASCII("bookmarks/model_with_sync_metadata_1.json");
+  std::optional<base::FilePath> encrypted_local_or_syncable_file_path =
+      CreateTempEncryptedFile(local_or_syncable_file_path,
+                              "TestEncryptedBookmarks", encryptor);
+  ASSERT_TRUE(encrypted_local_or_syncable_file_path);
+  const base::FilePath account_file_path =
+      GetTestDataDir().AppendASCII("bookmarks/model_with_sync_metadata_2.json");
+  std::optional<base::FilePath> encrypted_account_file_path =
+      GetTestDataDir().AppendASCII("bookmarks/encrypted_missing_file_2.json");
+
+  base::test::TestFuture<void> save_local_or_syncable_secondary_file_future;
+  base::test::TestFuture<void> save_account_secondary_file_future;
+  base::test::TestFuture<std::unique_ptr<BookmarkLoadDetails>> details_future;
+  scoped_refptr<ModelLoader> loader = ModelLoader::Create(
+      encryptor, local_or_syncable_file_path,
+      encrypted_local_or_syncable_file_path.value(), account_file_path,
+      encrypted_account_file_path.value(), LoadManagedNodeCallback(),
+      save_local_or_syncable_secondary_file_future.GetCallback(),
+      save_account_secondary_file_future.GetCallback(),
+      details_future.GetCallback());
+
+  task_environment.FastForwardUntilNoTasksRemain();
+
+  // Local or syncable reads succeed
+  histogram_tester.ExpectTotalCount(
+      base::StrCat({kBookmarksFileLoadResultMetricName, ".LocalOrSyncable",
+                    ".Encrypted"}),
+      /*expected_count=*/1);
+  histogram_tester.ExpectBucketCount(
+      base::StrCat({kBookmarksFileLoadResultMetricName, ".LocalOrSyncable",
+                    ".Encrypted"}),
+      metrics::BookmarksFileLoadResult::kSuccess,
+      /*expected_count=*/1);
+  histogram_tester.ExpectTotalCount(
+      base::StrCat(
+          {kEncryptedBookmarksFileMatchesResultMetricName, ".LocalOrSyncable"}),
+      /*expected_count=*/1);
+  histogram_tester.ExpectBucketCount(
+      base::StrCat(
+          {kEncryptedBookmarksFileMatchesResultMetricName, ".LocalOrSyncable"}),
+      true,
+      /*expected_count=*/1);
+  EXPECT_FALSE(save_local_or_syncable_secondary_file_future.IsReady());
+
+  // Account reads fail
+  histogram_tester.ExpectTotalCount(
+      base::StrCat(
+          {kBookmarksFileLoadResultMetricName, ".Account", ".Encrypted"}),
+      /*expected_count=*/1);
+  histogram_tester.ExpectBucketCount(
+      base::StrCat(
+          {kBookmarksFileLoadResultMetricName, ".Account", ".Encrypted"}),
+      metrics::BookmarksFileLoadResult::kFileMissing,
+      /*expected_count=*/1);
+  EXPECT_TRUE(save_account_secondary_file_future.IsReady());
 }
 
 TEST(ModelLoaderTest, LoadEncryptedFiles_SizeAndReadTimeAreRecorded) {
@@ -1254,12 +1376,13 @@
                               encryptor);
   ASSERT_TRUE(encrypted_account_file_path);
 
-  base::test::TestFuture<std::unique_ptr<BookmarkLoadDetails>> details_future;
   scoped_refptr<ModelLoader> loader = ModelLoader::Create(
       encryptor, local_or_syncable_file_path,
       encrypted_local_or_syncable_file_path.value(), account_file_path,
       encrypted_account_file_path.value(), LoadManagedNodeCallback(),
-      details_future.GetCallback());
+      /*save_local_or_syncable_secondary_file_callback=*/base::DoNothing(),
+      /*save_account_secondary_file_callback=*/base::DoNothing(),
+      /*callback=*/base::DoNothing());
 
   task_environment.FastForwardUntilNoTasksRemain();
 
diff --git a/components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheet.java b/components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheet.java
index 947f6e0f..cb28727 100644
--- a/components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheet.java
+++ b/components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheet.java
@@ -575,7 +575,7 @@
      *
      * @param content The {@link BottomSheetContent} to show, or null if no content should be shown.
      */
-    void showContent(@Nullable final BottomSheetContent content) {
+    void showContent(final @Nullable BottomSheetContent content) {
         // If the desired content is already showing, do nothing.
         if (mSheetContent == content) return;
 
@@ -1350,7 +1350,7 @@
      *
      * @param content The new sheet content, or null if the sheet has no content.
      */
-    protected void onSheetContentChanged(@Nullable final BottomSheetContent content) {
+    protected void onSheetContentChanged(final @Nullable BottomSheetContent content) {
         mSheetContent = content;
 
         boolean shouldLongPressMoveSheet =
diff --git a/components/browser_ui/bottomsheet/android/test/java/src/org/chromium/components/browser_ui/bottomsheet/TestBottomSheetContent.java b/components/browser_ui/bottomsheet/android/test/java/src/org/chromium/components/browser_ui/bottomsheet/TestBottomSheetContent.java
index 3baef00..f4ec7bb 100644
--- a/components/browser_ui/bottomsheet/android/test/java/src/org/chromium/components/browser_ui/bottomsheet/TestBottomSheetContent.java
+++ b/components/browser_ui/bottomsheet/android/test/java/src/org/chromium/components/browser_ui/bottomsheet/TestBottomSheetContent.java
@@ -124,9 +124,8 @@
         return mContentView;
     }
 
-    @Nullable
     @Override
-    public View getToolbarView() {
+    public @Nullable View getToolbarView() {
         return mToolbarView;
     }
 
diff --git a/components/browser_ui/client_certificate/android/java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequest.java b/components/browser_ui/client_certificate/android/java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequest.java
index 84e8f4a..258eec5 100644
--- a/components/browser_ui/client_certificate/android/java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequest.java
+++ b/components/browser_ui/client_certificate/android/java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequest.java
@@ -62,7 +62,7 @@
         // These fields will store the results computed in doInBackground so that they can be posted
         // back in onPostExecute.
         private byte @Nullable [][] mEncodedChain;
-        @Nullable private PrivateKey mPrivateKey;
+        private @Nullable PrivateKey mPrivateKey;
 
         // Pointer to the native certificate request needed to return the results.
         private final long mNativePtr;
diff --git a/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/ManagedPreferenceDelegate.java b/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/ManagedPreferenceDelegate.java
index a2d8c828..94dede3 100644
--- a/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/ManagedPreferenceDelegate.java
+++ b/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/ManagedPreferenceDelegate.java
@@ -62,8 +62,7 @@
      *     enterprise policy, false if recommendation is not followed, null if there is no
      *     recommendation.
      */
-    @Nullable
-    default Boolean isPreferenceRecommendation(Preference preference) {
+    default @Nullable Boolean isPreferenceRecommendation(Preference preference) {
         // TODO(crbug.com/428544701) Remove default after adding to existing child classes.
         // This is almost a feature flag insofar as it prevents behavior from changing.
         return null;
diff --git a/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/search/SettingsIndexData.java b/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/search/SettingsIndexData.java
index ef29fb37..6856d35 100644
--- a/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/search/SettingsIndexData.java
+++ b/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/search/SettingsIndexData.java
@@ -64,8 +64,7 @@
         return sInstance;
     }
 
-    @Nullable
-    public static SettingsIndexData getInstance() {
+    public static @Nullable SettingsIndexData getInstance() {
         return sInstance;
     }
 
@@ -79,8 +78,7 @@
      * @param input The string to normalize.
      * @return The normalized string, or null if the input was null.
      */
-    @Nullable
-    private static String normalizeString(@Nullable String input) {
+    private static @Nullable String normalizeString(@Nullable String input) {
         if (input == null) return null;
 
         // 1. Decompose characters into base letters and combining accent marks.
@@ -479,8 +477,7 @@
         addEntry(id, builder.build());
     }
 
-    @Nullable
-    public Entry getEntry(String id) {
+    public @Nullable Entry getEntry(String id) {
         return mEntries.get(id);
     }
 
@@ -491,8 +488,7 @@
      * @param key Key name of the preference entry.
      * @return entry The entry if it exists, null otherwise.
      */
-    @Nullable
-    public Entry getEntryForKey(String prefFragment, String key) {
+    public @Nullable Entry getEntryForKey(String prefFragment, String key) {
         return getEntry(PreferenceParser.createUniqueId(prefFragment, key));
     }
 
@@ -706,8 +702,7 @@
      * @return The title of the top-level preference that leads to this fragment, or {@code null} if
      *     no valid path back to the root can be found (i.e., the fragment is an orphan).
      */
-    @Nullable
-    private String findVisibleHeader(
+    private @Nullable String findVisibleHeader(
             String fragmentName, Map<String, String> cache, String rootFragmentName) {
         if (cache.containsKey(fragmentName)) {
             return cache.get(fragmentName);
diff --git a/components/browser_ui/settings/android/junit/src/org/chromium/components/browser_ui/settings/search/PreferenceParserTest.java b/components/browser_ui/settings/android/junit/src/org/chromium/components/browser_ui/settings/search/PreferenceParserTest.java
index bf2d9b3..af90c505 100644
--- a/components/browser_ui/settings/android/junit/src/org/chromium/components/browser_ui/settings/search/PreferenceParserTest.java
+++ b/components/browser_ui/settings/android/junit/src/org/chromium/components/browser_ui/settings/search/PreferenceParserTest.java
@@ -126,8 +126,7 @@
                 textMessageBundle);
     }
 
-    @Nullable
-    private Bundle findBundleByKey(List<Bundle> metadata, String key) {
+    private @Nullable Bundle findBundleByKey(List<Bundle> metadata, String key) {
         for (Bundle bundle : metadata) {
             if (key.equals(bundle.getString(PreferenceParser.METADATA_KEY))) {
                 return bundle;
diff --git a/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeButtonPreference.java b/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeButtonPreference.java
index a23ce1e8..2434ec1 100644
--- a/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeButtonPreference.java
+++ b/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeButtonPreference.java
@@ -39,7 +39,7 @@
     private @Nullable CharSequence mText;
 
     /** The color resource ID for tinting of the view's background. */
-    @ColorRes private @Nullable Integer mBackgroundColorRes;
+    private @Nullable @ColorRes Integer mBackgroundColorRes;
 
     /** The string to use for the Button widget content description. */
     private @Nullable CharSequence mContentDescription;
diff --git a/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeImageViewPreference.java b/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeImageViewPreference.java
index 65e3c5b..3c87ff8 100644
--- a/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeImageViewPreference.java
+++ b/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeImageViewPreference.java
@@ -48,10 +48,10 @@
     @DrawableRes private int mImageRes;
 
     /** The color resource ID for tinting of ImageView widget. */
-    @ColorRes private int mColorRes;
+    private @ColorRes int mColorRes;
 
     /** The color resource ID for tinting of the view's background. */
-    @ColorRes private @Nullable Integer mBackgroundColorRes;
+    private @Nullable @ColorRes Integer mBackgroundColorRes;
 
     /** The string to use for the ImageView widget content description. */
     private @Nullable CharSequence mContentDescription;
diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java
index c1dee80..897665b 100644
--- a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java
+++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java
@@ -241,7 +241,7 @@
     @ContentSettingsType.EnumType private int mHighlightedPermission = ContentSettingsType.DEFAULT;
 
     /** The highlight color. */
-    @ColorRes private int mHighlightColor;
+    private @ColorRes int mHighlightColor;
 
     // The callback to be run after this site is reset.
     private @Nullable Observer mWebsiteSettingsObserver;
diff --git a/components/browser_ui/styles/android/BUILD.gn b/components/browser_ui/styles/android/BUILD.gn
index 508042d..f23599d 100644
--- a/components/browser_ui/styles/android/BUILD.gn
+++ b/components/browser_ui/styles/android/BUILD.gn
@@ -151,10 +151,12 @@
     "java/res/drawable/ic_add_24dp.xml",
     "java/res/drawable/ic_all_bookmarks_icon_16dp.xml",
     "java/res/drawable/ic_android_messages_icon.xml",
+    "java/res/drawable/ic_arrow_selector_spark_24dp.xml",
     "java/res/drawable/ic_back_to_tab_20dp.xml",
     "java/res/drawable/ic_bar_chart_24dp.xml",
     "java/res/drawable/ic_brightness_medium_24dp.xml",
     "java/res/drawable/ic_call_end_white_24dp.xml",
+    "java/res/drawable/ic_check_circle_24dp.xml",
     "java/res/drawable/ic_check_circle_filled_green_24dp.xml",
     "java/res/drawable/ic_chevron_left_24dp.xml",
     "java/res/drawable/ic_chevron_right_24dp.xml",
@@ -227,6 +229,7 @@
     "java/res/drawable/ic_security_grey.xml",
     "java/res/drawable/ic_settings_24dp.xml",
     "java/res/drawable/ic_settings_applications_24dp.xml",
+    "java/res/drawable/ic_shield_24dp.xml",
     "java/res/drawable/ic_skip_next_white_24dp.xml",
     "java/res/drawable/ic_skip_previous_white_24dp.xml",
     "java/res/drawable/ic_speed_24dp.xml",
diff --git a/components/browser_ui/styles/android/java/res/drawable/ic_arrow_selector_spark_24dp.xml b/components/browser_ui/styles/android/java/res/drawable/ic_arrow_selector_spark_24dp.xml
new file mode 100644
index 0000000..767fb934
--- /dev/null
+++ b/components/browser_ui/styles/android/java/res/drawable/ic_arrow_selector_spark_24dp.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2026 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="UnusedResources"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path android:fillColor="@android:color/white"
+        android:pathData="M740,400Q739,400 732,394Q716,333 671.5,288.5Q627,244 566,228Q564,227 560,220Q560,218 566,212Q627,196 671.5,151.5Q716,107 732,46Q733,44 740,40Q742,40 748,46Q765,107 809,151.5Q853,196 914,212Q916,212 920,220Q920,221 914,228Q853,244 808.5,288.5Q764,333 748,394Q748,396 740,400ZM240,550L299,440L469,440L240,244L240,550ZM471,880L326,568L160,800L160,80L720,520L436,520L580,829L471,880ZM299,440L299,440L299,440L299,440Z"/>
+</vector>
diff --git a/components/browser_ui/styles/android/java/res/drawable/ic_check_circle_24dp.xml b/components/browser_ui/styles/android/java/res/drawable/ic_check_circle_24dp.xml
new file mode 100644
index 0000000..ea4b3ef1
--- /dev/null
+++ b/components/browser_ui/styles/android/java/res/drawable/ic_check_circle_24dp.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2026 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="UnusedResources"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path android:fillColor="@android:color/white"
+        android:pathData="M424,664L706,382L650,326L424,552L310,438L254,494L424,664ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
+</vector>
diff --git a/components/browser_ui/styles/android/java/res/drawable/ic_shield_24dp.xml b/components/browser_ui/styles/android/java/res/drawable/ic_shield_24dp.xml
new file mode 100644
index 0000000..57c9949
--- /dev/null
+++ b/components/browser_ui/styles/android/java/res/drawable/ic_shield_24dp.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2026 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="UnusedResources"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path android:fillColor="@android:color/white"
+        android:pathData="M480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,596 709.5,720.5Q619,845 480,880ZM480,796Q584,763 652,664Q720,565 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796ZM480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Z"/>
+</vector>
diff --git a/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/ChromeDialog.java b/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/ChromeDialog.java
index cd7938e3..f0dd7875 100644
--- a/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/ChromeDialog.java
+++ b/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/ChromeDialog.java
@@ -45,9 +45,9 @@
 public class ChromeDialog extends ComponentDialog {
     private final boolean mIsFullScreen;
     private final Activity mActivity;
-    @Nullable private InsetObserver mInsetObserver;
-    @Nullable private EdgeToEdgeLayoutCoordinator mEdgeToEdgeLayoutCoordinator;
-    @Nullable private WindowInsetsConsumer mWindowInsetsConsumer;
+    private @Nullable InsetObserver mInsetObserver;
+    private @Nullable EdgeToEdgeLayoutCoordinator mEdgeToEdgeLayoutCoordinator;
+    private @Nullable WindowInsetsConsumer mWindowInsetsConsumer;
     private final boolean mShouldPadForWindowInsets;
     private final WindowSystemBarColorHelper mWindowColorHelper;
 
diff --git a/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/ClipDrawableProgressBar.java b/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/ClipDrawableProgressBar.java
index 10dda79..163ef72 100644
--- a/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/ClipDrawableProgressBar.java
+++ b/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/ClipDrawableProgressBar.java
@@ -58,9 +58,9 @@
     // http://developer.android.com/reference/android/graphics/drawable/ScaleDrawable.html
     private static final int DRAWABLE_MAX_LEVEL = 10000;
 
-    @Nullable private ColorDrawable mForegroundColorDrawable;
-    @Nullable private GradientDrawable mForegroundGradientDrawable;
-    @Nullable private GradientDrawable mBackgroundGradientDrawable;
+    private @Nullable ColorDrawable mForegroundColorDrawable;
+    private @Nullable GradientDrawable mForegroundGradientDrawable;
+    private @Nullable GradientDrawable mBackgroundGradientDrawable;
     private int mForegroundColor;
     private int mBackgroundColor;
     private int mStaticBackgroundColor;
diff --git a/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/selectable_list/SelectableListToolbar.java b/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/selectable_list/SelectableListToolbar.java
index 4495b4a..baf88d0 100644
--- a/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/selectable_list/SelectableListToolbar.java
+++ b/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/selectable_list/SelectableListToolbar.java
@@ -118,7 +118,7 @@
     protected boolean mIsSelectionEnabled;
     // When we assign mSelectedItems, make sure we copy the contents so that we can properly track
     // whether the content actually changed.
-    @Nullable private Set<E> mSelectedItems;
+    private @Nullable Set<E> mSelectedItems;
 
     @SuppressWarnings("NullAway.Init")
     protected SelectionDelegate<E> mSelectionDelegate;
diff --git a/components/facilitated_payments/core/browser/BUILD.gn b/components/facilitated_payments/core/browser/BUILD.gn
index 4471f54..0e4343d 100644
--- a/components/facilitated_payments/core/browser/BUILD.gn
+++ b/components/facilitated_payments/core/browser/BUILD.gn
@@ -29,6 +29,7 @@
     "pix_account_linking_manager.h",
     "pix_manager.cc",
     "pix_manager.h",
+    "pix_manager_test_api.h",
     "strike_databases/payment_link_suggestion_strike_database.h",
   ]
 
diff --git a/components/facilitated_payments/core/browser/pix_manager.h b/components/facilitated_payments/core/browser/pix_manager.h
index f8ea587..5075387 100644
--- a/components/facilitated_payments/core/browser/pix_manager.h
+++ b/components/facilitated_payments/core/browser/pix_manager.h
@@ -70,168 +70,9 @@
 
  private:
   friend class PixManagerTest;
+  friend class PixManagerTestApi;
   friend class PixManagerTestForUiScreens;
   friend class PixManagerPaymentsNetworkInterfaceTest;
-  // Keep all entries in alphabetical order!
-  // TODO(crbug.com/479520609): Remove all FRIEND_TEST_ALL_PREFIXES macros from
-  // PixManager by introducing a new PixManagerTestApi.
-  FRIEND_TEST_ALL_PREFIXES(PixManagerPaymentsNetworkInterfaceTest,
-                           OnInitiatePaymentResponseReceived_FailureResponse);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerPaymentsNetworkInterfaceTest,
-                           SendInitiatePaymentRequest);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerPaymentsNetworkInterfaceTest,
-      OnInitiatePaymentResponseReceived_InvokePurchaseActionTriggered);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerPaymentsNetworkInterfaceTest,
-      OnInitiatePaymentResponseReceived_LoggedOutProfile_ErrorScreenShown);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerPaymentsNetworkInterfaceTest,
-      OnInitiatePaymentResponseReceived_NoActionToken_ErrorScreenShown);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerPaymentsNetworkInterfaceTest,
-      OnInitiatePaymentResponseReceived_NoCoreAccountInfo_ErrorScreenShown);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerPaymentsNetworkInterfaceTest, Reset);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           ApiClientInitializedLazily);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           ApiClientTriggeredAfterPixCodeValidation);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           CopyTrigger_UrlInAllowlist_PixValidationTriggered);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      CopyTrigger_UrlInAllowlist__ControlIdPopulatedInInitiatePaymentRequest);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      CopyTrigger_UrlNotInAllowlist_PixValidationNotTriggered);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      CopyTrigger_UrlNotInAllowlist_PayflowExitedHistogramLogged);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      CopyTrigger_InIframe_PspHostnamePopulatedInInitiatePaymentRequest);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      CopyTrigger_InIframe_ExperimentIdPopulatedInInitiatePaymentRequest);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           IsIframeStateUpdatedOnConsecutiveCalls);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           DismissPrompt);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      ChromeCustomTabWithGboardAsDefaultIme_PixFlowNotTriggered);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      ChromeCustomTabWithGboardNotAsDefaultIme_PixFlowTriggered);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      ErrorScreenNotAutoDismissedAfterInvokingPurchaseAction);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           HandlesFailureToLazilyInitializeApiClient);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           LogApiAvailabilityCheckResultAndLatency);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           LogGetClientTokenResultAndLatency);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           LogInitiatePurchaseActionAttempt);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           LogInitiatePurchaseActionResultAndLatency);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           LogTransactionResultAndLatency);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           LogTransactionResultForIframe);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           LogTransactionResultForMainFrame);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           NoPaymentsDataManager_PixFlowsAbandoned);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           PaymentsAutofillTurnedOff_PixFlowsAbandoned);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           PayflowExitedReason_PaymentsAutofillTurnedOff);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           UserOptedOut_PixFlowsAbandoned);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           NoPixAccounts_NoApiClientTriggered);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           NoPixPaymentPromptWhenApiClientNotAvailable);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           OnGetClientToken_ClientTokenEmpty_ErrorScreenShown);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           OnPixAccountSelected);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      OnPurchaseActionResult_CouldNotInvoke_ErrorScreenShown);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      OnPurchaseActionResult_ResultCanceled_UiScreenDismissed);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           OnPurchaseActionResult_ResultOk_UiScreenDismissed);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           PayflowExitedReason_ApiClientNotAvailable);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           PayflowExitedReason_ClientTokenNotAvailable);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           PayflowExitedReason_CodeValidatorFailed);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           PayflowExitedReason_InvalidCode);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           PayflowExitedReason_NoLinkedAccount);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      PayflowExitedReason_StaticCode_FeatureDisabled_PixFlowsAbandoned);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      PayflowExitedReason_StaticCode_ApiClientAvailabilityChecked);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      NoLinkedAccount_AccountLinkingFlagDisabled_AccountLinkingFlowNotTriggered);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           NoLinkedAccount_AccountLinkingFlowTriggered);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           PayflowExitedReason_RiskDataEmpty);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           PayflowExitedReason_UserOptedOut);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           InvalidCode_PixFlowsAbandoned);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           CodeValidatorFailed_PixFlowsAbandoned);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           PixFopSelectorShown_HistogramsLogged);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           UserOptedOut_PixPayflowAbandoned);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      ProgressScreenAutoDismissedAfterInvokingPurchaseAction);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           RegisterPixAllowlist);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           ResettingPreventsPayment);
-  FRIEND_TEST_ALL_PREFIXES(
-      PixManagerTestWithAccountLinkingEnabled,
-      RiskDataEmpty_GetClientTokenNotCalled_ErrorScreenShown);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           RiskDataEmpty_HistogramsLogged);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           RiskDataNotEmpty_GetClientTokenCalled);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           RiskDataNotEmpty_HistogramsLogged);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           ShowErrorScreen);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           ShowPixPaymentPrompt);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           ShowProgressScreen);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestWithAccountLinkingEnabled,
-                           ShowsPixPaymentPromptWhenApiClientAvailable);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestForUiScreens,
-                           NewScreenCouldNotBeShown);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestForUiScreens, NewScreenShown);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestForUiScreens, ScreenClosedByUser);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestForUiScreens, ScreenClosedNotByUser);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestInLandscapeMode,
-                           PayflowExitedReason_LandscapeScreenOrientation);
-  FRIEND_TEST_ALL_PREFIXES(PixManagerTestInLandscapeMode,
-                           PixPayflowBlockedWhenFlagDisabled);
 
   // Queries the allowlist for the `url`. The result could be:
   // 1. In the allowlist
diff --git a/components/facilitated_payments/core/browser/pix_manager_test_api.h b/components/facilitated_payments/core/browser/pix_manager_test_api.h
new file mode 100644
index 0000000..28b0f290
--- /dev/null
+++ b/components/facilitated_payments/core/browser/pix_manager_test_api.h
@@ -0,0 +1,119 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_FACILITATED_PAYMENTS_CORE_BROWSER_PIX_MANAGER_TEST_API_H_
+#define COMPONENTS_FACILITATED_PAYMENTS_CORE_BROWSER_PIX_MANAGER_TEST_API_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/functional/callback_forward.h"
+#include "base/memory/raw_ref.h"
+#include "base/types/expected.h"
+#include "components/facilitated_payments/core/browser/pix_manager.h"
+#include "components/facilitated_payments/core/mojom/pix_code_validator.mojom.h"
+
+namespace payments::facilitated {
+
+class PixManagerTestApi {
+ public:
+  explicit PixManagerTestApi(PixManager& manager) : manager_(manager) {}
+  ~PixManagerTestApi() = default;
+
+  void OnPixCodeValidated(
+      std::optional<PixCodeRustValidationResult> rust_validation_result,
+      std::string pix_code,
+      base::TimeTicks start_time,
+      base::expected<mojom::PixQrCodeType, std::string> pix_qr_code_type) {
+    manager_->OnPixCodeValidated(rust_validation_result, std::move(pix_code),
+                                 start_time, std::move(pix_qr_code_type));
+  }
+
+  void OnApiAvailabilityReceived(base::TimeTicks start_time,
+                                 bool is_api_available) {
+    manager_->OnApiAvailabilityReceived(start_time, is_api_available);
+  }
+
+  void OnPixAccountSelected(base::TimeTicks fop_selector_shown_timestamp,
+                            int64_t selected_instrument_id) {
+    manager_->OnPixAccountSelected(fop_selector_shown_timestamp,
+                                   selected_instrument_id);
+  }
+
+  void OnRiskDataLoaded(base::TimeTicks start_time,
+                        const std::string& risk_data) {
+    manager_->OnRiskDataLoaded(start_time, risk_data);
+  }
+
+  void OnGetClientToken(base::TimeTicks start_time,
+                        std::vector<uint8_t> client_token) {
+    manager_->OnGetClientToken(start_time, std::move(client_token));
+  }
+
+  void OnInitiatePaymentResponseReceived(
+      base::TimeTicks start_time,
+      autofill::payments::PaymentsAutofillClient::PaymentsRpcResult result,
+      std::unique_ptr<FacilitatedPaymentsInitiatePaymentResponseDetails>
+          response_details) {
+    manager_->OnInitiatePaymentResponseReceived(start_time, result,
+                                                std::move(response_details));
+  }
+
+  void OnPurchaseActionResult(base::TimeTicks start_time,
+                              PurchaseActionResult result) {
+    manager_->OnPurchaseActionResult(start_time, result);
+  }
+
+  void OnUiScreenEvent(UiEvent ui_event_type) {
+    manager_->OnUiScreenEvent(ui_event_type);
+  }
+
+  void SendInitiatePaymentRequest() { manager_->SendInitiatePaymentRequest(); }
+
+  void ShowPixPaymentPrompt(
+      base::span<const autofill::BankAccount> bank_account_suggestions,
+      base::OnceCallback<void(int64_t)> on_pix_account_selected) {
+    manager_->ShowPixPaymentPrompt(bank_account_suggestions,
+                                   std::move(on_pix_account_selected));
+  }
+
+  void ShowProgressScreen() { manager_->ShowProgressScreen(); }
+  void ShowErrorScreen() { manager_->ShowErrorScreen(); }
+  void DismissPrompt() { manager_->DismissPrompt(); }
+
+  FacilitatedPaymentsApiClient* api_client() {
+    return manager_->api_client_.get();
+  }
+
+  FacilitatedPaymentsApiClientCreator& api_client_creator() {
+    return manager_->api_client_creator_;
+  }
+
+  UiState ui_state() { return manager_->ui_state_; }
+
+  FacilitatedPaymentsInitiatePaymentRequestDetails*
+  initiate_payment_request_details() {
+    return manager_->initiate_payment_request_details_.get();
+  }
+
+  bool pix_code_is_in_iframe() { return manager_->pix_code_is_in_iframe_; }
+
+  bool HasWeakPtrs() const { return manager_->weak_ptr_factory_.HasWeakPtrs(); }
+
+  FacilitatedPaymentsApiClient* GetApiClient() {
+    return manager_->GetApiClient();
+  }
+
+ private:
+  const raw_ref<PixManager> manager_;
+};
+
+inline PixManagerTestApi test_api(PixManager& manager) {
+  return PixManagerTestApi(manager);
+}
+
+}  // namespace payments::facilitated
+
+#endif  // COMPONENTS_FACILITATED_PAYMENTS_CORE_BROWSER_PIX_MANAGER_TEST_API_H_
diff --git a/components/facilitated_payments/core/browser/pix_manager_unittest.cc b/components/facilitated_payments/core/browser/pix_manager_unittest.cc
index c566201e..5c22683c 100644
--- a/components/facilitated_payments/core/browser/pix_manager_unittest.cc
+++ b/components/facilitated_payments/core/browser/pix_manager_unittest.cc
@@ -28,6 +28,7 @@
 #include "components/facilitated_payments/core/browser/mock_facilitated_payments_client.h"
 #include "components/facilitated_payments/core/browser/model/secure_payload.h"
 #include "components/facilitated_payments/core/browser/network_api/mock_facilitated_payments_network_interface.h"
+#include "components/facilitated_payments/core/browser/pix_manager_test_api.h"
 #include "components/facilitated_payments/core/features/features.h"
 #include "components/facilitated_payments/core/metrics/facilitated_payments_metrics.h"
 #include "components/facilitated_payments/core/utils/facilitated_payments_ui_utils.h"
@@ -131,7 +132,7 @@
 
   MockFacilitatedPaymentsApiClient& GetApiClient() {
     return *static_cast<MockFacilitatedPaymentsApiClient*>(
-        pix_manager_->GetApiClient());
+        test_api(*pix_manager_).GetApiClient());
   }
 
  protected:
@@ -189,8 +190,9 @@
 
   EXPECT_CALL(*client_, ShowPixPaymentPrompt(testing::_, testing::_)).Times(0);
 
-  pix_manager_->OnApiAvailabilityReceived(/*start_time=*/base::TimeTicks::Now(),
-                                          /*is_api_available=*/false);
+  test_api(*pix_manager_)
+      .OnApiAvailabilityReceived(/*start_time=*/base::TimeTicks::Now(),
+                                 /*is_api_available=*/false);
 }
 
 // If the facilitated payment API is available, then the manager shows the Pix
@@ -208,8 +210,9 @@
                                                  {pix_account1, pix_account2}),
                                              testing::_));
 
-  pix_manager_->OnApiAvailabilityReceived(/*start_time=*/base::TimeTicks::Now(),
-                                          /*is_api_available=*/true);
+  test_api(*pix_manager_)
+      .OnApiAvailabilityReceived(/*start_time=*/base::TimeTicks::Now(),
+                                 /*is_api_available=*/true);
 }
 
 // If the user selects a Pix account on the payment prompt,
@@ -222,8 +225,9 @@
   EXPECT_CALL(*client_, ShowProgressScreen());
   EXPECT_CALL(*client_, LoadRiskData(testing::_));
 
-  pix_manager_->OnPixAccountSelected(base::TimeTicks::Now() - base::Seconds(2),
-                                     /*selected_instrument_id=*/0);
+  test_api(*pix_manager_)
+      .OnPixAccountSelected(base::TimeTicks::Now() - base::Seconds(2),
+                            /*selected_instrument_id=*/0);
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.FopSelector.UserAction",
@@ -246,8 +250,9 @@
        RiskDataNotEmpty_HistogramsLogged) {
   base::HistogramTester histogram_tester;
 
-  pix_manager_->OnRiskDataLoaded(base::TimeTicks::Now() - base::Seconds(2),
-                                 "seems pretty risky");
+  test_api(*pix_manager_)
+      .OnRiskDataLoaded(base::TimeTicks::Now() - base::Seconds(2),
+                        "seems pretty risky");
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.LoadRiskData.Success.Latency",
@@ -260,7 +265,8 @@
        RiskDataEmpty_HistogramsLogged) {
   base::HistogramTester histogram_tester;
 
-  pix_manager_->OnRiskDataLoaded(base::TimeTicks::Now() - base::Seconds(2), "");
+  test_api(*pix_manager_)
+      .OnRiskDataLoaded(base::TimeTicks::Now() - base::Seconds(2), "");
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.LoadRiskData.Failure.Latency",
@@ -274,7 +280,7 @@
        PayflowExitedReason_RiskDataEmpty) {
   base::HistogramTester histogram_tester;
 
-  pix_manager_->OnRiskDataLoaded(base::TimeTicks::Now(), "");
+  test_api(*pix_manager_).OnRiskDataLoaded(base::TimeTicks::Now(), "");
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -289,8 +295,9 @@
   EXPECT_CALL(GetApiClient(), GetClientToken(testing::_)).Times(0);
   EXPECT_CALL(*client_, ShowErrorScreen());
 
-  pix_manager_->OnRiskDataLoaded(/*start_time=*/base::TimeTicks::Now(),
-                                 /*risk_data=*/"");
+  test_api(*pix_manager_)
+      .OnRiskDataLoaded(/*start_time=*/base::TimeTicks::Now(),
+                        /*risk_data=*/"");
 }
 
 // If the risk data is not empty, then the manager retrieves a client token from
@@ -299,8 +306,9 @@
        RiskDataNotEmpty_GetClientTokenCalled) {
   EXPECT_CALL(GetApiClient(), GetClientToken(testing::_));
 
-  pix_manager_->OnRiskDataLoaded(/*start_time=*/base::TimeTicks::Now(),
-                                 /*risk_data=*/"seems pretty risky");
+  test_api(*pix_manager_)
+      .OnRiskDataLoaded(/*start_time=*/base::TimeTicks::Now(),
+                        /*risk_data=*/"seems pretty risky");
 }
 
 // Verify that the result and latency of the GetClientToken call is logged
@@ -310,10 +318,12 @@
   for (bool get_client_token_result : {true, false}) {
     base::HistogramTester histogram_tester;
 
-    pix_manager_->OnGetClientToken(
-        /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
-        get_client_token_result ? std::vector<uint8_t>{'t', 'o', 'k', 'e', 'n'}
-                                : std::vector<uint8_t>{});
+    test_api(*pix_manager_)
+        .OnGetClientToken(
+            /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
+            get_client_token_result
+                ? std::vector<uint8_t>{'t', 'o', 'k', 'e', 'n'}
+                : std::vector<uint8_t>{});
 
     histogram_tester.ExpectUniqueSample(
         base::StrCat({"FacilitatedPayments.Pix.GetClientToken.",
@@ -330,8 +340,9 @@
        PayflowExitedReason_ClientTokenNotAvailable) {
   base::HistogramTester histogram_tester;
 
-  pix_manager_->OnGetClientToken(/*start_time=*/base::TimeTicks::Now(),
-                                 std::vector<uint8_t>{});
+  test_api(*pix_manager_)
+      .OnGetClientToken(/*start_time=*/base::TimeTicks::Now(),
+                        std::vector<uint8_t>{});
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -343,29 +354,36 @@
        OnGetClientToken_ClientTokenEmpty_ErrorScreenShown) {
   EXPECT_CALL(*client_, ShowErrorScreen());
 
-  pix_manager_->OnGetClientToken(/*start_time=*/base::TimeTicks::Now(),
-                                 std::vector<uint8_t>{});
+  test_api(*pix_manager_)
+      .OnGetClientToken(/*start_time=*/base::TimeTicks::Now(),
+                        std::vector<uint8_t>{});
 }
 
 TEST_P(PixManagerTestWithAccountLinkingEnabled, ResettingPreventsPayment) {
-  pix_manager_->initiate_payment_request_details_->risk_data_ =
+  test_api(*pix_manager_).initiate_payment_request_details()->risk_data_ =
       "seems pretty risky";
-  pix_manager_->initiate_payment_request_details_->client_token_ =
+  test_api(*pix_manager_).initiate_payment_request_details()->client_token_ =
       std::vector<uint8_t>{'t', 'o', 'k', 'e', 'n'};
-  pix_manager_->initiate_payment_request_details_->billing_customer_number_ =
-      13;
-  pix_manager_->initiate_payment_request_details_
+  test_api(*pix_manager_)
+      .initiate_payment_request_details()
+      ->billing_customer_number_ = 13;
+  test_api(*pix_manager_)
+      .initiate_payment_request_details()
       ->merchant_payment_page_hostname_ = "foo.com";
-  pix_manager_->initiate_payment_request_details_->instrument_id_ = 13;
-  pix_manager_->initiate_payment_request_details_->pix_code_ = "a valid code";
+  test_api(*pix_manager_).initiate_payment_request_details()->instrument_id_ =
+      13;
+  test_api(*pix_manager_).initiate_payment_request_details()->pix_code_ =
+      "a valid code";
 
-  EXPECT_TRUE(
-      pix_manager_->initiate_payment_request_details_->IsReadyForPixPayment());
+  EXPECT_TRUE(test_api(*pix_manager_)
+                  .initiate_payment_request_details()
+                  ->IsReadyForPixPayment());
 
   pix_manager_->Reset();
 
-  EXPECT_FALSE(
-      pix_manager_->initiate_payment_request_details_->IsReadyForPixPayment());
+  EXPECT_FALSE(test_api(*pix_manager_)
+                   .initiate_payment_request_details()
+                   ->IsReadyForPixPayment());
 }
 
 TEST_P(PixManagerTestWithAccountLinkingEnabled, CopyTrigger_LogPixCodeCopied) {
@@ -422,9 +440,10 @@
   // asynchronously.
   task_environment_.RunUntilIdle();
 
-  EXPECT_THAT(
-      pix_manager_->initiate_payment_request_details_->chrome_experiment_ids_,
-      testing::ElementsAre(iframe_control_id));
+  EXPECT_THAT(test_api(*pix_manager_)
+                  .initiate_payment_request_details()
+                  ->chrome_experiment_ids_,
+              testing::ElementsAre(iframe_control_id));
 }
 
 TEST_P(PixManagerTestWithAccountLinkingEnabled,
@@ -703,8 +722,9 @@
       "00020126370014br.gov.bcb.pix2515www.example.com6304EA3F",
       ukm::UkmRecorder::GetNewSourceID());
 
-  EXPECT_EQ(pix_manager_->initiate_payment_request_details_->psp_hostname_,
-            "trusted-psp.com");
+  EXPECT_EQ(
+      test_api(*pix_manager_).initiate_payment_request_details()->psp_hostname_,
+      "trusted-psp.com");
 }
 
 TEST_P(PixManagerTestWithAccountLinkingEnabled,
@@ -734,9 +754,10 @@
       "00020126370014br.gov.bcb.pix2515www.example.com6304EA3F",
       ukm::UkmRecorder::GetNewSourceID());
 
-  EXPECT_THAT(
-      pix_manager_->initiate_payment_request_details_->chrome_experiment_ids_,
-      testing::ElementsAre(iframe_experiment_id));
+  EXPECT_THAT(test_api(*pix_manager_)
+                  .initiate_payment_request_details()
+                  ->chrome_experiment_ids_,
+              testing::ElementsAre(iframe_experiment_id));
 }
 
 TEST_P(PixManagerTestWithAccountLinkingEnabled,
@@ -810,7 +831,7 @@
       main_frame_url, iframe_url, origin, PixCodeRustValidationResult::kDynamic,
       "00020126370014br.gov.bcb.pix2515www.example.com6304EA3F",
       ukm::UkmRecorder::GetNewSourceID());
-  EXPECT_TRUE(pix_manager_->pix_code_is_in_iframe_);
+  EXPECT_TRUE(test_api(*pix_manager_).pix_code_is_in_iframe());
 
   // Second call: without iframe. `pix_code_is_in_iframe_` should be updated to
   // false.
@@ -819,7 +840,7 @@
       PixCodeRustValidationResult::kDynamic, "pix_code",
       ukm::UkmRecorder::GetNewSourceID());
   task_environment_.RunUntilIdle();
-  EXPECT_FALSE(pix_manager_->pix_code_is_in_iframe_);
+  EXPECT_FALSE(test_api(*pix_manager_).pix_code_is_in_iframe());
 
   // Third call: with iframe again. `pix_code_is_in_iframe_` should be updated
   // to true.
@@ -827,7 +848,7 @@
       main_frame_url, iframe_url, origin, PixCodeRustValidationResult::kDynamic,
       "pix_code", ukm::UkmRecorder::GetNewSourceID());
   task_environment_.RunUntilIdle();
-  EXPECT_TRUE(pix_manager_->pix_code_is_in_iframe_);
+  EXPECT_TRUE(test_api(*pix_manager_).pix_code_is_in_iframe());
 }
 
 TEST_P(PixManagerTestWithAccountLinkingEnabled,
@@ -872,10 +893,10 @@
   EXPECT_CALL(GetApiClient(), IsAvailable(testing::_));
   EXPECT_CALL(*client_, InitPixAccountLinkingFlow).Times(0);
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 }
 
 // If the validation utility process has disconnected (e.g., due to a crash in
@@ -889,11 +910,12 @@
   EXPECT_CALL(GetApiClient(), IsAvailable(testing::_)).Times(0);
   EXPECT_CALL(*client_, InitPixAccountLinkingFlow).Times(0);
 
-  pix_manager_->OnPixCodeValidated(
-      std::nullopt,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*is_pix_code_valid=*/
-      base::unexpected("Data Decoder terminated unexpectedly"));
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(
+          std::nullopt,
+          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+          /*pix_qr_code_type=*/
+          base::unexpected("Data Decoder terminated unexpectedly"));
 }
 
 // If the validation utility process has disconnected (e.g., due to a crash in
@@ -903,11 +925,12 @@
        PayflowExitedReason_CodeValidatorFailed) {
   base::HistogramTester histogram_tester;
 
-  pix_manager_->OnPixCodeValidated(
-      std::nullopt,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*is_pix_code_valid=*/
-      base::unexpected("Data Decoder terminated unexpectedly"));
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(
+          std::nullopt,
+          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+          /*pix_qr_code_type=*/
+          base::unexpected("Data Decoder terminated unexpectedly"));
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -924,10 +947,10 @@
   EXPECT_CALL(GetApiClient(), IsAvailable(testing::_)).Times(0);
   EXPECT_CALL(*client_, InitPixAccountLinkingFlow).Times(0);
 
-  pix_manager_->OnPixCodeValidated(
-      std::nullopt,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kInvalid);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(std::nullopt,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kInvalid);
 }
 
 // If the Pix code validation in the utility process has returned `false`, then
@@ -936,10 +959,10 @@
        PayflowExitedReason_InvalidCode) {
   base::HistogramTester histogram_tester;
 
-  pix_manager_->OnPixCodeValidated(
-      std::nullopt,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kInvalid);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(std::nullopt,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kInvalid);
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -953,10 +976,10 @@
        PayflowExitedReason_NoLinkedAccount) {
   base::HistogramTester histogram_tester;
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -974,10 +997,10 @@
   EXPECT_CALL(GetApiClient(), IsAvailable(testing::_)).Times(0);
   EXPECT_CALL(*client_, InitPixAccountLinkingFlow).Times(0);
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kStatic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kStatic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kStatic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kStatic);
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -994,10 +1017,10 @@
   feature_list.InitAndEnableFeature(kEnableStaticQrCodeForPix);
   EXPECT_CALL(GetApiClient(), IsAvailable(testing::_)).Times(1);
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kStatic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kStatic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kStatic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kStatic);
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PaymentCodeValidation.Result",
@@ -1017,10 +1040,10 @@
   EXPECT_CALL(GetApiClient(), IsAvailable(testing::_)).Times(0);
   EXPECT_CALL(*client_, InitPixAccountLinkingFlow).Times(0);
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 }
 
 // If the payments autofill pref is disabled, neither the payflow nor the
@@ -1035,10 +1058,10 @@
   EXPECT_CALL(GetApiClient(), IsAvailable(testing::_)).Times(0);
   EXPECT_CALL(*client_, InitPixAccountLinkingFlow(testing::_)).Times(0);
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 }
 
 // If the user has turned off autofilling payment methods, the
@@ -1051,10 +1074,10 @@
   // Disable payment methods pref.
   autofill::prefs::SetAutofillPaymentMethodsEnabled(pref_service_.get(), false);
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -1073,10 +1096,10 @@
   EXPECT_CALL(GetApiClient(), IsAvailable(testing::_)).Times(0);
   EXPECT_CALL(*client_, InitPixAccountLinkingFlow(testing::_)).Times(0);
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 }
 
 // If the user has opted out of the Pix flow, the PayflowExitedReason
@@ -1088,10 +1111,10 @@
       CreatePixBankAccount(/*instrument_id=*/1));
   autofill::prefs::SetFacilitatedPaymentsPix(pref_service_.get(), false);
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -1108,10 +1131,10 @@
   EXPECT_CALL(GetApiClient(), IsAvailable(testing::_)).Times(0);
   EXPECT_CALL(*client_, InitPixAccountLinkingFlow(testing::_));
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 }
 
 // If the account linking flag is disabled, the account linking flow shouldn't
@@ -1124,10 +1147,10 @@
 
   EXPECT_CALL(*client_, InitPixAccountLinkingFlow(testing::_)).Times(0);
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 }
 
 // Verify that the API check result and latency are logged.
@@ -1135,9 +1158,10 @@
        LogApiAvailabilityCheckResultAndLatency) {
   base::HistogramTester histogram_tester;
 
-  pix_manager_->OnApiAvailabilityReceived(
-      /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
-      /*is_api_available=*/true);
+  test_api(*pix_manager_)
+      .OnApiAvailabilityReceived(
+          /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
+          /*is_api_available=*/true);
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.IsApiAvailable.Success.Latency",
@@ -1152,8 +1176,9 @@
        PayflowExitedReason_ApiClientNotAvailable) {
   base::HistogramTester histogram_tester;
 
-  pix_manager_->OnApiAvailabilityReceived(/*start_time=*/base::TimeTicks::Now(),
-                                          /*is_api_available=*/false);
+  test_api(*pix_manager_)
+      .OnApiAvailabilityReceived(/*start_time=*/base::TimeTicks::Now(),
+                                 /*is_api_available=*/false);
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -1169,8 +1194,9 @@
 
   EXPECT_CALL(*client_, ShowErrorScreen);
 
-  pix_manager_->OnPurchaseActionResult(/*start_time=*/base::TimeTicks::Now(),
-                                       PurchaseActionResult::kCouldNotInvoke);
+  test_api(*pix_manager_)
+      .OnPurchaseActionResult(/*start_time=*/base::TimeTicks::Now(),
+                              PurchaseActionResult::kCouldNotInvoke);
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -1186,8 +1212,9 @@
   // received, and again when the test fixture destroys the `pix_manager_`.
   EXPECT_CALL(*client_, DismissPrompt).Times(2);
 
-  pix_manager_->OnPurchaseActionResult(/*start_time=*/base::TimeTicks::Now(),
-                                       PurchaseActionResult::kResultOk);
+  test_api(*pix_manager_)
+      .OnPurchaseActionResult(/*start_time=*/base::TimeTicks::Now(),
+                              PurchaseActionResult::kResultOk);
 }
 
 // Test that when Chrome is successful in invoking the purchase action, the UI
@@ -1198,8 +1225,9 @@
   // received, and again when the test fixture destroys the `pix_manager_`.
   EXPECT_CALL(*client_, DismissPrompt).Times(2);
 
-  pix_manager_->OnPurchaseActionResult(/*start_time=*/base::TimeTicks::Now(),
-                                       PurchaseActionResult::kResultCanceled);
+  test_api(*pix_manager_)
+      .OnPurchaseActionResult(/*start_time=*/base::TimeTicks::Now(),
+                              PurchaseActionResult::kResultCanceled);
 }
 
 // Test that when an InitiatePurchaseAction request is sent, the attempt is
@@ -1213,10 +1241,12 @@
   auto response_details =
       std::make_unique<FacilitatedPaymentsInitiatePaymentResponseDetails>();
   response_details->secure_payload_ = CreateSecurePayload();
-  pix_manager_->OnInitiatePaymentResponseReceived(
-      /*start_time=*/base::TimeTicks::Now(),
-      autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::kSuccess,
-      std::move(response_details));
+  test_api(*pix_manager_)
+      .OnInitiatePaymentResponseReceived(
+          /*start_time=*/base::TimeTicks::Now(),
+          autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::
+              kSuccess,
+          std::move(response_details));
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.InitiatePurchaseAction.Attempt",
@@ -1239,13 +1269,16 @@
     auto response_details =
         std::make_unique<FacilitatedPaymentsInitiatePaymentResponseDetails>();
     response_details->secure_payload_ = CreateSecurePayload();
-    pix_manager_->OnInitiatePaymentResponseReceived(
-        /*start_time=*/base::TimeTicks::Now(),
-        autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::kSuccess,
-        std::move(response_details));
+    test_api(*pix_manager_)
+        .OnInitiatePaymentResponseReceived(
+            /*start_time=*/base::TimeTicks::Now(),
+            autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::
+                kSuccess,
+            std::move(response_details));
 
-    pix_manager_->OnPurchaseActionResult(
-        /*start_time=*/base::TimeTicks::Now() - base::Seconds(2), result);
+    test_api(*pix_manager_)
+        .OnPurchaseActionResult(
+            /*start_time=*/base::TimeTicks::Now() - base::Seconds(2), result);
 
     std::string result_string;
     switch (result) {
@@ -1305,8 +1338,9 @@
         break;
     }
 
-    pix_manager_->OnPurchaseActionResult(
-        /*start_time=*/base::TimeTicks::Now(), result);
+    test_api(*pix_manager_)
+        .OnPurchaseActionResult(
+            /*start_time=*/base::TimeTicks::Now(), result);
 
     histogram_tester.ExpectBucketCount(
         base::StrCat({"FacilitatedPayments.Pix.Transaction.", result_string,
@@ -1345,8 +1379,9 @@
   for (PurchaseActionResult result :
        {PurchaseActionResult::kResultOk, PurchaseActionResult::kCouldNotInvoke,
         PurchaseActionResult::kResultCanceled}) {
-    pix_manager_->OnPurchaseActionResult(
-        /*start_time=*/base::TimeTicks::Now(), result);
+    test_api(*pix_manager_)
+        .OnPurchaseActionResult(
+            /*start_time=*/base::TimeTicks::Now(), result);
 
     histogram_tester.ExpectUniqueSample(
         base::StrCat({"FacilitatedPayments.Pix.Transaction.MainFrame.",
@@ -1392,8 +1427,9 @@
   for (PurchaseActionResult result :
        {PurchaseActionResult::kResultOk, PurchaseActionResult::kCouldNotInvoke,
         PurchaseActionResult::kResultCanceled}) {
-    pix_manager_->OnPurchaseActionResult(
-        /*start_time=*/base::TimeTicks::Now(), result);
+    test_api(*pix_manager_)
+        .OnPurchaseActionResult(
+            /*start_time=*/base::TimeTicks::Now(), result);
 
     histogram_tester.ExpectUniqueSample(
         base::StrCat({"FacilitatedPayments.Pix.Transaction.MainFrame.",
@@ -1414,14 +1450,14 @@
   payments_data_manager_->AddMaskedBankAccountForTest(
       CreatePixBankAccount(/*instrument_id=*/1));
 
-  EXPECT_EQ(nullptr, pix_manager_->api_client_.get());
+  EXPECT_EQ(nullptr, test_api(*pix_manager_).api_client());
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 
-  EXPECT_NE(nullptr, pix_manager_->api_client_.get());
+  EXPECT_NE(nullptr, test_api(*pix_manager_).api_client());
 }
 
 // Verify that a failure to lazily initialize the API client is not fatal.
@@ -1429,16 +1465,16 @@
        HandlesFailureToLazilyInitializeApiClient) {
   payments_data_manager_->AddMaskedBankAccountForTest(
       CreatePixBankAccount(/*instrument_id=*/1));
-  pix_manager_->api_client_creator_.Reset();
+  test_api(*pix_manager_).api_client_creator().Reset();
 
-  EXPECT_EQ(nullptr, pix_manager_->api_client_.get());
+  EXPECT_EQ(nullptr, test_api(*pix_manager_).api_client());
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 
-  EXPECT_EQ(nullptr, pix_manager_->api_client_.get());
+  EXPECT_EQ(nullptr, test_api(*pix_manager_).api_client());
 }
 
 // Test class for devices being used in the landscape mode.
@@ -1473,10 +1509,10 @@
   EXPECT_CALL(GetApiClient(), IsAvailable(testing::_))
       .Times(IsPaymentEnabledInLandscapeMode() ? 1 : 0);
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 }
 
 TEST_P(PixManagerTestInLandscapeMode,
@@ -1485,10 +1521,10 @@
   payments_data_manager_->AddMaskedBankAccountForTest(
       CreatePixBankAccount(/*instrument_id=*/1));
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 
   // In landscape mode, if the `EnablePixPaymentsInLandscapeMode` flag is
   // disabled, Pix payment is not offered, and a histogram should be logged.
@@ -1500,7 +1536,7 @@
 
 TEST_P(PixManagerTestWithAccountLinkingEnabled, ShowPixPaymentPrompt) {
   // Verify the default UI state.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kHidden);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kHidden);
 
   // Verify that when the feature wants to show the payment prompt, it asks the
   // client.
@@ -1508,39 +1544,39 @@
 
   const std::vector<autofill::BankAccount> bank_accounts = {
       autofill::test::CreatePixBankAccount(100L)};
-  pix_manager_->ShowPixPaymentPrompt(std::move(bank_accounts),
-                                     base::DoNothing());
+  test_api(*pix_manager_)
+      .ShowPixPaymentPrompt(std::move(bank_accounts), base::DoNothing());
 
   // Verify that the UI state is updated.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kFopSelector);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kFopSelector);
 }
 
 TEST_P(PixManagerTestWithAccountLinkingEnabled, ShowProgressScreen) {
   // Verify the default UI state.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kHidden);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kHidden);
 
   // Verify that when the feature wants to show the progress screen, it asks the
   // client.
   EXPECT_CALL(*client_, ShowProgressScreen);
 
-  pix_manager_->ShowProgressScreen();
+  test_api(*pix_manager_).ShowProgressScreen();
 
   // Verify that the UI state is updated.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kProgressScreen);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kProgressScreen);
 }
 
 TEST_P(PixManagerTestWithAccountLinkingEnabled, ShowErrorScreen) {
   // Verify the default UI state.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kHidden);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kHidden);
 
   // Verify that when the feature wants to show the error screen, it asks the
   // client.
   EXPECT_CALL(*client_, ShowErrorScreen);
 
-  pix_manager_->ShowErrorScreen();
+  test_api(*pix_manager_).ShowErrorScreen();
 
   // Verify that the UI state is updated.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kErrorScreen);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kErrorScreen);
 }
 
 TEST_P(PixManagerTestWithAccountLinkingEnabled, DismissPrompt) {
@@ -1548,10 +1584,10 @@
   // client. The second call is from test teardown.
   EXPECT_CALL(*client_, DismissPrompt).Times(2);
 
-  pix_manager_->DismissPrompt();
+  test_api(*pix_manager_).DismissPrompt();
 
   // Verify that the UI state is updated.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kHidden);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kHidden);
 }
 
 // Test that when the Pix FOP selector is shown, related Pix metrics are logged.
@@ -1570,9 +1606,9 @@
   // Simulate that the FOP selector was shown successfully.
   std::vector<autofill::BankAccount> bank_accounts = {
       autofill::test::CreatePixBankAccount(100L)};
-  pix_manager_->ShowPixPaymentPrompt(std::move(bank_accounts),
-                                     base::DoNothing());
-  pix_manager_->OnUiScreenEvent(UiEvent::kNewScreenShown);
+  test_api(*pix_manager_)
+      .ShowPixPaymentPrompt(std::move(bank_accounts), base::DoNothing());
+  test_api(*pix_manager_).OnUiScreenEvent(UiEvent::kNewScreenShown);
 
   // Verify that when the Pix FOP selector is shown, related metrics are
   // logged.
@@ -1590,7 +1626,7 @@
 TEST_P(PixManagerTestWithAccountLinkingEnabled,
        ProgressScreenAutoDismissedAfterInvokingPurchaseAction) {
   // When purchase action is invoked, the progress screen would be showing.
-  pix_manager_->ShowProgressScreen();
+  test_api(*pix_manager_).ShowProgressScreen();
   ON_CALL(*client_, GetCoreAccountInfo)
       .WillByDefault(testing::Return(CreateLoggedInAccountInfo()));
 
@@ -1599,19 +1635,21 @@
   auto response_details =
       std::make_unique<FacilitatedPaymentsInitiatePaymentResponseDetails>();
   response_details->secure_payload_ = CreateSecurePayload();
-  pix_manager_->OnInitiatePaymentResponseReceived(
-      /*start_time=*/base::TimeTicks::Now(),
-      autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::kSuccess,
-      std::move(response_details));
+  test_api(*pix_manager_)
+      .OnInitiatePaymentResponseReceived(
+          /*start_time=*/base::TimeTicks::Now(),
+          autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::
+              kSuccess,
+          std::move(response_details));
 
   // The progress screen is persisted for a short duration after invoking the
   // purchase action for a smooth transition to the platform screen.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kProgressScreen);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kProgressScreen);
 
   FastForwardBy(base::Seconds(2));
 
   // The progress screen should be dismissed after a short delay.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kHidden);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kHidden);
 }
 
 TEST_P(PixManagerTestWithAccountLinkingEnabled,
@@ -1626,10 +1664,10 @@
   payments_data_manager_->AddMaskedBankAccountForTest(
       CreatePixBankAccount(/*instrument_id=*/1));
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -1650,16 +1688,16 @@
       CreatePixBankAccount(/*instrument_id=*/1));
   EXPECT_CALL(GetApiClient(), IsAvailable(testing::_));
 
-  pix_manager_->OnPixCodeValidated(
-      PixCodeRustValidationResult::kDynamic,
-      /*pix_code=*/std::string(), base::TimeTicks::Now(),
-      /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
+  test_api(*pix_manager_)
+      .OnPixCodeValidated(PixCodeRustValidationResult::kDynamic,
+                          /*pix_code=*/std::string(), base::TimeTicks::Now(),
+                          /*pix_qr_code_type=*/mojom::PixQrCodeType::kDynamic);
 }
 
 TEST_P(PixManagerTestWithAccountLinkingEnabled,
        ErrorScreenNotAutoDismissedAfterInvokingPurchaseAction) {
   // When purchase action is invoked, the progress screen would be showing.
-  pix_manager_->ShowProgressScreen();
+  test_api(*pix_manager_).ShowProgressScreen();
   ON_CALL(*client_, GetCoreAccountInfo)
       .WillByDefault(testing::Return(CreateLoggedInAccountInfo()));
 
@@ -1668,19 +1706,22 @@
   auto response_details =
       std::make_unique<FacilitatedPaymentsInitiatePaymentResponseDetails>();
   response_details->secure_payload_ = CreateSecurePayload();
-  pix_manager_->OnInitiatePaymentResponseReceived(
-      /*start_time=*/base::TimeTicks::Now(),
-      autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::kSuccess,
-      std::move(response_details));
+  test_api(*pix_manager_)
+      .OnInitiatePaymentResponseReceived(
+          /*start_time=*/base::TimeTicks::Now(),
+          autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::
+              kSuccess,
+          std::move(response_details));
 
   // If the purchase action could not be invoked, the `PurchaseActionResult` is
   // returned immediately. The error screen is shown.
-  pix_manager_->OnPurchaseActionResult(/*start_time=*/base::TimeTicks::Now(),
-                                       PurchaseActionResult::kCouldNotInvoke);
+  test_api(*pix_manager_)
+      .OnPurchaseActionResult(/*start_time=*/base::TimeTicks::Now(),
+                              PurchaseActionResult::kCouldNotInvoke);
   FastForwardBy(base::Seconds(1));
 
   // The error screen shouldn't be auto-dismissed.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kErrorScreen);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kErrorScreen);
 }
 
 class PixManagerTestForUiScreens : public PixManagerTest,
@@ -1690,21 +1731,21 @@
     PixManagerTest::SetUp();
 
     // Default state.
-    EXPECT_EQ(pix_manager_->ui_state_, UiState::kHidden);
+    EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kHidden);
 
     switch (GetParam()) {
       case UiState::kFopSelector: {
         const std::vector<autofill::BankAccount> bank_accounts = {
             autofill::test::CreatePixBankAccount(100L)};
-        pix_manager_->ShowPixPaymentPrompt(std::move(bank_accounts),
-                                           base::DoNothing());
+        test_api(*pix_manager_)
+            .ShowPixPaymentPrompt(std::move(bank_accounts), base::DoNothing());
         break;
       }
       case UiState::kProgressScreen:
-        pix_manager_->ShowProgressScreen();
+        test_api(*pix_manager_).ShowProgressScreen();
         break;
       case UiState::kErrorScreen:
-        pix_manager_->ShowErrorScreen();
+        test_api(*pix_manager_).ShowErrorScreen();
         break;
       case UiState::kHidden:
         NOTREACHED();
@@ -1726,10 +1767,10 @@
   base::HistogramTester histogram_tester;
 
   // Simulate new screen was shown successfully.
-  pix_manager_->OnUiScreenEvent(UiEvent::kNewScreenShown);
+  test_api(*pix_manager_).OnUiScreenEvent(UiEvent::kNewScreenShown);
 
   // Verify feature has updated the UI state.
-  EXPECT_EQ(pix_manager_->ui_state_, ui_state());
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), ui_state());
   // Verify that the histogram is logged.
   histogram_tester.ExpectUniqueSample("FacilitatedPayments.Pix.UiScreenShown",
                                       /*sample=*/ui_state(),
@@ -1748,10 +1789,10 @@
   base::HistogramTester histogram_tester;
 
   // Simulate new screen could not be shown.
-  pix_manager_->OnUiScreenEvent(UiEvent::kScreenClosedNotByUser);
+  test_api(*pix_manager_).OnUiScreenEvent(UiEvent::kScreenClosedNotByUser);
 
   // Verify that the UI state is hidden.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kHidden);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kHidden);
   // Verify that the payflow exited histogram is logged.
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -1765,12 +1806,12 @@
   base::HistogramTester histogram_tester;
 
   // Simulate new screen was shown successfully.
-  pix_manager_->OnUiScreenEvent(UiEvent::kNewScreenShown);
+  test_api(*pix_manager_).OnUiScreenEvent(UiEvent::kNewScreenShown);
   // Simulate UI screen was closed, but it was not due to a user action.
-  pix_manager_->OnUiScreenEvent(UiEvent::kScreenClosedNotByUser);
+  test_api(*pix_manager_).OnUiScreenEvent(UiEvent::kScreenClosedNotByUser);
 
   // Verify that the UI state is hidden.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kHidden);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kHidden);
   // Verify that the payflow exited histogram is logged.
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -1784,12 +1825,12 @@
   base::HistogramTester histogram_tester;
 
   // Simulate new screen was shown successfully.
-  pix_manager_->OnUiScreenEvent(UiEvent::kNewScreenShown);
+  test_api(*pix_manager_).OnUiScreenEvent(UiEvent::kNewScreenShown);
   // Simulate UI screen was closed by the user.
-  pix_manager_->OnUiScreenEvent(UiEvent::kScreenClosedByUser);
+  test_api(*pix_manager_).OnUiScreenEvent(UiEvent::kScreenClosedByUser);
 
   // Verify that the UI state is hidden.
-  EXPECT_EQ(pix_manager_->ui_state_, UiState::kHidden);
+  EXPECT_EQ(test_api(*pix_manager_).ui_state(), UiState::kHidden);
   // Verify that the payflow exited histogram is logged.
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -1836,7 +1877,7 @@
   EXPECT_CALL(*payments_network_interface_,
               InitiatePayment(testing::_, testing::_, testing::_));
 
-  pix_manager_->SendInitiatePaymentRequest();
+  test_api(*pix_manager_).SendInitiatePaymentRequest();
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.InitiatePayment.Attempt",
@@ -1851,7 +1892,7 @@
 TEST_F(PixManagerPaymentsNetworkInterfaceTest,
        OnInitiatePaymentResponseReceived_FailureResponse) {
   base::HistogramTester histogram_tester;
-  pix_manager_->SendInitiatePaymentRequest();
+  test_api(*pix_manager_).SendInitiatePaymentRequest();
   ON_CALL(*client_, GetCoreAccountInfo)
       .WillByDefault(testing::Return(CreateLoggedInAccountInfo()));
 
@@ -1861,11 +1902,12 @@
   auto response_details =
       std::make_unique<FacilitatedPaymentsInitiatePaymentResponseDetails>();
   response_details->secure_payload_ = CreateSecurePayload();
-  pix_manager_->OnInitiatePaymentResponseReceived(
-      /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
-      autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::
-          kPermanentFailure,
-      std::move(response_details));
+  test_api(*pix_manager_)
+      .OnInitiatePaymentResponseReceived(
+          /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
+          autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::
+              kPermanentFailure,
+          std::move(response_details));
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.PayflowExitedReason",
@@ -1883,7 +1925,7 @@
 TEST_F(PixManagerPaymentsNetworkInterfaceTest,
        OnInitiatePaymentResponseReceived_NoActionToken_ErrorScreenShown) {
   base::HistogramTester histogram_tester;
-  pix_manager_->SendInitiatePaymentRequest();
+  test_api(*pix_manager_).SendInitiatePaymentRequest();
   ON_CALL(*client_, GetCoreAccountInfo)
       .WillByDefault(testing::Return(CreateLoggedInAccountInfo()));
 
@@ -1892,10 +1934,12 @@
 
   auto response_details =
       std::make_unique<FacilitatedPaymentsInitiatePaymentResponseDetails>();
-  pix_manager_->OnInitiatePaymentResponseReceived(
-      /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
-      autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::kSuccess,
-      std::move(response_details));
+  test_api(*pix_manager_)
+      .OnInitiatePaymentResponseReceived(
+          /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
+          autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::
+              kSuccess,
+          std::move(response_details));
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.InitiatePayment.Success.Latency",
@@ -1912,7 +1956,7 @@
 TEST_F(PixManagerPaymentsNetworkInterfaceTest,
        OnInitiatePaymentResponseReceived_NoCoreAccountInfo_ErrorScreenShown) {
   base::HistogramTester histogram_tester;
-  pix_manager_->SendInitiatePaymentRequest();
+  test_api(*pix_manager_).SendInitiatePaymentRequest();
   ON_CALL(*client_, GetCoreAccountInfo)
       .WillByDefault(testing::Return(std::nullopt));
 
@@ -1922,10 +1966,12 @@
   auto response_details =
       std::make_unique<FacilitatedPaymentsInitiatePaymentResponseDetails>();
   response_details->secure_payload_ = CreateSecurePayload();
-  pix_manager_->OnInitiatePaymentResponseReceived(
-      /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
-      autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::kSuccess,
-      std::move(response_details));
+  test_api(*pix_manager_)
+      .OnInitiatePaymentResponseReceived(
+          /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
+          autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::
+              kSuccess,
+          std::move(response_details));
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.InitiatePayment.Success.Latency",
@@ -1942,7 +1988,7 @@
 TEST_F(PixManagerPaymentsNetworkInterfaceTest,
        OnInitiatePaymentResponseReceived_LoggedOutProfile_ErrorScreenShown) {
   base::HistogramTester histogram_tester;
-  pix_manager_->SendInitiatePaymentRequest();
+  test_api(*pix_manager_).SendInitiatePaymentRequest();
   ON_CALL(*client_, GetCoreAccountInfo)
       .WillByDefault(testing::Return(CoreAccountInfo()));
 
@@ -1952,10 +1998,12 @@
   auto response_details =
       std::make_unique<FacilitatedPaymentsInitiatePaymentResponseDetails>();
   response_details->secure_payload_ = CreateSecurePayload();
-  pix_manager_->OnInitiatePaymentResponseReceived(
-      /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
-      autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::kSuccess,
-      std::move(response_details));
+  test_api(*pix_manager_)
+      .OnInitiatePaymentResponseReceived(
+          /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
+          autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::
+              kSuccess,
+          std::move(response_details));
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.InitiatePayment.Success.Latency",
@@ -1972,7 +2020,7 @@
 TEST_F(PixManagerPaymentsNetworkInterfaceTest,
        OnInitiatePaymentResponseReceived_InvokePurchaseActionTriggered) {
   base::HistogramTester histogram_tester;
-  pix_manager_->SendInitiatePaymentRequest();
+  test_api(*pix_manager_).SendInitiatePaymentRequest();
   ON_CALL(*client_, GetCoreAccountInfo)
       .WillByDefault(testing::Return(CreateLoggedInAccountInfo()));
 
@@ -1981,10 +2029,12 @@
   auto response_details =
       std::make_unique<FacilitatedPaymentsInitiatePaymentResponseDetails>();
   response_details->secure_payload_ = CreateSecurePayload();
-  pix_manager_->OnInitiatePaymentResponseReceived(
-      /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
-      autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::kSuccess,
-      std::move(response_details));
+  test_api(*pix_manager_)
+      .OnInitiatePaymentResponseReceived(
+          /*start_time=*/base::TimeTicks::Now() - base::Seconds(2),
+          autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::
+              kSuccess,
+          std::move(response_details));
 
   histogram_tester.ExpectUniqueSample(
       "FacilitatedPayments.Pix.InitiatePayment.Success.Latency",
@@ -1997,10 +2047,10 @@
 TEST_F(PixManagerPaymentsNetworkInterfaceTest, Reset) {
   EXPECT_CALL(*payments_network_interface_, InitiatePayment);
 
-  pix_manager_->SendInitiatePaymentRequest();
+  test_api(*pix_manager_).SendInitiatePaymentRequest();
   pix_manager_->Reset();
 
-  EXPECT_FALSE(pix_manager_->weak_ptr_factory_.HasWeakPtrs());
+  EXPECT_FALSE(test_api(*pix_manager_).HasWeakPtrs());
 }
 
 }  // namespace payments::facilitated
diff --git a/components/infobars/core/infobar_container_with_priority.cc b/components/infobars/core/infobar_container_with_priority.cc
index 2ceeb84..0636597 100644
--- a/components/infobars/core/infobar_container_with_priority.cc
+++ b/components/infobars/core/infobar_container_with_priority.cc
@@ -77,6 +77,12 @@
     if (!pending_infobars_.empty()) {
       base::UmaHistogramCounts100("InfoBar.Prioritization.StarvedCount",
                                   pending_infobars_.size());
+      for (const auto& entry : pending_infobars_) {
+        if (entry.infobar && entry.infobar->delegate()) {
+          base::UmaHistogramSparse("InfoBar.Prioritization.Starved",
+                                   entry.infobar->delegate()->GetIdentifier());
+        }
+      }
     }
 
     pending_infobars_.clear();
diff --git a/components/infobars/core/infobar_container_with_priority_unittest.cc b/components/infobars/core/infobar_container_with_priority_unittest.cc
index 7d73bbc..f0d00cb 100644
--- a/components/infobars/core/infobar_container_with_priority_unittest.cc
+++ b/components/infobars/core/infobar_container_with_priority_unittest.cc
@@ -537,6 +537,9 @@
 
   histogram_tester_.ExpectUniqueSample("InfoBar.Prioritization.StarvedCount", 3,
                                        1);
+  histogram_tester_.ExpectBucketCount(
+      "InfoBar.Prioritization.Starved",
+      InfoBarDelegate::ALTERNATE_NAV_INFOBAR_DELEGATE, 3);
 }
 
 TEST_F(InfoBarContainerWithPriorityTest, NoAnimationOnManagerChange) {
diff --git a/components/live_caption/views/translation_view_wrapper_base.cc b/components/live_caption/views/translation_view_wrapper_base.cc
index c3bd5650..7767a3b 100644
--- a/components/live_caption/views/translation_view_wrapper_base.cc
+++ b/components/live_caption/views/translation_view_wrapper_base.cc
@@ -34,6 +34,7 @@
 #include "ui/views/controls/menu/menu_runner.h"
 #include "ui/views/layout/box_layout.h"
 #include "ui/views/view.h"
+#include "ui/views/view_utils.h"
 
 namespace captions {
 namespace {
diff --git a/components/metrics/content/subprocess_metrics_provider.cc b/components/metrics/content/subprocess_metrics_provider.cc
index fd2a09b..a479932 100644
--- a/components/metrics/content/subprocess_metrics_provider.cc
+++ b/components/metrics/content/subprocess_metrics_provider.cc
@@ -273,7 +273,7 @@
     }
     // We expect histograms to match as subprocesses shouldn't have version skew
     // with the browser process.
-    bool merge_success =
+    base::PersistentHistogramAllocator::MergeResult result =
         allocator_ptr->MergeHistogramDeltaToStatisticsRecorder(histogram.get());
 
     // When merging child process histograms into the parent, we expect the
@@ -281,9 +281,11 @@
     // different types or buckets, which indicates a programming error (i.e.
     // non-matching logging code across browser and child for a histogram). In
     // this case DumpWithoutCrashing() with a crash key with the histogram name.
-    if (!merge_success) {
+    if (result != base::PersistentHistogramAllocator::MergeResult::kSuccess) {
       SCOPED_CRASH_KEY_STRING256("SubprocessMetricsProvider", "histogram",
                                  histogram->histogram_name());
+      SCOPED_CRASH_KEY_NUMBER("SubprocessMetricsProvider", "merge_result",
+                              static_cast<int>(result));
       base::debug::DumpWithoutCrashing();
     }
     ++histogram_count;
diff --git a/components/optimization_guide/core/BUILD.gn b/components/optimization_guide/core/BUILD.gn
index 1e6cd500..1b9d0e086 100644
--- a/components/optimization_guide/core/BUILD.gn
+++ b/components/optimization_guide/core/BUILD.gn
@@ -373,6 +373,34 @@
   }
 }
 
+source_set("manifest_broker") {
+  sources = [
+    "model_execution/manifest_broker/manifest.cc",
+    "model_execution/manifest_broker/manifest.h",
+  ]
+  deps = [
+    ":ondevice",
+    "//base",
+    "//components/optimization_guide/proto:optimization_guide_proto",
+    "//components/optimization_guide/public/mojom",
+  ]
+}
+
+static_library("manifest_broker_test_support") {
+  sources = [
+    "model_execution/manifest_broker/test/example_manifest.cc",
+    "model_execution/manifest_broker/test/example_manifest.h",
+    "model_execution/manifest_broker/test/manifest_builder.cc",
+    "model_execution/manifest_broker/test/manifest_builder.h",
+  ]
+  deps = [
+    ":manifest_broker",
+    "//base",
+    "//components/optimization_guide/proto:optimization_guide_proto",
+    "//components/optimization_guide/public/mojom",
+  ]
+}
+
 static_library("core") {
   sources = [
     "noisy_metrics_recorder.cc",
@@ -739,6 +767,7 @@
       "model_execution/aqa_response_parser_unittest.cc",
       "model_execution/fieldwise_response_parser_unittest.cc",
       "model_execution/json_response_parser_unittest.cc",
+      "model_execution/manifest_broker/manifest_unittest.cc",
       "model_execution/model_broker_client_unittest.cc",
       "model_execution/model_execution_features_controller_unittest.cc",
       "model_execution/model_execution_features_unittest.cc",
@@ -765,6 +794,8 @@
       "model_quality/model_execution_logging_wrappers_unittest.cc",
     ]
     deps += [
+      ":manifest_broker",
+      ":manifest_broker_test_support",
       ":prediction",
       "//components/component_updater",
       "//components/optimization_guide/public/mojom",
diff --git a/components/optimization_guide/core/model_execution/manifest_broker/manifest.cc b/components/optimization_guide/core/model_execution/manifest_broker/manifest.cc
new file mode 100644
index 0000000..08862ef
--- /dev/null
+++ b/components/optimization_guide/core/model_execution/manifest_broker/manifest.cc
@@ -0,0 +1,295 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/optimization_guide/core/model_execution/manifest_broker/manifest.h"
+
+#include <optional>
+
+#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
+#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
+
+namespace optimization_guide {
+
+namespace {
+
+proto::DeviceCategoryConfig SelectDeviceCategoryConfig(
+    const proto::Manifest& manifest,
+    DeviceCategory device_category) {
+  auto it = manifest.category_configs().find(base::ToString(device_category));
+  if (it == manifest.category_configs().end()) {
+    return {};
+  }
+  return it->second;
+}
+
+// Validates that all identifiers are unique across all types.
+std::optional<Manifest::ParseError> CheckUniqueIdentifiers(
+    const proto::Manifest& manifest) {
+  absl::flat_hash_set<std::string> asset_identifiers;
+  asset_identifiers.emplace(kManifestAssetName);
+  // Check that all asset ids are unique.
+  for (const auto& [id, _] : manifest.assets().on_demand_components()) {
+    if (!asset_identifiers.insert(id).second) {
+      return Manifest::ParseError::kDuplicateIdentifier;
+    }
+  }
+
+  absl::flat_hash_set<std::string> model_identifiers;
+  for (const auto& [id, _] : manifest.recipes().adaptations()) {
+    if (!model_identifiers.insert(id).second) {
+      return Manifest::ParseError::kDuplicateIdentifier;
+    }
+  }
+  for (const auto& [id, _] : manifest.recipes().base_models()) {
+    if (!model_identifiers.insert(id).second) {
+      return Manifest::ParseError::kDuplicateIdentifier;
+    }
+  }
+
+  return std::nullopt;
+}
+
+// Validates that all referenced identifiers exist and are of the correct type.
+std::optional<Manifest::ParseError> ValidateReferences(
+    const proto::Manifest& manifest) {
+  const proto::Recipes& recipes = manifest.recipes();
+  auto is_valid_asset = [&](const std::string& asset_id) {
+    return asset_id == kManifestAssetName ||
+           manifest.assets().on_demand_components().contains(asset_id);
+  };
+  for (const auto& [_, solution] : manifest.recipes().solutions()) {
+    if (!recipes.adaptations().contains(solution.model_recipe_id()) &&
+        !recipes.base_models().contains(solution.model_recipe_id())) {
+      return Manifest::ParseError::kMissingIdentifier;
+    }
+    if (!solution.safety_model_recipe_id().empty() &&
+        !recipes.safety_models().contains(solution.safety_model_recipe_id())) {
+      return Manifest::ParseError::kMissingIdentifier;
+    }
+    if (!is_valid_asset(solution.config_file().asset_id())) {
+      return Manifest::ParseError::kMissingIdentifier;
+    }
+  }
+
+  for (const auto& [_, adaptation] : manifest.recipes().adaptations()) {
+    if (!recipes.base_models().contains(adaptation.base_model_recipe_id())) {
+      return Manifest::ParseError::kMissingIdentifier;
+    }
+    if (!is_valid_asset(adaptation.weights_file().asset_id())) {
+      return Manifest::ParseError::kMissingIdentifier;
+    }
+  }
+
+  for (const auto& [_, base_model] : manifest.recipes().base_models()) {
+    if (!is_valid_asset(base_model.weights_file().asset_id())) {
+      return Manifest::ParseError::kMissingIdentifier;
+    }
+  }
+
+  for (const auto& [_, safety_model] : manifest.recipes().safety_models()) {
+    if (!is_valid_asset(safety_model.weights_file().asset_id())) {
+      return Manifest::ParseError::kMissingIdentifier;
+    }
+  }
+
+  for (const auto& [category, config] : manifest.category_configs()) {
+    for (const auto& [use_case, use_case_config] : config.use_cases()) {
+      if (!recipes.solutions().contains(use_case_config.solution_recipe_id())) {
+        return Manifest::ParseError::kMissingIdentifier;
+      }
+    }
+  }
+
+  return std::nullopt;
+}
+
+struct References {
+  absl::flat_hash_set<std::string> solutions;
+  absl::flat_hash_set<std::string> safety_models;
+  absl::flat_hash_set<std::string> adaptations;
+  absl::flat_hash_set<std::string> base_models;
+  absl::flat_hash_set<Manifest::AssetId> assets;
+
+  // Adds references to the solutions for all of the use cases.
+  void AddAllUseCases(const proto::DeviceCategoryConfig& config) {
+    for (const auto& [_, use_case_config] : config.use_cases()) {
+      solutions.insert(use_case_config.solution_recipe_id());
+    }
+  }
+
+  // Adds the solution for the given use case, if it is defined.  Returns true
+  // if the use case was found, false otherwise.
+  bool AddUseCase(const proto::DeviceCategoryConfig& config,
+                  const Manifest::UseCaseName& name) {
+    auto it = config.use_cases().find(name);
+    if (it != config.use_cases().end()) {
+      solutions.insert(it->second.solution_recipe_id());
+      return true;
+    }
+    return false;
+  }
+
+  // Adds all of the recipes and assets that are needed to support the already
+  // referenced recipes.  Recipes should be a valid transitive closure, and
+  // references should be valid against the recipes.
+  void AddDependencies(const proto::Recipes& recipes) {
+    for (const auto& id : solutions) {
+      const proto::SolutionRecipe& solution = recipes.solutions().at(id);
+      if (recipes.adaptations().contains(solution.model_recipe_id())) {
+        adaptations.insert(solution.model_recipe_id());
+      } else {
+        base_models.insert(solution.model_recipe_id());
+      }
+      if (solution.has_safety_model_recipe_id()) {
+        safety_models.insert(solution.safety_model_recipe_id());
+      }
+      assets.insert(solution.config_file().asset_id());
+    }
+    for (const auto& id : adaptations) {
+      const proto::AdaptationRecipe& adaptation = recipes.adaptations().at(id);
+      base_models.insert(adaptation.base_model_recipe_id());
+      assets.insert(adaptation.weights_file().asset_id());
+    }
+    for (const auto& id : base_models) {
+      const proto::BaseModelRecipe& base_model = recipes.base_models().at(id);
+      assets.insert(base_model.weights_file().asset_id());
+    }
+    for (const auto& id : safety_models) {
+      const proto::SafetyModelRecipe& safety_model =
+          recipes.safety_models().at(id);
+      assets.insert(safety_model.weights_file().asset_id());
+    }
+  }
+
+  // Constructs a new Recipes message with only the referenced recipes.
+  proto::Recipes CopyReferencedRecipes(const proto::Recipes& original) {
+    proto::Recipes referenced;
+    for (const auto& id : solutions) {
+      referenced.mutable_solutions()->emplace(id, original.solutions().at(id));
+    }
+    for (const auto& id : adaptations) {
+      referenced.mutable_adaptations()->emplace(id,
+                                                original.adaptations().at(id));
+    }
+    for (const auto& id : base_models) {
+      referenced.mutable_base_models()->emplace(id,
+                                                original.base_models().at(id));
+    }
+    for (const auto& id : safety_models) {
+      referenced.mutable_safety_models()->emplace(
+          id, original.safety_models().at(id));
+    }
+    return referenced;
+  }
+
+  // Constructs a new Assets message with only the referenced assets.
+  proto::Assets CopyReferencedAssets(const proto::Assets& original) {
+    proto::Assets referenced;
+    for (const auto& id : assets) {
+      if (id == kManifestAssetName) {
+        continue;
+      }
+      referenced.mutable_on_demand_components()->emplace(
+          id, original.on_demand_components().at(id));
+    }
+    return referenced;
+  }
+};
+
+// Validates that there are no two components with the same public key.
+// This would cause a conflict, as we can only download one version of a
+// component.
+std::optional<Manifest::ParseError> ValidateUniqueComponent(
+    const proto::Assets& assets) {
+  absl::flat_hash_set<std::string> public_keys;
+  for (const auto& [id, component] : assets.on_demand_components()) {
+    if (!public_keys.insert(component.public_key()).second) {
+      return Manifest::ParseError::kConflictingComponent;
+    }
+  }
+  return std::nullopt;
+}
+
+}  // namespace
+
+std::ostream& operator<<(std::ostream& stream, DeviceCategory device_category) {
+  switch (device_category) {
+    case DeviceCategory::kGpuHighTier:
+      stream << "gpu_high_tier";
+      break;
+    case DeviceCategory::kGpuLowTier:
+      stream << "gpu_low_tier";
+      break;
+    case DeviceCategory::kCpu:
+      stream << "cpu";
+      break;
+  }
+  return stream;
+}
+
+// static
+base::expected<Manifest, Manifest::ParseError> Manifest::Create(
+    proto::Manifest manifest,
+    DeviceCategory device_category) {
+  if (auto error = CheckUniqueIdentifiers(manifest); error.has_value()) {
+    return base::unexpected(error.value());
+  }
+
+  if (auto error = ValidateReferences(manifest); error.has_value()) {
+    return base::unexpected(error.value());
+  }
+
+  proto::DeviceCategoryConfig device_category_config =
+      SelectDeviceCategoryConfig(manifest, device_category);
+
+  // Narrow down the recipe maps to only those recipes that are reachable for
+  // the use cases relevant to the device category.
+  References references;
+  references.AddAllUseCases(device_category_config);
+  references.AddDependencies(manifest.recipes());
+  proto::Recipes recipes = references.CopyReferencedRecipes(manifest.recipes());
+  proto::Assets assets = references.CopyReferencedAssets(manifest.assets());
+  if (auto error = ValidateUniqueComponent(assets); error.has_value()) {
+    return base::unexpected(error.value());
+  }
+
+  return Manifest(std::move(device_category_config), std::move(recipes),
+                  std::move(assets));
+}
+
+Manifest::Manifest(proto::DeviceCategoryConfig device_category_config,
+                   proto::Recipes recipes,
+                   proto::Assets assets)
+    : device_category_config_(std::move(device_category_config)),
+      recipes_(std::move(recipes)),
+      assets_(std::move(assets)) {}
+
+Manifest::~Manifest() = default;
+
+Manifest::Manifest(const Manifest&) = default;
+Manifest& Manifest::operator=(const Manifest&) = default;
+Manifest::Manifest(Manifest&&) = default;
+Manifest& Manifest::operator=(Manifest&&) = default;
+
+std::optional<absl::flat_hash_set<Manifest::AssetId>>
+Manifest::GetRequiredAssets(const UseCaseName& use_case) const {
+  References references;
+  if (!references.AddUseCase(device_category_config_, use_case)) {
+    return std::nullopt;
+  }
+  references.AddDependencies(recipes_);
+  return references.assets;
+}
+
+absl::flat_hash_set<Manifest::AssetId> Manifest::GetRequiredAssets(
+    const std::vector<UseCaseName>& use_cases) const {
+  References references;
+  for (const auto& use_case : use_cases) {
+    references.AddUseCase(device_category_config_, use_case);
+  }
+  references.AddDependencies(recipes_);
+  return references.assets;
+}
+
+}  // namespace optimization_guide
diff --git a/components/optimization_guide/core/model_execution/manifest_broker/manifest.h b/components/optimization_guide/core/model_execution/manifest_broker/manifest.h
new file mode 100644
index 0000000..23090118
--- /dev/null
+++ b/components/optimization_guide/core/model_execution/manifest_broker/manifest.h
@@ -0,0 +1,85 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_OPTIMIZATION_GUIDE_CORE_MODEL_EXECUTION_MANIFEST_BROKER_MANIFEST_H_
+#define COMPONENTS_OPTIMIZATION_GUIDE_CORE_MODEL_EXECUTION_MANIFEST_BROKER_MANIFEST_H_
+
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "base/types/expected.h"
+#include "base/types/optional_ref.h"
+#include "components/optimization_guide/proto/manifest.pb.h"
+#include "components/optimization_guide/public/mojom/model_broker.mojom-shared.h"
+#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
+#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
+
+namespace optimization_guide {
+
+enum class DeviceCategory {
+  kGpuHighTier = 1,
+  kGpuLowTier = 2,
+  kCpu = 3,
+};
+
+// Stringifies a device category as a key into the manifest's category_configs.
+extern std::ostream& operator<<(std::ostream& stream,
+                                DeviceCategory device_category);
+
+inline constexpr std::string kManifestAssetName = "manifest";
+
+// Manifest is a C++ representation of the manifest proto. It provides APIs for
+// getting the information needed by the model broker implementation.
+class Manifest final {
+ public:
+  using AssetId = std::string;
+  using UseCaseName = std::string;
+
+  enum class ParseError {
+    kDuplicateIdentifier,
+    kMissingIdentifier,
+    kConflictingComponent,
+  };
+
+  static base::expected<Manifest, ParseError> Create(
+      proto::Manifest manifest,
+      DeviceCategory device_category);
+
+  ~Manifest();
+
+  Manifest(const Manifest&);
+  Manifest& operator=(const Manifest&);
+  Manifest(Manifest&&);
+  Manifest& operator=(Manifest&&);
+
+  // Returns the identifiers of the Assets required for a single use case.
+  // Returns nullopt if the use case is not defined in the manifest.
+  std::optional<absl::flat_hash_set<AssetId>> GetRequiredAssets(
+      const UseCaseName& use_case) const;
+  // Returns the identifiers of all of the Assets for the given use_cases.
+  // If a use case is not defined in the manifest, it is ignored.
+  absl::flat_hash_set<AssetId> GetRequiredAssets(
+      const std::vector<UseCaseName>& use_cases) const;
+
+  const proto::DeviceCategoryConfig& GetDeviceCategoryConfig() const {
+    return device_category_config_;
+  }
+  const proto::Recipes& GetRecipes() const { return recipes_; }
+  const proto::Assets& GetAssets() const { return assets_; }
+
+ private:
+  Manifest(proto::DeviceCategoryConfig device_category_config,
+           proto::Recipes recipes,
+           proto::Assets assets);
+
+  // Manifest content narrowed down to what is relevant for the device category.
+  proto::DeviceCategoryConfig device_category_config_;
+  proto::Recipes recipes_;
+  proto::Assets assets_;
+};
+
+}  // namespace optimization_guide
+
+#endif  // COMPONENTS_OPTIMIZATION_GUIDE_CORE_MODEL_EXECUTION_MANIFEST_BROKER_MANIFEST_H_
diff --git a/components/optimization_guide/core/model_execution/manifest_broker/manifest_unittest.cc b/components/optimization_guide/core/model_execution/manifest_broker/manifest_unittest.cc
new file mode 100644
index 0000000..188ae6f
--- /dev/null
+++ b/components/optimization_guide/core/model_execution/manifest_broker/manifest_unittest.cc
@@ -0,0 +1,158 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/optimization_guide/core/model_execution/manifest_broker/manifest.h"
+
+#include "components/optimization_guide/core/model_execution/manifest_broker/test/manifest_builder.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace optimization_guide {
+
+namespace {
+
+using ManifestTest = testing::Test;
+
+// Recipe args do not have interesting behavior, so we use the same values for
+// all tests.
+BaseModelRecipeArgs GenericRecipeArgs() {
+  return BaseModelRecipeArgs(
+      proto::BaseModelRecipe::BACKEND_TYPE_CPU,
+      proto::BaseModelRecipe::PERFORMANCE_HINT_FASTEST_INFERENCE, {1}, 1024);
+}
+
+// There is no special behavior for different device categories, so we do
+// most testing with CPU arbitrarily.
+auto CreateCpuManifest(ManifestBuilder builder) {
+  return Manifest::Create(builder.Build(), DeviceCategory::kCpu);
+}
+
+TEST_F(ManifestTest, ManifestDeviceCategory) {
+  EXPECT_EQ(base::ToString(DeviceCategory::kGpuHighTier), "gpu_high_tier");
+  EXPECT_EQ(base::ToString(DeviceCategory::kGpuLowTier), "gpu_low_tier");
+  EXPECT_EQ(base::ToString(DeviceCategory::kCpu), "cpu");
+}
+
+// A manifest that defines some valid elements of each type, so that tests
+// can reference them.
+ManifestBuilder ValidManifest() {
+  return ManifestBuilder()
+      .Add("valid_component", OnDemandComponent("valid_key", "1.0"))
+      .Add("valid_safety_model",
+           SafetyModelRecipe(
+               FileReference("valid_component", "valid_safety_model.bin")))
+      .Add("valid_base",
+           BaseModelRecipe(FileReference("valid_component", "valid_base.bin"),
+                           GenericRecipeArgs()))
+      .Add(
+          "valid_adaptation",
+          AdaptationRecipe("valid_base", FileReference("valid_component",
+                                                       "valid_adaptation.bin")))
+      .Add({DeviceCategory::kCpu, "feature"},
+           SolutionRecipe("valid_base", "valid_safety_model",
+                          FileReference(kManifestAssetName, "cpu_config.pb")));
+}
+
+TEST_F(ManifestTest, ValidManifest) {
+  auto result = CreateCpuManifest(ValidManifest());
+  ASSERT_TRUE(result.has_value());
+}
+
+TEST_F(ManifestTest, InvalidWithDuplicateAssetId) {
+  EXPECT_EQ(CreateCpuManifest(ValidManifest().Add(
+                "manifest", OnDemandComponent("bar_key", "1.0"))),
+            base::unexpected(Manifest::ParseError::kDuplicateIdentifier));
+}
+
+TEST_F(ManifestTest, InvalidWithDuplicateModelRecipeId) {
+  EXPECT_EQ(CreateCpuManifest(
+                ValidManifest()
+                    .Add("foo", BaseModelRecipe(FileReference("valid_component",
+                                                              "valid_base.bin"),
+                                                GenericRecipeArgs()))
+                    .Add("foo", AdaptationRecipe(
+                                    "valid_base",
+                                    FileReference("valid_component",
+                                                  "valid_adaptation.bin")))),
+            base::unexpected(Manifest::ParseError::kDuplicateIdentifier));
+}
+
+TEST_F(ManifestTest, InvalidWithMissingSolutionReference) {
+  EXPECT_EQ(CreateCpuManifest(ValidManifest().Add(
+                {DeviceCategory::kCpu, "use_case"}, "missing_solution")),
+            base::unexpected(Manifest::ParseError::kMissingIdentifier));
+  EXPECT_EQ(CreateCpuManifest(ValidManifest().Add(
+                "foo", BaseModelRecipe(
+                           FileReference("missing_component", "valid_base.bin"),
+                           GenericRecipeArgs()))),
+            base::unexpected(Manifest::ParseError::kMissingIdentifier));
+  EXPECT_EQ(
+      CreateCpuManifest(ValidManifest().Add(
+          "foo", AdaptationRecipe("valid_base",
+                                  FileReference("missing_component",
+                                                "valid_adaptation.bin")))),
+      base::unexpected(Manifest::ParseError::kMissingIdentifier));
+  EXPECT_EQ(
+      CreateCpuManifest(ValidManifest().Add(
+          "foo", AdaptationRecipe("missing_base",
+                                  FileReference("valid_component",
+                                                "valid_adaptation.bin")))),
+      base::unexpected(Manifest::ParseError::kMissingIdentifier));
+  EXPECT_EQ(CreateCpuManifest(ValidManifest().Add(
+                "foo", SafetyModelRecipe(FileReference(
+                           "missing_component", "valid_safety_model.bin")))),
+            base::unexpected(Manifest::ParseError::kMissingIdentifier));
+  EXPECT_EQ(CreateCpuManifest(ValidManifest().Add(
+                "foo", SolutionRecipe(
+                           "missing_base", kNoSafetyModel,
+                           FileReference(kManifestAssetName, "config.pb")))),
+            base::unexpected(Manifest::ParseError::kMissingIdentifier));
+  EXPECT_EQ(CreateCpuManifest(ValidManifest().Add(
+                "foo", SolutionRecipe(
+                           "valid_base", "missing_safety",
+                           FileReference(kManifestAssetName, "config.pb")))),
+            base::unexpected(Manifest::ParseError::kMissingIdentifier));
+  EXPECT_EQ(CreateCpuManifest(ValidManifest().Add(
+                "foo", SolutionRecipe(
+                           "valid_base", kNoSafetyModel,
+                           FileReference("missing_component", "config.pb")))),
+            base::unexpected(Manifest::ParseError::kMissingIdentifier));
+}
+
+TEST_F(ManifestTest, InvalidWithConflictingOnDemandComponents) {
+  auto result = CreateCpuManifest(
+      ValidManifest()
+          // Add two components with the same public key.
+          .Add("foo", OnDemandComponent("foo_key", "1.0"))
+          .Add("bar", OnDemandComponent("foo_key", "1.0"))
+          // Make both used by adding dependencies.
+          .Add({DeviceCategory::kCpu, "foo_feature"},
+               SolutionRecipe("valid_base", kNoSafetyModel,
+                              FileReference("foo", "config.pb")))
+          .Add({DeviceCategory::kCpu, "bar_feature"},
+               SolutionRecipe("valid_base", kNoSafetyModel,
+                              FileReference("bar", "config.pb"))));
+  EXPECT_EQ(result,
+            base::unexpected(Manifest::ParseError::kConflictingComponent));
+}
+
+TEST_F(ManifestTest, ValidWithConflictingComponentForDifferentDevices) {
+  auto result = CreateCpuManifest(
+      ValidManifest()
+          // Add two components with the same public key.
+          .Add("foo", OnDemandComponent("foo_key", "1.0"))
+          .Add("bar", OnDemandComponent("foo_key", "1.0"))
+          // Split the use cases between devices to avoid conflict.
+          .Add({DeviceCategory::kCpu, "foo_feature"},
+               SolutionRecipe("valid_base", kNoSafetyModel,
+                              FileReference("foo", "config.pb")))
+          .Add({DeviceCategory::kGpuHighTier, "bar_feature"},
+               SolutionRecipe("valid_base", kNoSafetyModel,
+                              FileReference("bar", "config.pb"))));
+  ASSERT_TRUE(result.has_value());
+}
+
+}  // namespace
+
+}  // namespace optimization_guide
diff --git a/components/optimization_guide/core/model_execution/manifest_broker/test/example_manifest.cc b/components/optimization_guide/core/model_execution/manifest_broker/test/example_manifest.cc
new file mode 100644
index 0000000..06906a3
--- /dev/null
+++ b/components/optimization_guide/core/model_execution/manifest_broker/test/example_manifest.cc
@@ -0,0 +1,107 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/optimization_guide/core/model_execution/manifest_broker/test/example_manifest.h"
+
+#include "components/optimization_guide/core/model_execution/manifest_broker/test/manifest_builder.h"
+#include "components/optimization_guide/proto/manifest.pb.h"
+
+namespace optimization_guide {
+
+proto::Manifest BuildExampleManifest() {
+  return ManifestBuilder()
+      // Safety model.
+      .Add("safety_model_component",
+           OnDemandComponent("abc12345...", "2026.1.21.1028"))
+      .Add("safety_model_recipe", SafetyModelRecipe(FileReference(
+                                      "safety_model_component", "weights.bin")))
+      // Tiny model for writer.
+      .Add("writer_tiny_model_component",
+           OnDemandComponent("12345...", "2026.1.21.1028"))
+      .Add("writer_tiny_model_recipe",
+           BaseModelRecipe(
+               FileReference("writer_tiny_model_component", "weights.bin"),
+               BaseModelRecipeArgs{
+                   proto::BaseModelRecipe::BACKEND_TYPE_CPU,
+                   proto::BaseModelRecipe::PERFORMANCE_HINT_FASTEST_INFERENCE,
+                   {},
+                   2048}))
+      .Add("writer_tiny_model_solution",
+           SolutionRecipe("writer_tiny_model_recipe", kNoSafetyModel,
+                          ManifestFileReference("writer_config.pb")),
+           {
+               DeviceUseCase{DeviceCategory::kCpu, kWriterUseCase},
+               DeviceUseCase{DeviceCategory::kGpuLowTier, kWriterUseCase},
+               DeviceUseCase{DeviceCategory::kGpuHighTier, kWriterUseCase},
+           })
+      // CPU recipes
+      .Add("cpu_base_model_component",
+           OnDemandComponent("5ab679....", "2025.8.21.1028"))
+      .Add("cpu_base_model_recipe",
+           BaseModelRecipe(
+               FileReference("cpu_base_model_component", "weights.bin"),
+               BaseModelRecipeArgs{
+                   proto::BaseModelRecipe::BACKEND_TYPE_CPU,
+                   proto::BaseModelRecipe::PERFORMANCE_HINT_FASTEST_INFERENCE,
+                   {1},
+                   1024}))
+      .Add("proofreader_weights_component",
+           OnDemandComponent("12345...", "2026.1.21.1028"))
+      .Add("cpu_proofreader_lora_recipe",
+           AdaptationRecipe(
+               "cpu_base_model_recipe",
+               FileReference("proofreader_weights_component", "weights.bin")))
+      .Add(DeviceUseCase{DeviceCategory::kCpu, kLanguageModelUseCase},
+           SolutionRecipe("cpu_base_model_recipe", "safety_model_recipe",
+                          ManifestFileReference("language_model_config.pb")))
+      .Add(DeviceUseCase{DeviceCategory::kCpu, kProofreaderUseCase},
+           SolutionRecipe("cpu_proofreader_lora_recipe", kNoSafetyModel,
+                          ManifestFileReference("proofreader_config.pb")))
+      .Add(DeviceUseCase{DeviceCategory::kCpu, kSummarizerUseCase},
+           SolutionRecipe("cpu_base_model_recipe", kNoSafetyModel,
+                          ManifestFileReference("summarizer_config.pb")))
+      // GPU recipes
+      .Add("gpu_base_model_component",
+           OnDemandComponent("5ab679....", "2025.8.8.1141"))
+      // Fast GPU recipes.
+      .Add("fast_gpu_base_model_recipe",
+           BaseModelRecipe(
+               FileReference("gpu_base_model_component", "weights.bin"),
+               BaseModelRecipeArgs{
+                   proto::BaseModelRecipe::BACKEND_TYPE_GPU,
+                   proto::BaseModelRecipe::PERFORMANCE_HINT_FASTEST_INFERENCE,
+                   {},
+                   1024}))
+      .Add(DeviceUseCase{DeviceCategory::kGpuLowTier, kLanguageModelUseCase},
+           SolutionRecipe("fast_gpu_base_model_recipe", "safety_model_recipe",
+                          ManifestFileReference("language_model_config.pb")))
+      .Add(DeviceUseCase{DeviceCategory::kGpuLowTier, kProofreaderUseCase},
+           SolutionRecipe("fast_gpu_base_model_recipe", kNoSafetyModel,
+                          ManifestFileReference("proofreader_config.pb")))
+      .Add(DeviceUseCase{DeviceCategory::kGpuLowTier, kSummarizerUseCase},
+           SolutionRecipe("fast_gpu_base_model_recipe", kNoSafetyModel,
+                          ManifestFileReference("summarizer_config.pb")))
+      // Quality GPU recipes.
+      .Add("quality_gpu_base_model_recipe",
+           BaseModelRecipe(
+               FileReference("gpu_base_model_component", "weights.bin"),
+               BaseModelRecipeArgs{
+                   proto::BaseModelRecipe::BACKEND_TYPE_GPU,
+                   proto::BaseModelRecipe::PERFORMANCE_HINT_HIGHEST_QUALITY,
+                   {},
+                   1024}))
+      .Add(
+          DeviceUseCase{DeviceCategory::kGpuHighTier, kLanguageModelUseCase},
+          SolutionRecipe("quality_gpu_base_model_recipe", "safety_model_recipe",
+                         ManifestFileReference("language_model_config.pb")))
+      .Add(DeviceUseCase{DeviceCategory::kGpuHighTier, kProofreaderUseCase},
+           SolutionRecipe("quality_gpu_base_model_recipe", kNoSafetyModel,
+                          ManifestFileReference("proofreader_config.pb")))
+      .Add(DeviceUseCase{DeviceCategory::kGpuHighTier, kSummarizerUseCase},
+           SolutionRecipe("quality_gpu_base_model_recipe", kNoSafetyModel,
+                          ManifestFileReference("summarizer_config.pb")))
+      .Build();
+}
+
+}  // namespace optimization_guide
diff --git a/components/optimization_guide/core/model_execution/manifest_broker/test/example_manifest.h b/components/optimization_guide/core/model_execution/manifest_broker/test/example_manifest.h
new file mode 100644
index 0000000..a427662
--- /dev/null
+++ b/components/optimization_guide/core/model_execution/manifest_broker/test/example_manifest.h
@@ -0,0 +1,16 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_OPTIMIZATION_GUIDE_CORE_MODEL_EXECUTION_MANIFEST_BROKER_TEST_EXAMPLE_MANIFEST_H_
+#define COMPONENTS_OPTIMIZATION_GUIDE_CORE_MODEL_EXECUTION_MANIFEST_BROKER_TEST_EXAMPLE_MANIFEST_H_
+
+#include "components/optimization_guide/proto/manifest.pb.h"
+
+namespace optimization_guide {
+
+proto::Manifest BuildExampleManifest();
+
+}  // namespace optimization_guide
+
+#endif  // COMPONENTS_OPTIMIZATION_GUIDE_CORE_MODEL_EXECUTION_MANIFEST_BROKER_TEST_EXAMPLE_MANIFEST_H_
diff --git a/components/optimization_guide/core/model_execution/manifest_broker/test/manifest_builder.cc b/components/optimization_guide/core/model_execution/manifest_broker/test/manifest_builder.cc
new file mode 100644
index 0000000..0bb45c2
--- /dev/null
+++ b/components/optimization_guide/core/model_execution/manifest_broker/test/manifest_builder.cc
@@ -0,0 +1,154 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/optimization_guide/core/model_execution/manifest_broker/test/manifest_builder.h"
+
+#include "components/optimization_guide/proto/manifest.pb.h"
+
+namespace optimization_guide {
+
+proto::OnDemandComponent OnDemandComponent(const std::string& public_key,
+                                           const std::string& target_version) {
+  proto::OnDemandComponent component;
+  component.set_public_key(public_key);
+  component.set_target_version(target_version);
+  return component;
+}
+
+proto::FileReference FileReference(const std::string& asset_id,
+                                   const std::string& relative_path) {
+  proto::FileReference file_ref;
+  file_ref.set_asset_id(asset_id);
+  file_ref.set_relative_path(relative_path);
+  return file_ref;
+}
+
+BaseModelRecipeArgs::BaseModelRecipeArgs(
+    proto::BaseModelRecipe::BackendType backend_type,
+    proto::BaseModelRecipe::PerformanceHint performance_hint,
+    std::vector<int32_t> supported_ranks,
+    int32_t max_tokens)
+    : backend_type(backend_type),
+      performance_hint(performance_hint),
+      supported_ranks(std::move(supported_ranks)),
+      max_tokens(max_tokens) {}
+
+BaseModelRecipeArgs::~BaseModelRecipeArgs() = default;
+
+BaseModelRecipeArgs::BaseModelRecipeArgs(const BaseModelRecipeArgs&) = default;
+
+BaseModelRecipeArgs& BaseModelRecipeArgs::operator=(
+    const BaseModelRecipeArgs&) = default;
+
+proto::BaseModelRecipe BaseModelRecipe(proto::FileReference weights_file,
+                                       BaseModelRecipeArgs args) {
+  proto::BaseModelRecipe recipe;
+  *recipe.mutable_weights_file() = std::move(weights_file);
+  recipe.set_backend_type(args.backend_type);
+  recipe.set_performance_hint(args.performance_hint);
+  for (int32_t rank : args.supported_ranks) {
+    recipe.add_supported_adaptation_ranks(rank);
+  }
+  recipe.set_max_tokens(args.max_tokens);
+  return recipe;
+}
+
+proto::AdaptationRecipe AdaptationRecipe(const std::string& base_model_id,
+                                         proto::FileReference weights_file) {
+  proto::AdaptationRecipe recipe;
+  recipe.set_base_model_recipe_id(base_model_id);
+  *recipe.mutable_weights_file() = std::move(weights_file);
+  return recipe;
+}
+
+proto::SafetyModelRecipe SafetyModelRecipe(proto::FileReference weights_file) {
+  proto::SafetyModelRecipe recipe;
+  *recipe.mutable_weights_file() = std::move(weights_file);
+  return recipe;
+}
+
+proto::SolutionRecipe SolutionRecipe(const std::string& model_recipe_id,
+                                     const std::string& safety_model_recipe_id,
+                                     proto::FileReference config_file) {
+  proto::SolutionRecipe recipe;
+  recipe.set_model_recipe_id(model_recipe_id);
+  if (!safety_model_recipe_id.empty()) {
+    recipe.set_safety_model_recipe_id(safety_model_recipe_id);
+  }
+  *recipe.mutable_config_file() = std::move(config_file);
+  return recipe;
+}
+
+ManifestBuilder::ManifestBuilder() = default;
+ManifestBuilder::~ManifestBuilder() = default;
+
+ManifestBuilder& ManifestBuilder::Add(const std::string& name,
+                                      proto::OnDemandComponent component) {
+  (*manifest_.mutable_assets()->mutable_on_demand_components())[name] =
+      std::move(component);
+  return *this;
+}
+
+ManifestBuilder& ManifestBuilder::Add(const std::string& name,
+                                      proto::BaseModelRecipe recipe) {
+  (*manifest_.mutable_recipes()->mutable_base_models())[name] =
+      std::move(recipe);
+  return *this;
+}
+
+ManifestBuilder& ManifestBuilder::Add(const std::string& name,
+                                      proto::AdaptationRecipe recipe) {
+  (*manifest_.mutable_recipes()->mutable_adaptations())[name] =
+      std::move(recipe);
+  return *this;
+}
+
+ManifestBuilder& ManifestBuilder::Add(const std::string& name,
+                                      proto::SafetyModelRecipe recipe) {
+  (*manifest_.mutable_recipes()->mutable_safety_models())[name] =
+      std::move(recipe);
+  return *this;
+}
+
+ManifestBuilder& ManifestBuilder::Add(const std::string& name,
+                                      proto::SolutionRecipe recipe) {
+  (*manifest_.mutable_recipes()->mutable_solutions())[name] = std::move(recipe);
+  return *this;
+}
+
+ManifestBuilder& ManifestBuilder::Add(const DeviceUseCase& device_use_case,
+                                      const std::string& solution_recipe_id) {
+  proto::DeviceCategoryConfig& config =
+      (*manifest_.mutable_category_configs())[base::ToString(
+          device_use_case.device)];
+  (*config.mutable_use_cases())[device_use_case.use_case]
+      .set_solution_recipe_id(solution_recipe_id);
+  return *this;
+}
+
+ManifestBuilder& ManifestBuilder::Add(
+    const std::string& name,
+    proto::SolutionRecipe recipe,
+    const std::vector<DeviceUseCase>& device_use_cases) {
+  Add(name, std::move(recipe));
+  for (const auto& device_use_case : device_use_cases) {
+    Add(device_use_case, name);
+  }
+  return *this;
+}
+
+ManifestBuilder& ManifestBuilder::Add(const DeviceUseCase& use_case,
+                                      proto::SolutionRecipe recipe) {
+  std::string recipe_id =
+      base::ToString(use_case.device) + "_" + use_case.use_case + "_solution";
+  Add(recipe_id, std::move(recipe));
+  Add(use_case, recipe_id);
+  return *this;
+}
+
+proto::Manifest ManifestBuilder::Build() {
+  return manifest_;
+}
+
+}  // namespace optimization_guide
diff --git a/components/optimization_guide/core/model_execution/manifest_broker/test/manifest_builder.h b/components/optimization_guide/core/model_execution/manifest_broker/test/manifest_builder.h
new file mode 100644
index 0000000..546b164
--- /dev/null
+++ b/components/optimization_guide/core/model_execution/manifest_broker/test/manifest_builder.h
@@ -0,0 +1,107 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_OPTIMIZATION_GUIDE_CORE_MODEL_EXECUTION_MANIFEST_BROKER_TEST_MANIFEST_BUILDER_H_
+#define COMPONENTS_OPTIMIZATION_GUIDE_CORE_MODEL_EXECUTION_MANIFEST_BROKER_TEST_MANIFEST_BUILDER_H_
+
+#include <initializer_list>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "components/optimization_guide/core/model_execution/manifest_broker/manifest.h"
+#include "components/optimization_guide/proto/manifest.pb.h"
+
+namespace optimization_guide {
+
+inline constexpr std::string kNoSafetyModel = "";
+
+// Well-known use case names.
+inline constexpr char kLanguageModelUseCase[] = "language_model";
+inline constexpr char kProofreaderUseCase[] = "proofreader";
+inline constexpr char kSummarizerUseCase[] = "summarizer";
+inline constexpr char kWriterUseCase[] = "writer";
+
+proto::OnDemandComponent OnDemandComponent(const std::string& public_key,
+                                           const std::string& target_version);
+
+proto::FileReference FileReference(const std::string& asset_id,
+                                   const std::string& relative_path);
+
+inline proto::FileReference ManifestFileReference(
+    const std::string& relative_path) {
+  return FileReference(kManifestAssetName, relative_path);
+}
+
+struct BaseModelRecipeArgs {
+  BaseModelRecipeArgs(proto::BaseModelRecipe::BackendType backend_type,
+                      proto::BaseModelRecipe::PerformanceHint performance_hint,
+                      std::vector<int32_t> supported_ranks,
+                      int32_t max_tokens);
+  ~BaseModelRecipeArgs();
+  BaseModelRecipeArgs(const BaseModelRecipeArgs&);
+  BaseModelRecipeArgs& operator=(const BaseModelRecipeArgs&);
+
+  proto::BaseModelRecipe::BackendType backend_type;
+  proto::BaseModelRecipe::PerformanceHint performance_hint;
+  std::vector<int32_t> supported_ranks;
+  int32_t max_tokens;
+};
+
+proto::BaseModelRecipe BaseModelRecipe(proto::FileReference weights_file,
+                                       BaseModelRecipeArgs args);
+
+proto::AdaptationRecipe AdaptationRecipe(const std::string& base_model_id,
+                                         proto::FileReference weights_file);
+
+proto::SafetyModelRecipe SafetyModelRecipe(proto::FileReference weights_file);
+
+proto::SolutionRecipe SolutionRecipe(const std::string& model_recipe_id,
+                                     const std::string& safety_model_recipe_id,
+                                     proto::FileReference config_file);
+
+// Declares a 'use_case' on 'device'.
+struct DeviceUseCase {
+  DeviceCategory device;
+  std::string use_case;
+};
+
+class ManifestBuilder {
+ public:
+  ManifestBuilder();
+  ~ManifestBuilder();
+
+  ManifestBuilder& Add(const std::string& name,
+                       proto::OnDemandComponent component);
+
+  ManifestBuilder& Add(const std::string& name, proto::BaseModelRecipe recipe);
+
+  ManifestBuilder& Add(const std::string& name, proto::AdaptationRecipe recipe);
+
+  ManifestBuilder& Add(const std::string& name,
+                       proto::SafetyModelRecipe recipe);
+
+  ManifestBuilder& Add(const std::string& name, proto::SolutionRecipe recipe);
+
+  ManifestBuilder& Add(const DeviceUseCase& device_use_case,
+                       const std::string& solution_recipe_id);
+
+  // Declares a recipe and the use cases it serves.
+  ManifestBuilder& Add(const std::string& name,
+                       proto::SolutionRecipe recipe,
+                       const std::vector<DeviceUseCase>& device_use_cases);
+
+  // Declares a device-specific use case.
+  ManifestBuilder& Add(const DeviceUseCase& use_case,
+                       proto::SolutionRecipe recipe);
+
+  proto::Manifest Build();
+
+ private:
+  proto::Manifest manifest_;
+};
+
+}  // namespace optimization_guide
+
+#endif  // COMPONENTS_OPTIMIZATION_GUIDE_CORE_MODEL_EXECUTION_MANIFEST_BROKER_TEST_MANIFEST_BUILDER_H_
diff --git a/components/optimization_guide/proto/BUILD.gn b/components/optimization_guide/proto/BUILD.gn
index 8e49624..6c53fac 100644
--- a/components/optimization_guide/proto/BUILD.gn
+++ b/components/optimization_guide/proto/BUILD.gn
@@ -59,6 +59,7 @@
     "history_query_intent_model_metadata.proto",
     "icon_view_metadata.proto",
     "loading_predictor_metadata.proto",
+    "manifest.proto",
     "model_execution.proto",
     "model_quality_metadata.proto",
     "model_quality_service.proto",
diff --git a/components/optimization_guide/proto/manifest.proto b/components/optimization_guide/proto/manifest.proto
new file mode 100644
index 0000000..8bdf850
--- /dev/null
+++ b/components/optimization_guide/proto/manifest.proto
@@ -0,0 +1,141 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Messages for describing the on-device model manifest.
+edition = "2023";
+
+package optimization_guide.proto;
+
+import "components/optimization_guide/proto/common_types.proto";
+
+option optimize_for = LITE_RUNTIME;
+option features.field_presence = IMPLICIT;
+
+// A manifest contains definitions for several types of objects, keyed by
+// unique identifier strings.
+// These names should not be referenced outside of the manifest, and a manifest
+// should only define a single object with a given name, even across different
+// types of objects.
+
+// An asset that can be requested from components updater.
+message OnDemandComponent {
+  // The public key of the component.
+  string public_key = 1;
+  // The target version of the component.
+  string target_version = 2;
+}
+
+// Defines the set of additional assets the client can request as needed.
+// Assets are usually components, but this could be extended to include other
+// things if needed (such as files on disk in local development).
+// A special "manifest" asset is implicitly defined to refer to the manifest
+// component itself.
+message Assets {
+  // Defines a set of component-updater components, keyed by identifier strings.
+  map<string, OnDemandComponent> on_demand_components = 1;
+}
+
+// A reference to a file within an asset.
+message FileReference {
+  // The identifier of the asset (defined by the Assets message) that contains
+  // the file.
+  string asset_id = 1;
+  // The relative path of the file within the asset, e.g. "path/to/file".
+  string relative_path = 2;
+}
+
+// A recipe for a base model (may or may not be general purpose).
+message BaseModelRecipe {
+  FileReference weights_file = 1;
+
+  enum BackendType {
+    BACKEND_TYPE_UNSPECIFIED = 0;
+    BACKEND_TYPE_GPU = 1;
+    BACKEND_TYPE_CPU = 2;
+  }
+  BackendType backend_type = 2;
+
+  enum PerformanceHint {
+    PERFORMANCE_HINT_UNSPECIFIED = 0;
+    PERFORMANCE_HINT_FASTEST_INFERENCE = 1;
+    PERFORMANCE_HINT_HIGHEST_QUALITY = 2;
+  }
+  PerformanceHint performance_hint = 3;
+
+  // Specifies the adaptation ranks that are supported by this model.
+  repeated int32 supported_adaptation_ranks = 4;
+  int32 max_tokens = 5;
+}
+
+// A recipe for a LoRA model.
+message AdaptationRecipe {
+  // The identifier of a BaseModelRecipe for this adaptation.
+  string base_model_recipe_id = 1;
+  FileReference weights_file = 2;
+}
+
+// A recipe for a safety model.
+message SafetyModelRecipe {
+  FileReference weights_file = 1;
+}
+
+// A recipe for a ModelSolution provided by the ModelBroker.
+message SolutionRecipe {
+  // The identifier of a BaseModelRecipe or AdaptationRecipe that provides the
+  // model for this use case.
+  string model_recipe_id = 1;
+  // The identifier of a SafetyModelRecipe that provides the safety model for
+  // this use case.  May be empty if no safety model is required.
+  string safety_model_recipe_id = 2 [features.field_presence = EXPLICIT];
+  // Which file contains the configuration proto for this use case.
+  // e.g. an "OnDeviceModelExecutionFeatureConfig"
+  FileReference config_file = 3;
+}
+
+// A collection of recipes that can be loaded from Assets.
+// The recipes are keyed by manifest-internal IDs.
+// The keys for different types of recipes should not overlap.
+message Recipes {
+  // Defines a set of base model recipes, keyed by identifier strings.
+  map<string, BaseModelRecipe> base_models = 1;
+  // Defines a set of adaptation recipes, keyed by identifier strings.
+  map<string, AdaptationRecipe> adaptations = 2;
+  // Defines a set of safety model recipes, keyed by identifier strings.
+  map<string, SafetyModelRecipe> safety_models = 3;
+  // Defines a set of solution recipes, keyed by identifier strings.
+  map<string, SolutionRecipe> solutions = 4;
+}
+
+message UseCaseConfig {
+  // The identifier of the solution recipe that implements this use case for
+  // the device category.
+  string solution_recipe_id = 1;
+
+  // This could be extended to include multiple solution recipe IDs per use
+  // case, if the Broker needs to select solutions for e.g. memory pressure.
+}
+
+// Device category specific configuration.
+message DeviceCategoryConfig {
+  // A mapping from a BrokerClient-visible use-case name to the configuration
+  // for that use case.
+  map<string, UseCaseConfig> use_cases = 1;
+
+  // A collection of configs that can be served by the Broker.
+  // These are opaque to the Broker, and are intended to allow the feature to
+  // configure how use cases are selected based on feature specific parameters.
+  map<string, Any> feature_configs = 2;
+}
+
+// The top-level message in the manifest file of manifest component.
+message Manifest {
+  // A list of on-demand downloadable assets (components updater components)
+  Assets assets = 1;
+  // The recipes that can be constructed from the assets.
+  Recipes recipes = 2;
+  // Device category specific configuration, which configures the externally
+  // visible use-cases. The device category names are hardcoded in chromium.
+  // See DeviceCategoryToString.
+  map<string, DeviceCategoryConfig> category_configs = 3;
+}
diff --git a/components/page_info/android/java/src/org/chromium/components/page_info/PageInfoPermissionsController.java b/components/page_info/android/java/src/org/chromium/components/page_info/PageInfoPermissionsController.java
index bb685ad..50e641f 100644
--- a/components/page_info/android/java/src/org/chromium/components/page_info/PageInfoPermissionsController.java
+++ b/components/page_info/android/java/src/org/chromium/components/page_info/PageInfoPermissionsController.java
@@ -78,7 +78,7 @@
     private boolean mDataIsStale;
     private @Nullable SingleWebsiteSettings mSubPage;
     @ContentSettingsType.EnumType private final int mHighlightedPermission;
-    @ColorRes private final int mHighlightColor;
+    private final @ColorRes int mHighlightColor;
 
     public PageInfoPermissionsController(
             PageInfoMainController mainController,
diff --git a/components/policy/core/common/cloud/user_cloud_policy_store.cc b/components/policy/core/common/cloud/user_cloud_policy_store.cc
index d493145c..19c12769f 100644
--- a/components/policy/core/common/cloud/user_cloud_policy_store.cc
+++ b/components/policy/core/common/cloud/user_cloud_policy_store.cc
@@ -295,6 +295,13 @@
   static_assert(std::is_same<PayloadProto, em::CloudPolicySettings>() ||
                 std::is_same<PayloadProto, em::ExtensionInstallPolicies>());
 
+  // TODO(crbug.com/483099777): Reenable signature verification once a good
+  // solution is found. This is temporarily disabled to avoid breaking existing
+  // enterprise policy fetches.
+  if (IsExtensionInstallPolicyType(policy_type())) {
+    return;
+  }
+
   VLOG_POLICY(1, POLICY_PROCESSING)
       << PolicyTypeLogPrefix(policy_type(), std::string())
       << "Has cached key: " << cached_key;
diff --git a/components/saved_tab_groups/internal/saved_tab_group_model.cc b/components/saved_tab_groups/internal/saved_tab_group_model.cc
index 7907d4b..6971bdd 100644
--- a/components/saved_tab_groups/internal/saved_tab_group_model.cc
+++ b/components/saved_tab_groups/internal/saved_tab_group_model.cc
@@ -49,27 +49,42 @@
 }
 
 // Compare function for 2 SavedTabGroup.
+//
+// If projects panel is disabled:
 // SaveTabGroup with position set is always placed before the one without
 // position set. If both have position set, the one with lower position number
 // should place before. If both positions are the same or both are not set, the
 // one with more recent update time should place before.
+//
+// If projects panel is enabled:
+// Unpositioned groups are placed before positioned groups and ordered by
+// most to least recent creation time.
 bool ShouldPlaceBefore(const SavedTabGroup& group1,
                        const SavedTabGroup& group2) {
   std::optional<size_t> position1 = group1.position();
   std::optional<size_t> position2 = group2.position();
-  if (position1.has_value() && position2.has_value()) {
-    if (position1.value() != position2.value()) {
-      return position1.value() < position2.value();
-    } else {
-      return group1.update_time() >= group2.update_time();
-    }
-  } else if (position1.has_value() && !position2.has_value()) {
-    return true;
-  } else if (!position1.has_value() && position2.has_value()) {
-    return false;
-  } else {
-    return group1.update_time() >= group2.update_time();
+
+  const bool projects_panel_enabled =
+      tab_groups::IsProjectsPanelFeatureEnabled();
+
+  // Handle only one of the positions having a value.
+  if (position1.has_value() != position2.has_value()) {
+    return projects_panel_enabled ? !position1.has_value()
+                                  : position1.has_value();
   }
+
+  // Handle both positions with different values.
+  if (position1.has_value() && position2.has_value() &&
+      position1.value() != position2.value()) {
+    return position1.value() < position2.value();
+  }
+
+  // Handle no positions.
+  if (projects_panel_enabled && !position1.has_value()) {
+    return group1.creation_time() >= group2.creation_time();
+  }
+
+  return group1.update_time() >= group2.update_time();
 }
 
 // URL and title used for pending NTP.
@@ -768,6 +783,46 @@
   }
 }
 
+void SavedTabGroupModel::ReorderGroupBefore(const base::Uuid& id,
+                                            const base::Uuid& next_id) {
+  if (id == next_id) {
+    return;
+  }
+
+  std::optional<int> index_to_move = GetIndexOf(id);
+  std::optional<int> reference_index = GetIndexOf(next_id);
+
+  if (!index_to_move.has_value() || !reference_index.has_value()) {
+    return;
+  }
+
+  int new_index = reference_index.value() > index_to_move.value()
+                      ? reference_index.value() - 1
+                      : reference_index.value();
+
+  ReorderGroupLocally(id, new_index);
+}
+
+void SavedTabGroupModel::ReorderGroupAfter(const base::Uuid& id,
+                                           const base::Uuid& prev_id) {
+  if (id == prev_id) {
+    return;
+  }
+
+  std::optional<int> index_to_move = GetIndexOf(id);
+  std::optional<int> reference_index = GetIndexOf(prev_id);
+
+  if (!index_to_move.has_value() || !reference_index.has_value()) {
+    return;
+  }
+
+  int new_index = reference_index.value() > index_to_move.value()
+                      ? reference_index.value()
+                      : reference_index.value() + 1;
+
+  ReorderGroupLocally(id, new_index);
+}
+
 std::pair<std::set<base::Uuid>, std::set<base::Uuid>>
 SavedTabGroupModel::UpdateLocalCacheGuid(
     std::optional<std::string> old_cache_guid,
@@ -1026,13 +1081,20 @@
   saved_tab_groups_.erase(saved_tab_groups_.begin() + index.value());
   saved_tab_groups_.emplace(saved_tab_groups_.begin() + new_index,
                             std::move(group));
+
+  if (tab_groups::IsProjectsPanelFeatureEnabled()) {
+    saved_tab_groups_[new_index].SetPosition(new_index);
+  }
 }
 
 void SavedTabGroupModel::UpdateGroupPositionsImpl() {
+  const bool projects_panel_enabled =
+      tab_groups::IsProjectsPanelFeatureEnabled();
+  bool positioned_group_seen = false;
   for (size_t i = 0; i < saved_tab_groups_.size(); ++i) {
-    //  Only update position for tab groups for which position is set.
-    if (saved_tab_groups_[i].position().has_value()) {
+    if (positioned_group_seen || saved_tab_groups_[i].position().has_value()) {
       saved_tab_groups_[i].SetPosition(i);
+      positioned_group_seen = projects_panel_enabled;
     }
   }
 }
diff --git a/components/saved_tab_groups/internal/saved_tab_group_model.h b/components/saved_tab_groups/internal/saved_tab_group_model.h
index 6b68d5c..d6a042d2 100644
--- a/components/saved_tab_groups/internal/saved_tab_group_model.h
+++ b/components/saved_tab_groups/internal/saved_tab_group_model.h
@@ -199,6 +199,11 @@
   void ReorderGroupLocally(const base::Uuid& id, int new_index);
   void ReorderGroupFromSync(const base::Uuid& id, int new_index);
 
+  // Reorders the group with `id` to be before or after the group with
+  // `next_id` or `prev_id`.
+  void ReorderGroupBefore(const base::Uuid& id, const base::Uuid& next_id);
+  void ReorderGroupAfter(const base::Uuid& id, const base::Uuid& prev_id);
+
   // Update the creator cache guid for all saved groups that have
   // `old_cache_guid`, to `new_cache_guid`.
   std::pair<std::set<base::Uuid>, std::set<base::Uuid>> UpdateLocalCacheGuid(
diff --git a/components/saved_tab_groups/internal/saved_tab_group_model_unittest.cc b/components/saved_tab_groups/internal/saved_tab_group_model_unittest.cc
index 8e6ea11..174d85c 100644
--- a/components/saved_tab_groups/internal/saved_tab_group_model_unittest.cc
+++ b/components/saved_tab_groups/internal/saved_tab_group_model_unittest.cc
@@ -584,6 +584,70 @@
                                  {group->saved_tabs()[0], tab1, tab2});
 }
 
+TEST_F(SavedTabGroupModelTest, ReorderGroupBefore) {
+  RemoveTestData();
+  SavedTabGroup group_A(u"Group A", tab_groups::TabGroupColorId::kRed, {},
+                        std::nullopt, id_1_);
+  SavedTabGroup group_B(u"Group B", tab_groups::TabGroupColorId::kOrange, {},
+                        std::nullopt, id_2_);
+  SavedTabGroup group_C(u"Group C", tab_groups::TabGroupColorId::kYellow, {},
+                        std::nullopt, id_3_);
+  base::Uuid id_4 = base::Uuid::GenerateRandomV4();
+  SavedTabGroup group_D(u"Group D", tab_groups::TabGroupColorId::kGreen, {},
+                        std::nullopt, id_4);
+
+  // Added locally inserts at the front if no position is set.
+  // So adding in order A, B, C, D results in [D, C, B, A]
+  saved_tab_group_model_->AddedLocally(group_A);
+  saved_tab_group_model_->AddedLocally(group_B);
+  saved_tab_group_model_->AddedLocally(group_C);
+  saved_tab_group_model_->AddedLocally(group_D);
+
+  ASSERT_THAT(GetSavedTabGroupIds(),
+              testing::ElementsAre(id_4, id_3_, id_2_, id_1_));
+
+  // Move D before B: [D, C, B, A] -> [C, D, B, A]
+  saved_tab_group_model_->ReorderGroupBefore(id_4, id_2_);
+  EXPECT_THAT(GetSavedTabGroupIds(),
+              testing::ElementsAre(id_3_, id_4, id_2_, id_1_));
+
+  // Move A before C: [C, D, B, A] -> [A, C, D, B]
+  saved_tab_group_model_->ReorderGroupBefore(id_1_, id_3_);
+  EXPECT_THAT(GetSavedTabGroupIds(),
+              testing::ElementsAre(id_1_, id_3_, id_4, id_2_));
+}
+
+TEST_F(SavedTabGroupModelTest, ReorderGroupAfter) {
+  RemoveTestData();
+  SavedTabGroup group_A(u"Group A", tab_groups::TabGroupColorId::kRed, {},
+                        std::nullopt, id_1_);
+  SavedTabGroup group_B(u"Group B", tab_groups::TabGroupColorId::kOrange, {},
+                        std::nullopt, id_2_);
+  SavedTabGroup group_C(u"Group C", tab_groups::TabGroupColorId::kYellow, {},
+                        std::nullopt, id_3_);
+  base::Uuid id_4 = base::Uuid::GenerateRandomV4();
+  SavedTabGroup group_D(u"Group D", tab_groups::TabGroupColorId::kGreen, {},
+                        std::nullopt, id_4);
+
+  saved_tab_group_model_->AddedLocally(group_A);
+  saved_tab_group_model_->AddedLocally(group_B);
+  saved_tab_group_model_->AddedLocally(group_C);
+  saved_tab_group_model_->AddedLocally(group_D);
+
+  ASSERT_THAT(GetSavedTabGroupIds(),
+              testing::ElementsAre(id_4, id_3_, id_2_, id_1_));
+
+  // Move A after C: [D, C, B, A] -> [D, C, A, B]
+  saved_tab_group_model_->ReorderGroupAfter(id_1_, id_3_);
+  EXPECT_THAT(GetSavedTabGroupIds(),
+              testing::ElementsAre(id_4, id_3_, id_1_, id_2_));
+
+  // Move D after B: [D, C, A, B] -> [C, A, B, D]
+  saved_tab_group_model_->ReorderGroupAfter(id_4, id_2_);
+  EXPECT_THAT(GetSavedTabGroupIds(),
+              testing::ElementsAre(id_3_, id_1_, id_2_, id_4));
+}
+
 TEST_F(SavedTabGroupModelTest, MoveElement) {
   ASSERT_EQ(0, saved_tab_group_model_->GetIndexOf(id_3_));
   ASSERT_EQ(1, saved_tab_group_model_->GetIndexOf(id_2_));
@@ -777,6 +841,126 @@
   EXPECT_EQ(model_group->shared_attribution().updated_by, kUpdater);
 }
 
+TEST_F(SavedTabGroupModelTest, ReorderGroupBeforeAfter_ProjectsPanelEnabled) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndEnableFeature(tab_groups::kProjectsPanel);
+  RemoveTestData();
+
+  // Create 4 unpositioned groups.
+  SavedTabGroup group1(u"Group 1", tab_groups::TabGroupColorId::kRed, {},
+                       std::nullopt);
+  SavedTabGroup group2(u"Group 2", tab_groups::TabGroupColorId::kOrange, {},
+                       std::nullopt);
+  SavedTabGroup group3(u"Group 3", tab_groups::TabGroupColorId::kYellow, {},
+                       std::nullopt);
+  SavedTabGroup group4(u"Group 4", tab_groups::TabGroupColorId::kGreen, {},
+                       std::nullopt);
+
+  base::Uuid guid1 = group1.saved_guid();
+  base::Uuid guid2 = group2.saved_guid();
+  base::Uuid guid3 = group3.saved_guid();
+  base::Uuid guid4 = group4.saved_guid();
+
+  // Add them to the model. Since they are all unpositioned, they are ordered by
+  // creation time. We'll add them with slight delays to ensure creation time
+  // order.
+  saved_tab_group_model_->AddedLocally(group1);
+  saved_tab_group_model_->AddedLocally(group2);
+  saved_tab_group_model_->AddedLocally(group3);
+  saved_tab_group_model_->AddedLocally(group4);
+
+  // Initial order (by creation time, descending): group4, group3, group2,
+  // group1.
+  EXPECT_THAT(GetSavedTabGroupIds(),
+              testing::ElementsAre(guid4, guid3, guid2, guid1));
+  EXPECT_FALSE(saved_tab_group_model_->Get(guid1)->position().has_value());
+  EXPECT_FALSE(saved_tab_group_model_->Get(guid2)->position().has_value());
+  EXPECT_FALSE(saved_tab_group_model_->Get(guid3)->position().has_value());
+  EXPECT_FALSE(saved_tab_group_model_->Get(guid4)->position().has_value());
+
+  // Reorder group 1 BEFORE group 3.
+  // guid1 is at index 3, guid3 is at index 1.
+  // ReorderGroupBefore(guid1, guid3) should move guid1 to index 1.
+  saved_tab_group_model_->ReorderGroupBefore(guid1, guid3);
+
+  // New order: guid4, guid1, guid3, guid2.
+  EXPECT_THAT(GetSavedTabGroupIds(),
+              testing::ElementsAre(guid4, guid1, guid3, guid2));
+
+  // Behavior check:
+  // 1. guid1 was moved, it should now have a position.
+  // 2. all groups AFTER guid1 (guid3, guid2) should also have positions.
+  // 3. groups BEFORE guid1 (guid4) should NOT have a position if they didn't
+  // before.
+  EXPECT_FALSE(saved_tab_group_model_->Get(guid4)->position().has_value());
+  EXPECT_EQ(1, saved_tab_group_model_->Get(guid1)->position());
+  EXPECT_EQ(2, saved_tab_group_model_->Get(guid3)->position());
+  EXPECT_EQ(3, saved_tab_group_model_->Get(guid2)->position());
+
+  // Reorder group 4 AFTER group 3.
+  // guid4 is at index 0, guid3 is at index 2.
+  // ReorderGroupAfter(guid4, guid3) should move guid4 to index 2.
+  saved_tab_group_model_->ReorderGroupAfter(guid4, guid3);
+
+  // New order: guid1, guid3, guid4, guid2.
+  EXPECT_THAT(GetSavedTabGroupIds(),
+              testing::ElementsAre(guid1, guid3, guid4, guid2));
+
+  // Now all groups should have positions because guid1 (at index 0) has a
+  // position.
+  EXPECT_EQ(0, saved_tab_group_model_->Get(guid1)->position());
+  EXPECT_EQ(1, saved_tab_group_model_->Get(guid3)->position());
+  EXPECT_EQ(2, saved_tab_group_model_->Get(guid4)->position());
+  EXPECT_EQ(3, saved_tab_group_model_->Get(guid2)->position());
+}
+
+TEST_F(SavedTabGroupModelTest,
+       OrdersUnpositionedGroupsFirstByCreationTime_ProjectsPanelEnabled) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndEnableFeature(tab_groups::kProjectsPanel);
+  RemoveTestData();
+
+  base::Time fixed_time = base::Time::Now();
+
+  // Create positioned groups.
+  SavedTabGroup positioned_0(u"Pos 0", tab_groups::TabGroupColorId::kRed, {},
+                             0);
+  SavedTabGroup positioned_1(u"Pos 1", tab_groups::TabGroupColorId::kBlue, {},
+                             1);
+
+  // Create unpositioned groups.
+  // Group A is older.
+  SavedTabGroup unpositioned_older(
+      u"Unpos Older", tab_groups::TabGroupColorId::kGreen, {}, std::nullopt,
+      base::Uuid::GenerateRandomV4(), std::nullopt, std::nullopt, std::nullopt,
+      false, fixed_time - base::Days(1));
+  // Group B is newer.
+  SavedTabGroup unpositioned_newer(
+      u"Unpos Newer", tab_groups::TabGroupColorId::kYellow, {}, std::nullopt,
+      base::Uuid::GenerateRandomV4(), std::nullopt, std::nullopt, std::nullopt,
+      false, fixed_time);
+
+  base::Uuid id_pos_0 = positioned_0.saved_guid();
+  base::Uuid id_pos_1 = positioned_1.saved_guid();
+  base::Uuid id_unpos_older = unpositioned_older.saved_guid();
+  base::Uuid id_unpos_newer = unpositioned_newer.saved_guid();
+
+  // Add them in random order.
+  saved_tab_group_model_->AddedLocally(positioned_1);
+  saved_tab_group_model_->AddedLocally(unpositioned_older);
+  saved_tab_group_model_->AddedLocally(positioned_0);
+  saved_tab_group_model_->AddedLocally(unpositioned_newer);
+
+  // Expected order:
+  // Unpositioned Newer (most recent creation time)
+  // Unpositioned Older
+  // Positioned 0
+  // Positioned 1
+  EXPECT_THAT(
+      GetSavedTabGroupIds(),
+      testing::ElementsAre(id_unpos_newer, id_unpos_older, id_pos_0, id_pos_1));
+}
+
 // Tests that merging a tab with the same tab_id changes the state of the object
 // correctly.
 TEST_F(SavedTabGroupModelTest, MergeTabsFromModel) {
diff --git a/components/saved_tab_groups/internal/tab_group_sync_service_impl.cc b/components/saved_tab_groups/internal/tab_group_sync_service_impl.cc
index 051299f..d8cbc5b9 100644
--- a/components/saved_tab_groups/internal/tab_group_sync_service_impl.cc
+++ b/components/saved_tab_groups/internal/tab_group_sync_service_impl.cc
@@ -505,6 +505,22 @@
   }
 }
 
+void TabGroupSyncServiceImpl::ReorderGroupBefore(
+    const base::Uuid& sync_id,
+    const base::Uuid& next_sync_id) {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  VLOG(2) << __func__;
+  model_->ReorderGroupBefore(sync_id, next_sync_id);
+}
+
+void TabGroupSyncServiceImpl::ReorderGroupAfter(
+    const base::Uuid& sync_id,
+    const base::Uuid& prev_sync_id) {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  VLOG(2) << __func__;
+  model_->ReorderGroupAfter(sync_id, prev_sync_id);
+}
+
 void TabGroupSyncServiceImpl::UpdateBookmarkNodeId(
     const base::Uuid& sync_id,
     std::optional<base::Uuid> bookmark_node_id) {
diff --git a/components/saved_tab_groups/internal/tab_group_sync_service_impl.h b/components/saved_tab_groups/internal/tab_group_sync_service_impl.h
index eddeb83..86538be 100644
--- a/components/saved_tab_groups/internal/tab_group_sync_service_impl.h
+++ b/components/saved_tab_groups/internal/tab_group_sync_service_impl.h
@@ -90,6 +90,11 @@
                            std::optional<bool> is_pinned,
                            std::optional<int> new_index) override;
 
+  void ReorderGroupBefore(const base::Uuid& sync_id,
+                          const base::Uuid& next_sync_id) override;
+  void ReorderGroupAfter(const base::Uuid& sync_id,
+                         const base::Uuid& prev_sync_id) override;
+
   void UpdateBookmarkNodeId(
       const base::Uuid& sync_id,
       std::optional<base::Uuid> bookmark_node_id) override;
diff --git a/components/saved_tab_groups/internal/tab_group_sync_service_impl_unittest.cc b/components/saved_tab_groups/internal/tab_group_sync_service_impl_unittest.cc
index 0bc0a912..4d732fb 100644
--- a/components/saved_tab_groups/internal/tab_group_sync_service_impl_unittest.cc
+++ b/components/saved_tab_groups/internal/tab_group_sync_service_impl_unittest.cc
@@ -1753,6 +1753,44 @@
   EXPECT_EQ(1, group->position());
 }
 
+TEST_F(TabGroupSyncServiceImplTest, ReorderGroupBefore) {
+  base::Uuid guid1 = group_1_.saved_guid();
+  base::Uuid guid2 = group_2_.saved_guid();
+  base::Uuid guid3 = group_3_.saved_guid();
+
+  // Initial order: [group_1, group_2, group_3]
+
+  EXPECT_CALL(*observer_, OnTabGroupsReordered(Eq(TriggerSource::LOCAL)))
+      .Times(1);
+  tab_group_sync_service_->ReorderGroupBefore(guid3, guid2);
+
+  auto all_groups = tab_group_sync_service_->GetAllGroups();
+  // guid3 should now be before guid2.
+  // [group_1, group_3, group_2]
+  EXPECT_EQ(all_groups[0].saved_guid(), guid1);
+  EXPECT_EQ(all_groups[1].saved_guid(), guid3);
+  EXPECT_EQ(all_groups[2].saved_guid(), guid2);
+}
+
+TEST_F(TabGroupSyncServiceImplTest, ReorderGroupAfter) {
+  base::Uuid guid1 = group_1_.saved_guid();
+  base::Uuid guid2 = group_2_.saved_guid();
+  base::Uuid guid3 = group_3_.saved_guid();
+
+  // Initial order: [group_1, group_2, group_3]
+
+  EXPECT_CALL(*observer_, OnTabGroupsReordered(Eq(TriggerSource::LOCAL)))
+      .Times(1);
+  tab_group_sync_service_->ReorderGroupAfter(guid1, guid2);
+
+  auto all_groups = tab_group_sync_service_->GetAllGroups();
+  // guid1 should now be after guid2.
+  // [group_2, group_1, group_3]
+  EXPECT_EQ(all_groups[0].saved_guid(), guid2);
+  EXPECT_EQ(all_groups[1].saved_guid(), guid1);
+  EXPECT_EQ(all_groups[2].saved_guid(), guid3);
+}
+
 TEST_F(TabGroupSyncServiceImplTest, TabIDMappingIsCleardOnGroupClose) {
   auto group = tab_group_sync_service_->GetGroup(group_1_.saved_guid());
   EXPECT_TRUE(group->local_group_id().has_value());
diff --git a/components/saved_tab_groups/public/tab_group_sync_service.h b/components/saved_tab_groups/public/tab_group_sync_service.h
index f6cd654a..3f55d36 100644
--- a/components/saved_tab_groups/public/tab_group_sync_service.h
+++ b/components/saved_tab_groups/public/tab_group_sync_service.h
@@ -182,6 +182,13 @@
                                    std::optional<bool> is_pinned,
                                    std::optional<int> new_index) = 0;
 
+  // Reorders the group with `sync_id` to be before or after the group with
+  // `next_sync_id` or `prev_sync_id`.
+  virtual void ReorderGroupBefore(const base::Uuid& sync_id,
+                                  const base::Uuid& next_sync_id) = 0;
+  virtual void ReorderGroupAfter(const base::Uuid& sync_id,
+                                 const base::Uuid& prev_sync_id) = 0;
+
   // Update bookmark node id of the tab group.
   // This is used to connect/disconnect bookmark folder with a saved tab group.
   virtual void UpdateBookmarkNodeId(
diff --git a/components/saved_tab_groups/test_support/fake_tab_group_sync_service.cc b/components/saved_tab_groups/test_support/fake_tab_group_sync_service.cc
index 8830ad6..77e6f2b 100644
--- a/components/saved_tab_groups/test_support/fake_tab_group_sync_service.cc
+++ b/components/saved_tab_groups/test_support/fake_tab_group_sync_service.cc
@@ -117,6 +117,18 @@
   NotifyObserversOfTabGroupUpdated(group);
 }
 
+void FakeTabGroupSyncService::ReorderGroupBefore(
+    const base::Uuid& sync_id,
+    const base::Uuid& next_sync_id) {
+  // No op.
+}
+
+void FakeTabGroupSyncService::ReorderGroupAfter(
+    const base::Uuid& sync_id,
+    const base::Uuid& prev_sync_id) {
+  // No op.
+}
+
 void FakeTabGroupSyncService::UpdateBookmarkNodeId(
     const base::Uuid& sync_id,
     std::optional<base::Uuid> bookmark_node_id) {
diff --git a/components/saved_tab_groups/test_support/fake_tab_group_sync_service.h b/components/saved_tab_groups/test_support/fake_tab_group_sync_service.h
index fb26f33..7849657 100644
--- a/components/saved_tab_groups/test_support/fake_tab_group_sync_service.h
+++ b/components/saved_tab_groups/test_support/fake_tab_group_sync_service.h
@@ -33,6 +33,10 @@
   void UpdateGroupPosition(const base::Uuid& sync_id,
                            std::optional<bool> is_pinned,
                            std::optional<int> new_index) override;
+  void ReorderGroupBefore(const base::Uuid& sync_id,
+                          const base::Uuid& next_sync_id) override;
+  void ReorderGroupAfter(const base::Uuid& sync_id,
+                         const base::Uuid& prev_sync_id) override;
   void UpdateBookmarkNodeId(
       const base::Uuid& sync_id,
       std::optional<base::Uuid> bookmark_node_id) override;
diff --git a/components/saved_tab_groups/test_support/mock_tab_group_sync_service.h b/components/saved_tab_groups/test_support/mock_tab_group_sync_service.h
index 55f6e27..90c62f24 100644
--- a/components/saved_tab_groups/test_support/mock_tab_group_sync_service.h
+++ b/components/saved_tab_groups/test_support/mock_tab_group_sync_service.h
@@ -32,7 +32,13 @@
               UpdateGroupPosition,
               (const base::Uuid& sync_id,
                std::optional<bool> is_pinned,
-               std ::optional<int> new_index));
+               std::optional<int> new_index));
+  MOCK_METHOD(void,
+              ReorderGroupBefore,
+              (const base::Uuid& sync_id, const base::Uuid& next_sync_id));
+  MOCK_METHOD(void,
+              ReorderGroupAfter,
+              (const base::Uuid& sync_id, const base::Uuid& prev_sync_id));
   MOCK_METHOD(void,
               UpdateBookmarkNodeId,
               (const base::Uuid&, std::optional<base::Uuid>));
diff --git a/components/services/storage/dom_storage/dom_storage_database.h b/components/services/storage/dom_storage/dom_storage_database.h
index dd27796..a3ac465d 100644
--- a/components/services/storage/dom_storage/dom_storage_database.h
+++ b/components/services/storage/dom_storage/dom_storage_database.h
@@ -310,13 +310,8 @@
   // the top-level site is same-site with one of those origins.
   virtual DbStatus PurgeOrigins(std::set<url::Origin> origins) = 0;
 
-  // For LevelDB only. Rewrites the database on disk to
-  // clean up traces of deleted entries.
-  //
-  // NOTE: If `RewriteDB()` fails, this DomStorageDatabase may no longer
-  // be usable; in such cases, all future operations will return an IOError
-  // status.
-  // TODO(crbug.com/485785252): Also implement this for the SQLite backend.
+  // Removes all traces of deleted data from the backing storage.  For example,
+  // removes all traces of an origin URL that might exist in the deleted data.
   virtual DbStatus RewriteDB() = 0;
 
   // Test-only functions.
diff --git a/components/services/storage/dom_storage/sqlite/local_storage_sqlite.cc b/components/services/storage/dom_storage/sqlite/local_storage_sqlite.cc
index a27826a..aacb79e 100644
--- a/components/services/storage/dom_storage/sqlite/local_storage_sqlite.cc
+++ b/components/services/storage/dom_storage/sqlite/local_storage_sqlite.cc
@@ -307,7 +307,7 @@
 }
 
 DbStatus LocalStorageSqlite::RewriteDB() {
-  // SQLite does not need to rewrite its database to fully erase deleted data.
+  RETURN_STATUS_ON_ERROR(database_->CheckpointDatabase(/*truncate=*/true));
   return DbStatus::OK();
 }
 
diff --git a/components/services/storage/dom_storage/sqlite/local_storage_sqlite_unittest.cc b/components/services/storage/dom_storage/sqlite/local_storage_sqlite_unittest.cc
index 3cc4509..83007ad 100644
--- a/components/services/storage/dom_storage/sqlite/local_storage_sqlite_unittest.cc
+++ b/components/services/storage/dom_storage/sqlite/local_storage_sqlite_unittest.cc
@@ -872,4 +872,49 @@
   EXPECT_EQ(actual_entries, kFirstPartyMapEntries);
 }
 
+TEST_F(LocalStorageSqliteTest, RewriteDB) {
+  std::unique_ptr<LocalStorageSqlite> database;
+  ASSERT_NO_FATAL_FAILURE(OpenOnDisk(&database));
+
+  // Add one metadata row to the database, which includes `kFirstStorageKey`.
+  const DomStorageDatabase::MapMetadata kExpectedMapMetadata{
+      .map_locator{kFirstMapLocator.Clone()},
+      .last_accessed{kMapLastAccessed},
+  };
+  ASSERT_NO_FATAL_FAILURE(
+      UpdateMapWithMetadata(*database, kExpectedMapMetadata));
+
+  // Add one key/value pair to the database.
+  const std::map<DomStorageDatabase::Key, DomStorageDatabase::Value>
+      kFirstMapEntries{
+          {ToBytes("key_1"), ToBytes("value_1")},
+      };
+  ASSERT_NO_FATAL_FAILURE(
+      InsertMapEntries(*database, kFirstMapLocator.Clone(), kFirstMapEntries));
+
+  // Delete the storage key's metadata and key/value pair from the database.
+  std::vector<DomStorageDatabase::MapLocator> maps_to_delete;
+  maps_to_delete.push_back(kFirstMapLocator.Clone());
+
+  DbStatus status = database->DeleteStorageKeysFromSession(
+      /*session_id=*/std::string(), {kFirstStorageKey},
+      std::move(maps_to_delete));
+  EXPECT_TRUE(status.ok()) << status.ToString();
+
+  // After deletion, `kFirstStorageKey` still exists in SQLite's WAL file.
+  const std::string kSerializedFirstStorageKey = kFirstStorageKey.Serialize();
+  ASSERT_NO_FATAL_FAILURE(SearchDirectoryContent(
+      temp_dir_.GetPath(), /*query=*/kSerializedFirstStorageKey,
+      /*expected_is_found=*/true));
+
+  // Use `RewriteDB()` to checkpoint and truncate the WAL file.
+  status = database->RewriteDB();
+  EXPECT_TRUE(status.ok()) << status.ToString();
+
+  // `kFirstStorageKey` must not exist on disk.
+  ASSERT_NO_FATAL_FAILURE(SearchDirectoryContent(
+      temp_dir_.GetPath(), /*query=*/kSerializedFirstStorageKey,
+      /*expected_is_found=*/false));
+}
+
 }  // namespace storage
diff --git a/components/services/storage/dom_storage/sqlite/session_storage_sqlite.cc b/components/services/storage/dom_storage/sqlite/session_storage_sqlite.cc
index 08c7071..56559070 100644
--- a/components/services/storage/dom_storage/sqlite/session_storage_sqlite.cc
+++ b/components/services/storage/dom_storage/sqlite/session_storage_sqlite.cc
@@ -256,7 +256,7 @@
 }
 
 DbStatus SessionStorageSqlite::RewriteDB() {
-  // SQLite does not need to rewrite its database to fully erase deleted data.
+  RETURN_STATUS_ON_ERROR(database_->CheckpointDatabase(/*truncate=*/true));
   return DbStatus::OK();
 }
 
diff --git a/components/services/storage/dom_storage/sqlite/session_storage_sqlite_unittest.cc b/components/services/storage/dom_storage/sqlite/session_storage_sqlite_unittest.cc
index bf71289..43ae0247 100644
--- a/components/services/storage/dom_storage/sqlite/session_storage_sqlite_unittest.cc
+++ b/components/services/storage/dom_storage/sqlite/session_storage_sqlite_unittest.cc
@@ -992,4 +992,46 @@
   EXPECT_EQ(actual_map_entries, kThirdMapEntries);
 }
 
+TEST_F(SessionStorageSqliteTest, RewriteDB) {
+  std::unique_ptr<SessionStorageSqlite> database;
+  ASSERT_NO_FATAL_FAILURE(OpenOnDisk(&database));
+
+  // Add one metadata row to the database, which includes `kFirstStorageKey`.
+  ASSERT_NO_FATAL_FAILURE(InitializeMetadata(
+      *database, DomStorageDatabase::Metadata(CloneMapMetadataVector(
+                     {{.map_locator{kFirstMapLocator.Clone()}}}))));
+
+  // Add one key/value pair to the database.
+  const std::map<DomStorageDatabase::Key, DomStorageDatabase::Value>
+      kFirstMapEntries{
+          {ToBytes("key_1"), ToBytes("value_1")},
+      };
+  ASSERT_NO_FATAL_FAILURE(
+      InsertMapEntries(*database, kFirstMapLocator.Clone(), kFirstMapEntries));
+
+  // Delete the storage key's metadata and key/value pair from the database.
+  std::vector<DomStorageDatabase::MapLocator> maps_to_delete;
+  maps_to_delete.push_back(kFirstMapLocator.Clone());
+
+  DbStatus status = database->DeleteStorageKeysFromSession(
+      kFirstSessionId, /*metadata_to_delete=*/{kFirstStorageKey},
+      /*maps_to_delete=*/{});
+  EXPECT_TRUE(status.ok()) << status.ToString();
+
+  // After deletion, `kFirstStorageKey` still exists in SQLite's WAL file.
+  const std::string kSerializedFirstStorageKey = kFirstStorageKey.Serialize();
+  ASSERT_NO_FATAL_FAILURE(SearchDirectoryContent(
+      temp_dir_.GetPath(), /*query=*/kSerializedFirstStorageKey,
+      /*expected_is_found=*/true));
+
+  // Use `RewriteDB()` to checkpoint and truncate the WAL file.
+  status = database->RewriteDB();
+  EXPECT_TRUE(status.ok()) << status.ToString();
+
+  // `kFirstStorageKey` must not exist on disk.
+  ASSERT_NO_FATAL_FAILURE(SearchDirectoryContent(
+      temp_dir_.GetPath(), /*query=*/kSerializedFirstStorageKey,
+      /*expected_is_found=*/false));
+}
+
 }  // namespace storage
diff --git a/components/services/storage/dom_storage/test_support/dom_storage_database_testing.cc b/components/services/storage/dom_storage/test_support/dom_storage_database_testing.cc
index 46166c2..a306144 100644
--- a/components/services/storage/dom_storage/test_support/dom_storage_database_testing.cc
+++ b/components/services/storage/dom_storage/test_support/dom_storage_database_testing.cc
@@ -6,10 +6,10 @@
 
 #include <algorithm>
 
-#include "base/memory/scoped_refptr.h"
+#include "base/files/file_enumerator.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
 #include "base/run_loop.h"
-#include "base/task/sequenced_task_runner.h"
-#include "base/task/thread_pool.h"
 #include "base/test/bind.h"
 #include "base/test/gmock_expected_support.h"
 #include "base/test/test_future.h"
@@ -370,4 +370,28 @@
   EXPECT_TRUE(status.ok()) << status.ToString();
 }
 
+void SearchDirectoryContent(const base::FilePath& directory_path,
+                            std::string query,
+                            bool expected_is_found) {
+  int query_found_count = 0;
+  base::FileEnumerator file_enumerator(directory_path, /*recursive=*/true,
+                                       base::FileEnumerator::FILES);
+
+  for (base::FilePath file_path = file_enumerator.Next(); !file_path.empty();
+       file_path = file_enumerator.Next()) {
+    std::string file_contents;
+    ASSERT_TRUE(base::ReadFileToString(file_path, &file_contents));
+
+    if (file_contents.find(query) != std::string::npos) {
+      ++query_found_count;
+      if (!expected_is_found) {
+        LOG(ERROR) << "Found '" << query << "' in " << file_path;
+      }
+    }
+  }
+
+  EXPECT_EQ(query_found_count > 0, expected_is_found)
+      << "Found '" << query << "' " << query_found_count << " times";
+}
+
 }  // namespace storage
diff --git a/components/services/storage/dom_storage/test_support/dom_storage_database_testing.h b/components/services/storage/dom_storage/test_support/dom_storage_database_testing.h
index b36154c6..d57d8ad 100644
--- a/components/services/storage/dom_storage/test_support/dom_storage_database_testing.h
+++ b/components/services/storage/dom_storage/test_support/dom_storage_database_testing.h
@@ -11,15 +11,14 @@
 #include <vector>
 
 #include "base/containers/span.h"
-#include "base/memory/scoped_refptr.h"
 #include "components/services/storage/dom_storage/async_dom_storage_database.h"
 #include "components/services/storage/dom_storage/dom_storage_database.h"
-#include "components/services/storage/dom_storage/session_storage_metadata.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
 
 namespace base {
+class FilePath;
 class RunLoop;
-}
+}  // namespace base
 
 namespace storage {
 
@@ -136,6 +135,13 @@
 void PutVersionForTesting(AsyncDomStorageDatabase& async_database,
                           int64_t version);
 
+// Enumerates all files under `directory_path`, searching for `query` in the
+// file's content. Fails when `query` results differ from `expected_is_found`.
+// Also fails after file read errors.
+void SearchDirectoryContent(const base::FilePath& directory_path,
+                            std::string query,
+                            bool expected_is_found);
+
 }  // namespace storage
 
 #endif  // COMPONENTS_SERVICES_STORAGE_DOM_STORAGE_TEST_SUPPORT_DOM_STORAGE_DATABASE_TESTING_H_
diff --git a/components/sessions/core/command_storage_backend.cc b/components/sessions/core/command_storage_backend.cc
index a5c3dd3..ee85fe5a 100644
--- a/components/sessions/core/command_storage_backend.cc
+++ b/components/sessions/core/command_storage_backend.cc
@@ -364,18 +364,7 @@
 #endif
 }
 
-// Returns the directory the files are stored in.
-base::FilePath GetSessionDirName(SessionType type,
-                                 const base::FilePath& supplied_path) {
-  if (type == SessionType::kOther) {
-    return supplied_path.DirName();
-  }
-  return supplied_path.Append(kSessionsDirectory);
-}
-
-base::FilePath::StringType GetSessionBaseName(
-    SessionType type,
-    const base::FilePath& supplied_path) {
+base::FilePath::StringType GetSessionBaseName(SessionType type) {
   switch (type) {
     case SessionType::kAppRestore:
       return kAppSessionFileNamePrefix;
@@ -383,18 +372,14 @@
       return kTabSessionFileNamePrefix;
     case SessionType::kSessionRestore:
       return kSessionFileNamePrefix;
-    case SessionType::kOther:
-      return supplied_path.BaseName().value();
   }
 }
 
 base::FilePath::StringType GetSessionFilename(
     SessionType type,
-    const base::FilePath& supplied_path,
     const base::FilePath::StringType& timestamp_str) {
-  return base::JoinString(
-      {GetSessionBaseName(type, supplied_path), timestamp_str},
-      kTimestampSeparator);
+  return base::JoinString({GetSessionBaseName(type), timestamp_str},
+                          kTimestampSeparator);
 }
 
 }  // namespace
@@ -552,9 +537,6 @@
 }
 
 void CommandStorageBackend::MoveCurrentSessionToLastSession() {
-  // TODO(sky): make this work for kOther.
-  DCHECK_NE(SessionType::kOther, type_);
-
   InitIfNecessary();
   CloseFile();
   DeleteLastSession();
@@ -604,7 +586,7 @@
   }
 
   inited_ = true;
-  base::CreateDirectory(GetSessionDirName(type_, supplied_path_));
+  base::CreateDirectory(supplied_path_.Append(kSessionsDirectory));
 
   // TODO(sky): this is expensive. See if it can be delayed.
   last_session_info_ = FindLastSessionFile();
@@ -618,8 +600,8 @@
     const SessionType type,
     const base::FilePath& path,
     base::Time time) {
-  return GetSessionDirName(type, path)
-      .Append(GetSessionFilename(type, path, TimestampToString(time)));
+  return path.Append(kSessionsDirectory)
+      .Append(GetSessionFilename(type, TimestampToString(time)));
 }
 
 // static
@@ -747,8 +729,8 @@
     SessionType type) {
   std::vector<SessionInfo> sessions;
   base::FileEnumerator file_enum(
-      GetSessionDirName(type, path), false, base::FileEnumerator::FILES,
-      GetSessionFilename(type, path, FILE_PATH_LITERAL("*")));
+      path.Append(kSessionsDirectory), false, base::FileEnumerator::FILES,
+      GetSessionFilename(type, FILE_PATH_LITERAL("*")));
   for (base::FilePath name = file_enum.Next(); !name.empty();
        name = file_enum.Next()) {
     base::Time file_time;
diff --git a/components/sessions/core/command_storage_backend_unittest.cc b/components/sessions/core/command_storage_backend_unittest.cc
index 22a8a94..8b6767d 100644
--- a/components/sessions/core/command_storage_backend_unittest.cc
+++ b/components/sessions/core/command_storage_backend_unittest.cc
@@ -64,10 +64,9 @@
   // testing::TestWithParam:
   void SetUp() override {
     ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
-    file_path_ = temp_dir_.GetPath().Append(FILE_PATH_LITERAL("Session"));
-    restore_path_ =
-        temp_dir_.GetPath().Append(FILE_PATH_LITERAL("SessionTestDirs"));
-    base::CreateDirectory(restore_path_);
+    init_path_ = temp_dir_.GetPath();
+    sessions_dir_ = init_path_.Append(kSessionsDirectory);
+    base::CreateDirectory(sessions_dir_);
   }
 
   void AssertCommandEqualsData(const TestData& data,
@@ -90,15 +89,8 @@
   scoped_refptr<CommandStorageBackend> CreateBackend(
       base::Clock* clock = nullptr) {
     return MakeRefCounted<CommandStorageBackend>(
-        task_environment_.GetMainThreadTaskRunner(), file_path_,
-        CommandStorageManager::SessionType::kOther, clock);
-  }
-
-  scoped_refptr<CommandStorageBackend> CreateBackendWithRestoreType() {
-    const CommandStorageManager::SessionType type =
-        CommandStorageManager::SessionType::kSessionRestore;
-    return MakeRefCounted<CommandStorageBackend>(
-        task_environment_.GetMainThreadTaskRunner(), restore_path_, type);
+        task_environment_.GetMainThreadTaskRunner(), init_path_,
+        CommandStorageManager::SessionType::kSessionRestore, clock);
   }
 
   // Functions that call into private members of CommandStorageBackend.
@@ -111,7 +103,7 @@
 
   std::vector<base::FilePath> GetSessionFilePathsSortedByReverseTimestamp() {
     auto infos = CommandStorageBackend::GetSessionFilesSortedByReverseTimestamp(
-        file_path_, CommandStorageManager::SessionType::kOther);
+        init_path_, CommandStorageManager::SessionType::kSessionRestore);
     std::vector<base::FilePath> result;
     for (const auto& info : infos) {
       result.push_back(info.path);
@@ -126,7 +118,8 @@
     return CommandStorageBackend::FilePathFromTime(type, path, time);
   }
 
-  bool copyTestDataToSessionFile(const std::string& test_data_filename) {
+  bool copyTestDataToSessionFile(const std::string& test_data_filename,
+                                 const std::string& session_filename) {
     base::FilePath test_file_path;
     if (!base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT,
                                 &test_file_path)) {
@@ -138,21 +131,16 @@
                          .AppendASCII("sessions");
     test_file_path = test_file_path.AppendASCII(test_data_filename);
     return base::CopyFile(test_file_path,
-                          file_path()
-                              .InsertBeforeExtension(kTimestampSeparator)
-                              // 1234 is a dummy timestamp.
-                              .InsertBeforeExtensionUTF8("1234"));
+                          sessions_dir().AppendUTF8(session_filename));
   }
 
-  const base::FilePath& file_path() const { return file_path_; }
-  const base::FilePath& restore_path() const { return restore_path_; }
+  const base::FilePath& init_path() const { return init_path_; }
+  const base::FilePath& sessions_dir() const { return sessions_dir_; }
 
  private:
   base::test::TaskEnvironment task_environment_;
-  // Path used by CreateBackend().
-  base::FilePath file_path_;
-  // Path used by CreateBackendWithRestoreType().
-  base::FilePath restore_path_;
+  base::FilePath init_path_;     // Passed to CommandStorageBackend constructor.
+  base::FilePath sessions_dir_;  // The directory containing the session files.
   base::ScopedTempDir temp_dir_;
 };
 
@@ -289,7 +277,6 @@
   return command;
 }
 
-
 TEST_F(CommandStorageBackendTest, MaxSizeType) {
   scoped_refptr<CommandStorageBackend> backend = CreateBackend();
 
@@ -313,11 +300,12 @@
 }
 
 TEST_F(CommandStorageBackendTest, IsValidFileWithInvalidFiles) {
-  base::WriteFile(file_path(), "z");
-  EXPECT_FALSE(CommandStorageBackend::IsValidFileForTest(file_path()));
+  const auto file_path = sessions_dir().AppendASCII("Session_123");
+  base::WriteFile(file_path, "z");  // invalid file contents
+  EXPECT_FALSE(CommandStorageBackend::IsValidFileForTest(file_path));
 
-  base::WriteFile(file_path(), "a longer string that does not match header");
-  EXPECT_FALSE(CommandStorageBackend::IsValidFileForTest(file_path()));
+  base::WriteFile(file_path, "a longer string that does not match header");
+  EXPECT_FALSE(CommandStorageBackend::IsValidFileForTest(file_path));
 }
 
 TEST_F(CommandStorageBackendTest, IsNotValidFileWithoutMarker) {
@@ -329,110 +317,26 @@
   EXPECT_FALSE(CommandStorageBackend::IsValidFileForTest(path));
 }
 
-TEST_F(CommandStorageBackendTest, SimpleReadWriteWithRestoreType) {
-  scoped_refptr<CommandStorageBackend> backend = CreateBackendWithRestoreType();
+TEST_F(CommandStorageBackendTest, DeleteLastSession) {
+  scoped_refptr<CommandStorageBackend> backend = CreateBackend();
   struct TestData data = {1, "a"};
   SessionCommands commands;
   commands.push_back(CreateCommandFromData(data));
   backend->AppendCommands(std::move(commands), true, base::DoNothing());
 
-  // Read it back in.
-  backend = nullptr;
-  backend = CreateBackendWithRestoreType();
-  commands.clear();
-  backend->AppendCommands(std::move(commands), true, base::DoNothing());
-  commands = backend->ReadLastSessionCommands().commands;
-
-  ASSERT_EQ(1U, commands.size());
-  AssertCommandEqualsData(data, commands[0].get());
-
-  backend = nullptr;
-  backend = CreateBackendWithRestoreType();
-  commands = backend->ReadLastSessionCommands().commands;
-
-  ASSERT_EQ(0U, commands.size());
-
-  // Make sure we can delete.
+  backend = CreateBackend();  // Necessary to recognize the file we just wrote.
   backend->DeleteLastSession();
   commands = backend->ReadLastSessionCommands().commands;
-  ASSERT_EQ(0U, commands.size());
-}
+  ASSERT_TRUE(commands.empty());
 
-TEST_F(CommandStorageBackendTest, RandomDataWithRestoreType) {
-  auto data = std::to_array<TestData>({
-      {1, "a"},
-      {2, "ab"},
-      {3, "abc"},
-      {4, "abcd"},
-      {5, "abcde"},
-      {6, "abcdef"},
-      {7, "abcdefg"},
-      {8, "abcdefgh"},
-      {9, "abcdefghi"},
-      {10, "abcdefghij"},
-      {11, "abcdefghijk"},
-      {12, "abcdefghijkl"},
-      {13, "abcdefghijklm"},
-  });
-
-  for (size_t i = 0; i < std::size(data); ++i) {
-    scoped_refptr<CommandStorageBackend> backend =
-        CreateBackendWithRestoreType();
-    SessionCommands commands;
-    if (i != 0) {
-      // Read previous data.
-      commands = backend->ReadLastSessionCommands().commands;
-      ASSERT_EQ(i, commands.size());
-      for (auto j = commands.begin(); j != commands.end(); ++j) {
-        AssertCommandEqualsData(data[j - commands.begin()], j->get());
-      }
-
-      // Write the previous data back.
-      backend->AppendCommands(std::move(commands), true, base::DoNothing());
-      commands.clear();
-    }
-    commands.push_back(CreateCommandFromData(data[i]));
-    backend->AppendCommands(std::move(commands), i == 0, base::DoNothing());
-  }
-}
-
-TEST_F(CommandStorageBackendTest, BigDataWithRestoreType) {
-  auto data = std::to_array<TestData>({
-      {1, "a"},
-      {2, "ab"},
-  });
-
-  scoped_refptr<CommandStorageBackend> backend = CreateBackendWithRestoreType();
-  std::vector<std::unique_ptr<SessionCommand>> commands;
-
-  commands.push_back(CreateCommandFromData(data[0]));
-  const SessionCommand::size_type big_size =
-      CommandStorageBackend::kFileReadBufferSize + 100;
-  const SessionCommand::id_type big_id = 50;
-  std::unique_ptr<SessionCommand> big_command =
-      std::make_unique<SessionCommand>(big_id, big_size);
-  big_command->contents()[0] = 'a';
-  big_command->contents()[big_size - 1] = 'z';
-  commands.push_back(std::move(big_command));
-  commands.push_back(CreateCommandFromData(data[1]));
-  backend->AppendCommands(std::move(commands), true, base::DoNothing());
-
-  backend = nullptr;
-  backend = CreateBackendWithRestoreType();
-
+  // Also confirm deletion with a new backend.
+  backend = CreateBackend();
   commands = backend->ReadLastSessionCommands().commands;
-  ASSERT_EQ(3U, commands.size());
-  AssertCommandEqualsData(data[0], commands[0].get());
-  AssertCommandEqualsData(data[1], commands[2].get());
-
-  EXPECT_EQ(big_id, commands[1]->id());
-  ASSERT_EQ(big_size, commands[1]->size());
-  EXPECT_EQ('a', commands[1]->contents()[0]);
-  EXPECT_EQ('z', commands[1]->contents()[big_size - 1]);
+  ASSERT_TRUE(commands.empty());
 }
 
-TEST_F(CommandStorageBackendTest, CommandWithRestoreType) {
-  scoped_refptr<CommandStorageBackend> backend = CreateBackendWithRestoreType();
+TEST_F(CommandStorageBackendTest, ReadEmptyCommands) {
+  scoped_refptr<CommandStorageBackend> backend = CreateBackend();
   SessionCommands commands;
   backend->AppendCommands(std::move(commands), true, base::DoNothing());
   backend->MoveCurrentSessionToLastSession();
@@ -441,33 +345,8 @@
   ASSERT_EQ(0U, commands.size());
 }
 
-// Writes a command, appends another command with reset to true, then reads
-// making sure we only get back the second command.
-TEST_F(CommandStorageBackendTest, TruncateWithRestoreType) {
-  scoped_refptr<CommandStorageBackend> backend = CreateBackendWithRestoreType();
-  struct TestData first_data = {1, "a"};
-  SessionCommands commands;
-  commands.push_back(CreateCommandFromData(first_data));
-  backend->AppendCommands(std::move(commands), false, base::DoNothing());
-  commands.clear();
-
-  // Write another command, this time resetting the file when appending.
-  struct TestData second_data = {2, "b"};
-  commands.push_back(CreateCommandFromData(second_data));
-  backend->AppendCommands(std::move(commands), true, base::DoNothing());
-
-  // Read it back in.
-  backend = nullptr;
-  backend = CreateBackendWithRestoreType();
-  commands = backend->ReadLastSessionCommands().commands;
-
-  // And make sure we get back the expected data.
-  ASSERT_EQ(1U, commands.size());
-  AssertCommandEqualsData(second_data, commands[0].get());
-}
-
 // Test parsing the timestamp of a session from the path.
-TEST_F(CommandStorageBackendTest, TimestampFromPathWithRestoreType) {
+TEST_F(CommandStorageBackendTest, TimestampFromPath) {
   const auto base_dir = base::FilePath(kSessionsDirectory);
 
   // Test parsing the timestamp from a valid session.
@@ -500,7 +379,7 @@
 }
 
 // Test serializing a timestamp to string.
-TEST_F(CommandStorageBackendTest, FilePathFromTimeWithRestoreType) {
+TEST_F(CommandStorageBackendTest, FilePathFromTime) {
   const auto base_dir = base::FilePath(kSessionsDirectory);
   const auto test_time_1 = base::Time();
   const auto result_path_1 =
@@ -518,22 +397,19 @@
 }
 
 // Test that the previous session is empty if no session files exist.
-TEST_F(CommandStorageBackendTest,
-       DeterminePreviousSessionEmptyWithRestoreType) {
-  scoped_refptr<CommandStorageBackend> backend = CreateBackendWithRestoreType();
+TEST_F(CommandStorageBackendTest, DeterminePreviousSessionEmpty) {
+  scoped_refptr<CommandStorageBackend> backend = CreateBackend();
   ASSERT_FALSE(GetLastSessionInfo(backend.get()));
 }
 
 // Test that the previous session is selected correctly when a file is present.
-TEST_F(CommandStorageBackendTest,
-       DeterminePreviousSessionSingleWithRestoreType) {
-  const auto prev_path = restore_path().Append(
-      base::FilePath(kSessionsDirectory)
-          .Append(FILE_PATH_LITERAL("Session_13235178308836991")));
+TEST_F(CommandStorageBackendTest, DeterminePreviousSessionSingle) {
+  const auto prev_path =
+      sessions_dir().AppendASCII("Session_13235178308836991");
   ASSERT_TRUE(base::CreateDirectory(prev_path.DirName()));
   ASSERT_TRUE(base::WriteFile(prev_path, ""));
 
-  scoped_refptr<CommandStorageBackend> backend = CreateBackendWithRestoreType();
+  scoped_refptr<CommandStorageBackend> backend = CreateBackend();
   auto last_session_info = GetLastSessionInfo(backend.get());
   ASSERT_TRUE(last_session_info);
   ASSERT_EQ(prev_path, last_session_info->path);
@@ -541,51 +417,43 @@
 
 // Test that the previous session is selected correctly when multiple session
 // files are present.
-TEST_F(CommandStorageBackendTest,
-       DeterminePreviousSessionMultipleWithRestoreType) {
-  const auto sessions_dir =
-      restore_path().Append(base::FilePath(kSessionsDirectory));
+TEST_F(CommandStorageBackendTest, DeterminePreviousSessionMultiple) {
   const auto prev_path =
-      sessions_dir.Append(FILE_PATH_LITERAL("Session_13235178308836991"));
+      sessions_dir().Append(FILE_PATH_LITERAL("Session_13235178308836991"));
   const auto old_path_1 =
-      sessions_dir.Append(FILE_PATH_LITERAL("Session_13235178308548874"));
-  const auto old_path_2 = sessions_dir.Append(FILE_PATH_LITERAL("Session_0"));
+      sessions_dir().Append(FILE_PATH_LITERAL("Session_13235178308548874"));
+  const auto old_path_2 = sessions_dir().Append(FILE_PATH_LITERAL("Session_0"));
   ASSERT_TRUE(base::CreateDirectory(prev_path.DirName()));
   ASSERT_TRUE(base::WriteFile(prev_path, ""));
   ASSERT_TRUE(base::WriteFile(old_path_1, ""));
   ASSERT_TRUE(base::WriteFile(old_path_2, ""));
 
-  scoped_refptr<CommandStorageBackend> backend = CreateBackendWithRestoreType();
+  scoped_refptr<CommandStorageBackend> backend = CreateBackend();
   auto last_session_info = GetLastSessionInfo(backend.get());
   ASSERT_TRUE(last_session_info);
   ASSERT_EQ(prev_path, last_session_info->path);
 }
 
 // Test that the a file with an invalid name won't be used.
-TEST_F(CommandStorageBackendTest,
-       DeterminePreviousSessionInvalidWithRestoreType) {
+TEST_F(CommandStorageBackendTest, DeterminePreviousSessionInvalid) {
   const auto prev_path =
-      restore_path().Append(base::FilePath(kSessionsDirectory)
-                                .Append(FILE_PATH_LITERAL("Session_invalid")));
+      sessions_dir().Append(FILE_PATH_LITERAL("Session_invalid"));
   ASSERT_TRUE(base::CreateDirectory(prev_path.DirName()));
   ASSERT_TRUE(base::WriteFile(prev_path, ""));
 
-  scoped_refptr<CommandStorageBackend> backend = CreateBackendWithRestoreType();
+  scoped_refptr<CommandStorageBackend> backend = CreateBackend();
   auto last_session_info = GetLastSessionInfo(backend.get());
   ASSERT_FALSE(last_session_info);
 }
 
 // Tests that MoveCurrentSessionToLastSession deletes the last session file.
-TEST_F(CommandStorageBackendTest,
-       MoveCurrentSessionToLastDeletesLastSessionWithRestoreType) {
-  const auto sessions_dir =
-      restore_path().Append(base::FilePath(kSessionsDirectory));
+TEST_F(CommandStorageBackendTest, MoveCurrentSessionToLastDeletesLastSession) {
   const auto last_session =
-      sessions_dir.Append(FILE_PATH_LITERAL("Session_13235178308548874"));
+      sessions_dir().Append(FILE_PATH_LITERAL("Session_13235178308548874"));
   ASSERT_TRUE(base::CreateDirectory(last_session.DirName()));
   ASSERT_TRUE(base::WriteFile(last_session, ""));
 
-  scoped_refptr<CommandStorageBackend> backend = CreateBackendWithRestoreType();
+  scoped_refptr<CommandStorageBackend> backend = CreateBackend();
   char buffer[1];
   ASSERT_EQ(0, base::ReadFile(last_session, buffer, 0));
   backend->MoveCurrentSessionToLastSession();
@@ -593,34 +461,26 @@
 }
 
 TEST_F(CommandStorageBackendTest, GetSessionFiles) {
-  EXPECT_TRUE(CommandStorageBackend::GetSessionFilePaths(file_path(),
-                                                         SessionType::kOther)
+  EXPECT_TRUE(CommandStorageBackend::GetSessionFilePaths(
+                  init_path(), SessionType::kSessionRestore)
                   .empty());
-  ASSERT_TRUE(base::WriteFile(file_path(), ""));
   // Not a valid name, as doesn't contain timestamp separator.
-  ASSERT_TRUE(
-      base::WriteFile(file_path().DirName().AppendASCII("Session 123"), ""));
+  ASSERT_TRUE(base::WriteFile(sessions_dir().AppendASCII("Session 123"), ""));
   // Valid name.
-  ASSERT_TRUE(
-      base::WriteFile(file_path().DirName().AppendASCII("Session_124"), ""));
+  ASSERT_TRUE(base::WriteFile(sessions_dir().AppendASCII("Session_124"), ""));
   // Valid name, but should not be returned as beginning doesn't match.
-  ASSERT_TRUE(
-      base::WriteFile(file_path().DirName().AppendASCII("Foo_125"), ""));
-  auto paths = CommandStorageBackend::GetSessionFilePaths(file_path(),
-                                                          SessionType::kOther);
+  ASSERT_TRUE(base::WriteFile(sessions_dir().AppendASCII("Foo_125"), ""));
+  auto paths = CommandStorageBackend::GetSessionFilePaths(
+      init_path(), SessionType::kSessionRestore);
   ASSERT_EQ(1u, paths.size());
   EXPECT_EQ("Session_124", paths.begin()->BaseName().MaybeAsASCII());
 }
 
 TEST_F(CommandStorageBackendTest, GetSessionFilesAreSortedByReverseTimestamp) {
-  ASSERT_TRUE(
-      base::WriteFile(file_path().DirName().AppendASCII("Session_130"), ""));
-  ASSERT_TRUE(
-      base::WriteFile(file_path().DirName().AppendASCII("Session_120"), ""));
-  ASSERT_TRUE(
-      base::WriteFile(file_path().DirName().AppendASCII("Session_125"), ""));
-  ASSERT_TRUE(
-      base::WriteFile(file_path().DirName().AppendASCII("Session_128"), ""));
+  ASSERT_TRUE(base::WriteFile(sessions_dir().AppendASCII("Session_130"), ""));
+  ASSERT_TRUE(base::WriteFile(sessions_dir().AppendASCII("Session_120"), ""));
+  ASSERT_TRUE(base::WriteFile(sessions_dir().AppendASCII("Session_125"), ""));
+  ASSERT_TRUE(base::WriteFile(sessions_dir().AppendASCII("Session_128"), ""));
   auto paths = GetSessionFilePathsSortedByReverseTimestamp();
   ASSERT_EQ(4u, paths.size());
   EXPECT_EQ("Session_130", paths[0].BaseName().MaybeAsASCII());
@@ -638,7 +498,7 @@
 
   // Read it back in.
   backend = nullptr;
-  backend = CreateBackendWithRestoreType();
+  backend = CreateBackend();
   commands = backend->ReadLastSessionCommands().commands;
   // There should be no commands as a valid marker was not written.
   ASSERT_TRUE(commands.empty());
@@ -650,11 +510,12 @@
 TEST_F(CommandStorageBackendTest, ReadSessionFileV1) {
   // V1 files do not contain markers.
   // They were used in production prior to commit 223e5cd on 2021-05-25.
-  ASSERT_TRUE(copyTestDataToSessionFile("Session-v1NoMarker"));
+  ASSERT_TRUE(copyTestDataToSessionFile("Session-v1NoMarker", "Session_1234"));
 
   // V1 files are no longer supported.
   scoped_refptr<CommandStorageBackend> backend = CreateBackend();
-  ASSERT_FALSE(backend->IsValidFileForTest(file_path()));
+  ASSERT_FALSE(
+      backend->IsValidFileForTest(sessions_dir().AppendASCII("Session_1234")));
   SessionCommands commands = backend->ReadLastSessionCommands().commands;
   ASSERT_TRUE(commands.empty());
 }
@@ -663,11 +524,13 @@
   // V2 files are encrypted and do not contain markers.
   // They could have been written prior to commit 223e5cd on 2021-05-25.
   // They were never used in production.
-  ASSERT_TRUE(copyTestDataToSessionFile("Session-v2NoMarkerEncrypted"));
+  ASSERT_TRUE(
+      copyTestDataToSessionFile("Session-v2NoMarkerEncrypted", "Session_1234"));
 
   // V2 files are no longer supported.
   scoped_refptr<CommandStorageBackend> backend = CreateBackend();
-  ASSERT_FALSE(backend->IsValidFileForTest(file_path()));
+  ASSERT_FALSE(
+      backend->IsValidFileForTest(sessions_dir().AppendASCII("Session_1234")));
   SessionCommands commands = backend->ReadLastSessionCommands().commands;
   ASSERT_TRUE(commands.empty());
 }
@@ -675,7 +538,8 @@
 TEST_F(CommandStorageBackendTest, ReadSessionFileV3) {
   // V3 files contain markers.
   // They have been used in production from early 2021 through at least 2026-02.
-  ASSERT_TRUE(copyTestDataToSessionFile("Session-v3WithMarker"));
+  ASSERT_TRUE(
+      copyTestDataToSessionFile("Session-v3WithMarker", "Session_1234"));
 
   scoped_refptr<CommandStorageBackend> backend = CreateBackend();
   SessionCommands commands = backend->ReadLastSessionCommands().commands;
@@ -689,17 +553,19 @@
   // V4 files contain markers and are encrypted.
   // They have never been used in production, but could have been written from
   // early 2021 through at least 2026-02.
-  ASSERT_TRUE(copyTestDataToSessionFile("Session-v4WithMarkerEncrypted"));
+  ASSERT_TRUE(copyTestDataToSessionFile("Session-v4WithMarkerEncrypted",
+                                        "Session_1234"));
 
   // V4 files are no longer supported.
   scoped_refptr<CommandStorageBackend> backend = CreateBackend();
-  ASSERT_FALSE(backend->IsValidFileForTest(file_path()));
+  ASSERT_FALSE(
+      backend->IsValidFileForTest(sessions_dir().AppendASCII("Session_1234")));
   SessionCommands commands = backend->ReadLastSessionCommands().commands;
   ASSERT_TRUE(commands.empty());
 }
 
 TEST_F(CommandStorageBackendTest, NewFileOnTruncate) {
-  scoped_refptr<CommandStorageBackend> backend = CreateBackendWithRestoreType();
+  scoped_refptr<CommandStorageBackend> backend = CreateBackend();
   struct TestData data = {1, "a"};
   SessionCommands commands;
   commands.push_back(CreateCommandFromData(data));
diff --git a/components/sessions/core/command_storage_manager.h b/components/sessions/core/command_storage_manager.h
index ec92746..103385b 100644
--- a/components/sessions/core/command_storage_manager.h
+++ b/components/sessions/core/command_storage_manager.h
@@ -45,19 +45,12 @@
 
   // Identifies the type of session service this is. This is used by the
   // backend to determine the name of the files.
-  // TODO(sky): this enum is purely for legacy reasons, and should be replaced
-  // with consumers building the path. Remove in approximately a year (1/2022),
-  // when we shouldn't need to worry too much about migrating older data.
-  enum class SessionType { kAppRestore, kSessionRestore, kTabRestore, kOther };
+  enum class SessionType { kAppRestore, kSessionRestore, kTabRestore };
 
   // Creates a new CommandStorageManager. `delegate` is not owned by this and
   // must outlive this.
   //
-  // The meaning of `path` depends upon the type. If `type` is `kOther`, then
-  // the path is a file name to which `_TIMESTAMP` is added. If `type` is not
-  // `kOther`, then it is a path to a directory. The actual file name used
-  // depends upon the type. Once SessionType can be removed, this logic can
-  // standardize on that of `kOther`.
+  // `path` is the base directory into which session files are written.
   CommandStorageManager(
       SessionType type,
       const base::FilePath& path,
diff --git a/components/sessions/core/command_storage_manager_unittest.cc b/components/sessions/core/command_storage_manager_unittest.cc
index 44d5184..2be4cf5 100644
--- a/components/sessions/core/command_storage_manager_unittest.cc
+++ b/components/sessions/core/command_storage_manager_unittest.cc
@@ -22,7 +22,7 @@
   // testing::TestWithParam:
   void SetUp() override {
     ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
-    path_ = temp_dir_.GetPath().Append(FILE_PATH_LITERAL("Session"));
+    path_ = temp_dir_.GetPath();
   }
 
   base::FilePath path_;
@@ -49,7 +49,7 @@
 
 TEST_F(CommandStorageManagerTest, OnErrorWritingSessionCommands) {
   TestCommandStorageManagerDelegate delegate;
-  CommandStorageManager manager(SessionType::kOther, path_, &delegate);
+  CommandStorageManager manager(SessionType::kSessionRestore, path_, &delegate);
   CommandStorageManagerTestHelper test_helper(&manager);
   manager.set_pending_reset(true);
   // Write a command, the delegate should not be notified of an error.
diff --git a/components/viz/common/features.cc b/components/viz/common/features.cc
index fe88944..c37673a 100644
--- a/components/viz/common/features.cc
+++ b/components/viz/common/features.cc
@@ -361,7 +361,12 @@
 // messages and, in turn, all interfaces associated with it e.g. root compositor
 // frame sink, display private - skipping the IO thread hop.
 BASE_FEATURE(kVizDirectCompositorThreadIpcFrameSinkManager,
-             base::FEATURE_DISABLED_BY_DEFAULT);
+#if BUILDFLAG(IS_ANDROID)
+             base::FEATURE_ENABLED_BY_DEFAULT
+#else
+             base::FEATURE_DISABLED_BY_DEFAULT
+#endif
+);
 
 // Switches the message pump to base::MessagePumpType::IO on the Viz thread.
 BASE_FEATURE(kVizWithIoMessagePump, base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/content/browser/browser_main_loop.cc b/content/browser/browser_main_loop.cc
index 6dd9039..21ca6e9 100644
--- a/content/browser/browser_main_loop.cc
+++ b/content/browser/browser_main_loop.cc
@@ -368,7 +368,7 @@
   std::unique_ptr<memory_pressure::MultiSourceMemoryPressureMonitor> monitor;
 
 #if BUILDFLAG(IS_APPLE) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_FUCHSIA) || \
-    BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+    BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID)
   monitor =
       std::make_unique<memory_pressure::MultiSourceMemoryPressureMonitor>();
 #endif
@@ -377,6 +377,13 @@
   if (monitor)
     monitor->MaybeStartPlatformVoter();
 
+#if BUILDFLAG(IS_ANDROID)
+  if (auto evaluator = UserLevelMemoryPressureSignalGenerator::MaybeCreate(
+          monitor->CreateVoter())) {
+    monitor->SetSystemEvaluator(std::move(evaluator));
+  }
+#endif
+
   return monitor;
 }
 
@@ -774,9 +781,6 @@
   }
 
   InitializeMemoryManagementComponent();
-#if BUILDFLAG(IS_ANDROID)
-  content::UserLevelMemoryPressureSignalGenerator::Initialize();
-#endif
 
 #if BUILDFLAG(ENABLE_PLUGINS)
   // Prior to any processing happening on the IO thread, we create the
diff --git a/content/browser/dom_storage/dom_storage_browsertest.cc b/content/browser/dom_storage/dom_storage_browsertest.cc
index eb92f15..0abc43f3 100644
--- a/content/browser/dom_storage/dom_storage_browsertest.cc
+++ b/content/browser/dom_storage/dom_storage_browsertest.cc
@@ -6,8 +6,10 @@
 #include "base/functional/bind.h"
 #include "base/run_loop.h"
 #include "base/test/bind.h"
+#include "base/test/with_feature_override.h"
 #include "base/threading/thread_restrictions.h"
 #include "build/build_config.h"
+#include "components/services/storage/dom_storage/features.h"
 #include "components/services/storage/dom_storage/local_storage_impl.h"
 #include "components/services/storage/public/cpp/constants.h"
 #include "components/services/storage/public/cpp/filesystem/filesystem_proxy.h"
@@ -35,9 +37,11 @@
 
 // This browser test is aimed towards exercising the DOMStorage system
 // from end-to-end.
-class DOMStorageBrowserTest : public ContentBrowserTest {
+class DOMStorageBrowserTest : public base::test::WithFeatureOverride,
+                              public ContentBrowserTest {
  public:
-  DOMStorageBrowserTest() {}
+  DOMStorageBrowserTest()
+      : base::test::WithFeatureOverride(storage::kDomStorageSqlite) {}
 
   void SimpleTest(const GURL& test_url, bool incognito) {
     // The test page will perform tests then navigate to either
@@ -87,11 +91,17 @@
 static const bool kIncognito = true;
 static const bool kNotIncognito = false;
 
-IN_PROC_BROWSER_TEST_F(DOMStorageBrowserTest, SanityCheck) {
+IN_PROC_BROWSER_TEST_P(DOMStorageBrowserTest, SanityCheck) {
   SimpleTest(GetTestUrl("dom_storage", "sanity_check.html"), kNotIncognito);
 }
 
-IN_PROC_BROWSER_TEST_F(DOMStorageBrowserTest, SanityCheckIncognito) {
+// TODO(crbug.com/488417166): Fix flakiness on android-x86-rel and re-enable.
+#if BUILDFLAG(IS_ANDROID)
+#define MAYBE_SanityCheckIncognito DISABLED_SanityCheckIncognito
+#else
+#define MAYBE_SanityCheckIncognito SanityCheckIncognito
+#endif
+IN_PROC_BROWSER_TEST_P(DOMStorageBrowserTest, MAYBE_SanityCheckIncognito) {
   SimpleTest(GetTestUrl("dom_storage", "sanity_check.html"), kIncognito);
 }
 
@@ -102,7 +112,7 @@
 #else
 #define MAYBE_DataPersists DataPersists
 #endif
-IN_PROC_BROWSER_TEST_F(DOMStorageBrowserTest, PRE_DataPersists) {
+IN_PROC_BROWSER_TEST_P(DOMStorageBrowserTest, PRE_DataPersists) {
   SimpleTest(GetTestUrl("dom_storage", "store_data.html"), kNotIncognito);
 
   // Browser shutdown can always race with async work on non-shutdown-blocking
@@ -120,7 +130,7 @@
   loop.Run();
 }
 
-IN_PROC_BROWSER_TEST_F(DOMStorageBrowserTest, MAYBE_DataPersists) {
+IN_PROC_BROWSER_TEST_P(DOMStorageBrowserTest, MAYBE_DataPersists) {
   SimpleTest(GetTestUrl("dom_storage", "verify_data.html"), kNotIncognito);
 }
 
@@ -130,7 +140,7 @@
 #else
 #define MAYBE_DeletePhysicalStorageKey DeletePhysicalStorageKey
 #endif
-IN_PROC_BROWSER_TEST_F(DOMStorageBrowserTest, MAYBE_DeletePhysicalStorageKey) {
+IN_PROC_BROWSER_TEST_P(DOMStorageBrowserTest, MAYBE_DeletePhysicalStorageKey) {
   EXPECT_EQ(0U, GetUsage().size());
   SimpleTest(GetTestUrl("dom_storage", "store_data.html"), kNotIncognito);
   std::vector<StorageUsageInfo> usage = GetUsage();
@@ -146,7 +156,7 @@
 // is no disagreement between 1) site URL used for browser-side isolation
 // enforcement and 2) the origin requested by Blink.  Before this bug was fixed,
 // (1) was file://localhost/ and (2) was file:// - this led to renderer kills.
-IN_PROC_BROWSER_TEST_F(DOMStorageBrowserTest, FileUrlWithHost) {
+IN_PROC_BROWSER_TEST_P(DOMStorageBrowserTest, FileUrlWithHost) {
   // Navigate to file://localhost/.../title1.html
   GURL regular_file_url = GetTestUrl(nullptr, "title1.html");
   GURL::Replacements host_replacement;
@@ -168,4 +178,12 @@
 }
 #endif
 
+INSTANTIATE_TEST_SUITE_P(
+    /*no prefix*/,
+    DOMStorageBrowserTest,
+    testing::Bool(),
+    [](const testing::TestParamInfo<DOMStorageBrowserTest::ParamType>& info) {
+      return info.param ? "SQLite" : "LevelDB";
+    });
+
 }  // namespace content
diff --git a/content/browser/memory_pressure/user_level_memory_pressure_signal_generator.cc b/content/browser/memory_pressure/user_level_memory_pressure_signal_generator.cc
index b272d08..b1681d90e 100644
--- a/content/browser/memory_pressure/user_level_memory_pressure_signal_generator.cc
+++ b/content/browser/memory_pressure/user_level_memory_pressure_signal_generator.cc
@@ -18,6 +18,7 @@
 #include "base/files/scoped_file.h"
 #include "base/functional/bind.h"
 #include "base/memory/memory_pressure_listener_registry.h"
+#include "base/memory/ptr_util.h"
 #include "base/metrics/field_trial_params.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/metrics/histogram_macros.h"
@@ -52,49 +53,53 @@
 // system memory were 6GB.
 constexpr base::ByteCount kMemoryThresholdOf6GbDevices = base::MiB(494);
 
+UserLevelMemoryPressureSignalGenerator* g_instance = nullptr;
+
 }  // namespace
 
 // static
-void UserLevelMemoryPressureSignalGenerator::Initialize() {
+std::unique_ptr<UserLevelMemoryPressureSignalGenerator>
+UserLevelMemoryPressureSignalGenerator::MaybeCreate(
+    std::unique_ptr<memory_pressure::MemoryPressureVoter> voter) {
+  std::unique_ptr<UserLevelMemoryPressureSignalGenerator> generator;
+
   if (base::SysInfo::Is4GbDevice() || base::SysInfo::Is6GbDevice()) {
-    auto memory_threshold = base::SysInfo::Is4GbDevice()
-                                ? kMemoryThresholdOf4GbDevices
-                                : kMemoryThresholdOf6GbDevices;
-    UserLevelMemoryPressureSignalGenerator::Get().Start(
-        memory_threshold, kDefaultMeasurementInterval, kDefaultMinimumInterval);
+    generator = base::WrapUnique(
+        new UserLevelMemoryPressureSignalGenerator(std::move(voter)));
   }
+
+  return generator;
+}
+
+UserLevelMemoryPressureSignalGenerator::
+    ~UserLevelMemoryPressureSignalGenerator() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  CHECK_EQ(g_instance, this);
+  g_instance = nullptr;
 }
 
 // static
 std::optional<UserLevelMemoryPressureMetrics>
 UserLevelMemoryPressureSignalGenerator::GetLatestMemoryMetrics() {
-  return Get().latest_metrics_;
+  return g_instance ? g_instance->GetLatestMemoryMetricsImpl() : std::nullopt;
 }
 
-// static
-UserLevelMemoryPressureSignalGenerator&
-UserLevelMemoryPressureSignalGenerator::Get() {
-  static base::NoDestructor<UserLevelMemoryPressureSignalGenerator> instance;
-  return *instance.get();
-}
+UserLevelMemoryPressureSignalGenerator::UserLevelMemoryPressureSignalGenerator(
+    std::unique_ptr<memory_pressure::MemoryPressureVoter> voter)
+    : memory_pressure::SystemMemoryPressureEvaluator(std::move(voter)),
+      memory_threshold_(base::SysInfo::Is4GbDevice()
+                            ? kMemoryThresholdOf4GbDevices
+                            : kMemoryThresholdOf6GbDevices),
+      measure_interval_(kDefaultMeasurementInterval),
+      minimum_interval_(kDefaultMinimumInterval) {
+  CHECK(!g_instance);
+  g_instance = this;
 
-UserLevelMemoryPressureSignalGenerator::
-    UserLevelMemoryPressureSignalGenerator() = default;
-UserLevelMemoryPressureSignalGenerator::
-    ~UserLevelMemoryPressureSignalGenerator() = default;
-
-void UserLevelMemoryPressureSignalGenerator::Start(
-    base::ByteCount memory_threshold,
-    base::TimeDelta measure_interval,
-    base::TimeDelta minimum_interval) {
-  memory_threshold_ = memory_threshold;
-  measure_interval_ = measure_interval;
-  minimum_interval_ = minimum_interval;
-  UserLevelMemoryPressureSignalGenerator::Get().StartPeriodicTimer(
-      kFirstMeasurementInterval);
+  StartPeriodicTimer(kFirstMeasurementInterval);
 }
 
 void UserLevelMemoryPressureSignalGenerator::StartMetricsCollection() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   periodic_measuring_timer_.Start(
       FROM_HERE, kDefaultMeasurementInterval,
       base::BindRepeating(
@@ -103,6 +108,7 @@
 }
 
 void UserLevelMemoryPressureSignalGenerator::CollectMemoryMetrics() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   base::SystemMemoryInfo meminfo;
   base::GetSystemMemoryInfo(&meminfo);
 
@@ -135,7 +141,22 @@
   };
 }
 
+void UserLevelMemoryPressureSignalGenerator::StartPeriodicTimer(
+    base::TimeDelta interval) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  // Don't try to start the timer in tests that don't support it.
+  if (!base::SequencedTaskRunner::HasCurrentDefault()) {
+    return;
+  }
+  periodic_measuring_timer_.Start(
+      FROM_HERE, interval,
+      base::BindOnce(&UserLevelMemoryPressureSignalGenerator::OnTimerFired,
+                     // Unretained is safe because |this| owns this timer.
+                     base::Unretained(this)));
+}
+
 void UserLevelMemoryPressureSignalGenerator::OnTimerFired() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   base::TimeDelta interval = measure_interval_;
   base::ByteCount total_pmf =
       GetTotalPrivateFootprintVisibleOrHigherPriorityRenderers();
@@ -153,20 +174,8 @@
   StartPeriodicTimer(interval);
 }
 
-void UserLevelMemoryPressureSignalGenerator::StartPeriodicTimer(
-    base::TimeDelta interval) {
-  // Don't try to start the timer in tests that don't support it.
-  if (!base::SequencedTaskRunner::HasCurrentDefault()) {
-    return;
-  }
-  periodic_measuring_timer_.Start(
-      FROM_HERE, interval,
-      base::BindOnce(&UserLevelMemoryPressureSignalGenerator::OnTimerFired,
-                     // Unretained is safe because |this| owns this timer.
-                     base::Unretained(this)));
-}
-
 void UserLevelMemoryPressureSignalGenerator::StartReportingTimer() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   // Don't try to start the timer in tests that don't support it.
   if (!base::SequencedTaskRunner::HasCurrentDefault()) {
     return;
@@ -179,6 +188,7 @@
 }
 
 void UserLevelMemoryPressureSignalGenerator::OnReportingTimerFired() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   base::ByteCount total_pmf =
       GetTotalPrivateFootprintVisibleOrHigherPriorityRenderers();
   ReportBeforeAfterMetrics(total_pmf, "After");
@@ -250,6 +260,7 @@
 
 void UserLevelMemoryPressureSignalGenerator::HandleMemoryPressureLevel(
     base::MemoryPressureLevel level) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   // MODERATE level is not used.
   CHECK_NE(level, base::MEMORY_PRESSURE_LEVEL_MODERATE);
 
@@ -261,7 +272,8 @@
   }
 
   current_level_ = level;
-  base::MemoryPressureListenerRegistry::NotifyMemoryPressure(level);
+  SetCurrentVote(level);
+  SendCurrentVote(true);
 }
 
 // static
@@ -353,4 +365,10 @@
   return CalculateProcessMemoryFootprint(statm_file, status_file);
 }
 
+std::optional<content::UserLevelMemoryPressureMetrics>
+UserLevelMemoryPressureSignalGenerator::GetLatestMemoryMetricsImpl() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  return latest_metrics_;
+}
+
 }  // namespace content
diff --git a/content/browser/memory_pressure/user_level_memory_pressure_signal_generator.h b/content/browser/memory_pressure/user_level_memory_pressure_signal_generator.h
index a2920b3..eb7d3739 100644
--- a/content/browser/memory_pressure/user_level_memory_pressure_signal_generator.h
+++ b/content/browser/memory_pressure/user_level_memory_pressure_signal_generator.h
@@ -5,14 +5,18 @@
 #ifndef CONTENT_BROWSER_MEMORY_PRESSURE_USER_LEVEL_MEMORY_PRESSURE_SIGNAL_GENERATOR_H_
 #define CONTENT_BROWSER_MEMORY_PRESSURE_USER_LEVEL_MEMORY_PRESSURE_SIGNAL_GENERATOR_H_
 
+#include <memory>
 #include <optional>
 #include <utility>
 
 #include "base/byte_count.h"
 #include "base/memory/memory_pressure_level.h"
-#include "base/no_destructor.h"
+#include "base/sequence_checker.h"
 #include "base/timer/timer.h"
 #include "build/build_config.h"
+#include "components/memory_pressure/memory_pressure_voter.h"
+#include "components/memory_pressure/multi_source_memory_pressure_monitor.h"
+#include "components/memory_pressure/system_memory_pressure_evaluator.h"
 #include "content/public/browser/user_level_memory_pressure_metrics.h"
 
 namespace base {
@@ -24,35 +28,34 @@
 
 // Generates extra memory pressure signals (on top of the OS generated ones)
 // when the memory usage exceeds a threshold.
-class UserLevelMemoryPressureSignalGenerator {
+class UserLevelMemoryPressureSignalGenerator
+    : public memory_pressure::SystemMemoryPressureEvaluator {
  public:
-  static void Initialize();
+  // Creates an instance. Returns nullptr if
+  // UserLevelMemoryPressureSignalGenerator if disabled on this device.
+  static std::unique_ptr<UserLevelMemoryPressureSignalGenerator> MaybeCreate(
+      std::unique_ptr<memory_pressure::MemoryPressureVoter> voter);
+
+  ~UserLevelMemoryPressureSignalGenerator() override;
 
   // Returns the latest memory metrics if the metrics collection is enabled.
   static std::optional<content::UserLevelMemoryPressureMetrics>
   GetLatestMemoryMetrics();
 
  private:
-  friend class base::NoDestructor<UserLevelMemoryPressureSignalGenerator>;
-
-  // Singleton
-  static UserLevelMemoryPressureSignalGenerator& Get();
-
-  UserLevelMemoryPressureSignalGenerator();
-  ~UserLevelMemoryPressureSignalGenerator();
-
-  void Start(base::ByteCount memory_threshold,
-             base::TimeDelta measure_interval,
-             base::TimeDelta minimum_interval);
-  void OnTimerFired();
-  void OnReportingTimerFired();
-
-  void StartPeriodicTimer(base::TimeDelta interval);
-  void StartReportingTimer();
+  explicit UserLevelMemoryPressureSignalGenerator(
+      std::unique_ptr<memory_pressure::MemoryPressureVoter> voter);
 
   void StartMetricsCollection();
+
   void CollectMemoryMetrics();
 
+  void StartPeriodicTimer(base::TimeDelta interval);
+  void OnTimerFired();
+
+  void StartReportingTimer();
+  void OnReportingTimerFired();
+
   static base::ByteCount
   GetTotalPrivateFootprintVisibleOrHigherPriorityRenderers();
 
@@ -65,6 +68,9 @@
   static std::optional<base::ByteCount> GetPrivateFootprint(
       const base::Process& process);
 
+  std::optional<content::UserLevelMemoryPressureMetrics>
+  GetLatestMemoryMetricsImpl();
+
   base::ByteCount memory_threshold_;
   base::TimeDelta measure_interval_;
   base::TimeDelta minimum_interval_;
@@ -74,6 +80,8 @@
   base::MemoryPressureLevel current_level_ = base::MEMORY_PRESSURE_LEVEL_NONE;
 
   std::optional<UserLevelMemoryPressureMetrics> latest_metrics_;
+
+  SEQUENCE_CHECKER(sequence_checker_);
 };
 
 }  // namespace content
diff --git a/content/browser/preloading/prerender/prerender_browsertest.cc b/content/browser/preloading/prerender/prerender_browsertest.cc
index 79e4a1c..930becb 100644
--- a/content/browser/preloading/prerender/prerender_browsertest.cc
+++ b/content/browser/preloading/prerender/prerender_browsertest.cc
@@ -1114,6 +1114,8 @@
 //    tab is not scheduled. So, the prerender is never unblocked. Skip the test
 //    as it is not testable.
 // - `PrerenderInBackground_*`: Ditto.
+// - `PreloadingTriggeringOutcomeForStartingPrerenderBeforeDestruction`: See the
+// test.
 class PrerenderBrowserTestFallbackDisabled : public PrerenderBrowserTest {
  public:
   PrerenderBrowserTestFallbackDisabled()
@@ -9045,8 +9047,13 @@
 // Test that when the running prerender is destroyed due to the activation of
 // another already prerendered page, other pending prerender's outcome is
 // recorded as `kTriggeredButPending`.
+//
+// Rationale for `PrerenderBrowserTestFallbackDisabled`: This test triggers
+// multiple prerenders. It's hard to control both prefetch/prerender states, as
+// they progress asynchronously. The aspect to be tested is unrelated to
+// prefetch ahead of prerender.
 IN_PROC_BROWSER_TEST_F(
-    PrerenderBrowserTest,
+    PrerenderBrowserTestFallbackDisabled,
     PreloadingTriggeringOutcomeForStartingPrerenderBeforeDestruction) {
   net::test_server::ControllableHttpResponse response2(
       embedded_test_server(), "/empty.html?prerender2");
diff --git a/content/browser/renderer_host/navigation_controller_impl.cc b/content/browser/renderer_host/navigation_controller_impl.cc
index db22ac77..2d62185 100644
--- a/content/browser/renderer_host/navigation_controller_impl.cc
+++ b/content/browser/renderer_host/navigation_controller_impl.cc
@@ -644,8 +644,6 @@
 
 void NavigationControllerImpl::ScopedDeferredNavigationStateChangeNotifier::
     RequestDeferredNotification() {
-  CHECK(base::FeatureList::IsEnabled(
-      features::kSkipRedundantNavigationStateNotification));
   requested_ = true;
 }
 
@@ -4691,9 +4689,7 @@
   // That function passes in a pointer for `deferred_notifier`, which tells this
   // function to not send the notification immediately. The notification will be
   // sent when RendererDidNavigate returns.
-  if (deferred_notifier &&
-      base::FeatureList::IsEnabled(
-          features::kSkipRedundantNavigationStateNotification)) {
+  if (deferred_notifier) {
     deferred_notifier->RequestDeferredNotification();
   } else {
     delegate_->NotifyNavigationStateChangedFromController(INVALIDATE_TYPE_ALL);
@@ -4869,9 +4865,7 @@
   // That function passes in a pointer for `deferred_notifier`, which tells this
   // function to not send the notification immediately. The notification will be
   // sent when RendererDidNavigate returns.
-  if (deferred_notifier &&
-      base::FeatureList::IsEnabled(
-          features::kSkipRedundantNavigationStateNotification)) {
+  if (deferred_notifier) {
     deferred_notifier->RequestDeferredNotification();
     return;
   }
diff --git a/content/browser/renderer_host/navigation_controller_impl_browsertest.cc b/content/browser/renderer_host/navigation_controller_impl_browsertest.cc
index b12ee4f2..d2dd58e 100644
--- a/content/browser/renderer_host/navigation_controller_impl_browsertest.cc
+++ b/content/browser/renderer_host/navigation_controller_impl_browsertest.cc
@@ -21202,16 +21202,10 @@
     // committing the new NavigationEntry which keeps the "initial" status:
     // #1 was triggered by DiscardNonCommittedEntries().
     // #2 is triggered by NotifyNavigationEntryCommitted().
+    // Only one notification will actually be sent out.
     // Note that this is different from the _Ignore test below, which wouldn't
     // fire the events because the client chooses to ignore the updates.
-    // With "SkipRedundantNavigationStateNotification" enabled, only 1 call will
-    // take place.
-    if (base::FeatureList::IsEnabled(
-            features::kSkipRedundantNavigationStateNotification)) {
-      EXPECT_EQ(1, all_navigation_state_changed_delegate.call_count());
-    } else {
-      EXPECT_EQ(2, all_navigation_state_changed_delegate.call_count());
-    }
+    EXPECT_EQ(1, all_navigation_state_changed_delegate.call_count());
   }
 
   {
@@ -21229,20 +21223,12 @@
     EXPECT_EQ(1, controller.GetEntryCount());
     EXPECT_FALSE(controller.GetLastCommittedEntry()->IsInitialEntry());
 
-    // 1 or 2 additional INVALIDATE_TYPE_ALL NavigationStateChanged calls were
-    // triggered (increasing the count to either 2 or 4 depending on whether
-    // "SkipRedundantNavigationStateNotification" is enabled), and they're not
-    // for the initial NavigationEntry.
+    // 2 additional INVALIDATE_TYPE_ALL NavigationStateChanged calls were
+    // triggered (though only 1 notification will be sent, increasing the count
+    // to 2), and they're not for the initial NavigationEntry.
     // #1 was triggered by DiscardNonCommittedEntries().
     // #2 is triggered by NotifyNavigationEntryCommitted().
-    // With "SkipRedundantNavigationStateNotification" enabled, only 1 call will
-    // take place.
-    if (base::FeatureList::IsEnabled(
-            features::kSkipRedundantNavigationStateNotification)) {
-      EXPECT_EQ(2, all_navigation_state_changed_delegate.call_count());
-    } else {
-      EXPECT_EQ(4, all_navigation_state_changed_delegate.call_count());
-    }
+    EXPECT_EQ(2, all_navigation_state_changed_delegate.call_count());
   }
 }
 
diff --git a/content/browser/renderer_host/navigation_controller_impl_unittest.cc b/content/browser/renderer_host/navigation_controller_impl_unittest.cc
index 497c23b1..c337752 100644
--- a/content/browser/renderer_host/navigation_controller_impl_unittest.cc
+++ b/content/browser/renderer_host/navigation_controller_impl_unittest.cc
@@ -1294,14 +1294,9 @@
   EXPECT_FALSE(controller.GetPendingEntry());
   // The pending entry deletion and commit of the new NavigationEntry both
   // count as "navigation state change", though only one notification will be
-  // sent if kSkipRedundantNavigationStateNotification is enabled.
+  // sent.
   EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
-  if (base::FeatureList::IsEnabled(
-          features::kSkipRedundantNavigationStateNotification)) {
-    EXPECT_EQ(2, delegate->navigation_state_change_count());
-  } else {
-    EXPECT_EQ(3, delegate->navigation_state_change_count());
-  }
+  EXPECT_EQ(2, delegate->navigation_state_change_count());
 
   contents()->SetDelegate(nullptr);
 }
diff --git a/content/browser/renderer_host/render_view_host_impl.cc b/content/browser/renderer_host/render_view_host_impl.cc
index b819357..4e63b70e 100644
--- a/content/browser/renderer_host/render_view_host_impl.cc
+++ b/content/browser/renderer_host/render_view_host_impl.cc
@@ -611,11 +611,9 @@
   params->blink_page_broadcast =
       page_broadcast_.BindNewEndpointAndPassReceiver();
 
-  if (base::FeatureList::IsEnabled(features::kSetHistoryInfoOnViewCreation)) {
-    params->history_index =
-        frame_tree()->controller().GetLastCommittedEntryIndex();
-    params->history_length = frame_tree()->controller().GetEntryCount();
-  }
+  params->history_index =
+      frame_tree()->controller().GetLastCommittedEntryIndex();
+  params->history_length = frame_tree()->controller().GetEntryCount();
 
   // The renderer process's `blink::WebView` is owned by this lifecycle of
   // the `page_broadcast_` channel.
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index a4f4036..5106dc26 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -10651,15 +10651,6 @@
     ReattachOuterDelegateIfNeeded();
   }
 
-  // With SetHistoryInfoOnViewCreation enabled, the history and index length are
-  // sent as part of the the CreateView() IPC via the CreateViewParams.
-  if (!base::FeatureList::IsEnabled(features::kSetHistoryInfoOnViewCreation)) {
-    SetHistoryIndexAndLengthForView(
-        render_view_host,
-        rvh_impl->frame_tree()->controller().GetLastCommittedEntryIndex(),
-        rvh_impl->frame_tree()->controller().GetEntryCount());
-  }
-
 #if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_ANDROID)
   // Force a ViewMsg_Resize to be sent, needed to make plugins show up on
   // linux. See crbug.com/83941.
diff --git a/content/browser/web_contents/web_contents_impl_browsertest.cc b/content/browser/web_contents/web_contents_impl_browsertest.cc
index bd9968f..8ca9c70 100644
--- a/content/browser/web_contents/web_contents_impl_browsertest.cc
+++ b/content/browser/web_contents/web_contents_impl_browsertest.cc
@@ -3511,13 +3511,6 @@
 
   // Set up all the expected title change in the original WebContents.
   std::queue<std::u16string> original_expected_title_changes;
-  if (!base::FeatureList::IsEnabled(
-          features::kSkipRedundantNavigationStateNotification)) {
-    // The first "title change" is not an actual title change, it's triggered by
-    // a INVALIDATE_TYPE_ALL NotifyNavigationStateChanged call from
-    // NavigationControllerImpl::DiscardNonCommittedEntries().
-    original_expected_title_changes.push(u"");
-  }
   // When the navigation to `main_url` commits, the document title is not set
   // yet, so we use the URL as the title.
   original_expected_title_changes.push(main_url_as_title);
@@ -3565,18 +3558,6 @@
 
   // Set up all the expected title change in the new WebContents.
   std::queue<std::u16string> new_expected_title_changes;
-  if (!base::FeatureList::IsEnabled(
-          features::kSkipRedundantNavigationStateNotification)) {
-    // Similar to the original WebContents' case above, the first "title change"
-    // is not an actual title change, but instead triggered by a
-    // INVALIDATE_TYPE_ALL NotifyNavigationStateChanged call from
-    // NavigationControllerImpl::DiscardNonCommittedEntries(). For the original
-    // WebContents' case we expect an empty title because there's no entries and
-    // GetNavigationEntryForTitle() returns null. However, in the new
-    // WebContents we already have the restored entry, so we will use the
-    // entry's title.
-    new_expected_title_changes.push(main_title);
-  }
   // When the navigation to `main_url` commits, we also got another "update"
   // that is not really a title change, but it is triggered by a
   // INVALIDATE_TYPE_ALL NotifyNavigationStateChanged call from
diff --git a/content/common/features.cc b/content/common/features.cc
index a2e42f52..98d558b 100644
--- a/content/common/features.cc
+++ b/content/common/features.cc
@@ -674,11 +674,6 @@
 BASE_FEATURE(kSkipEarlyCommitPendingForCrashedFrame,
              base::FEATURE_DISABLED_BY_DEFAULT);
 
-// Feature to skip a redundant NotifyNavigationStateChanged call during
-// RendererDidNavigate.
-BASE_FEATURE(kSkipRedundantNavigationStateNotification,
-             base::FEATURE_ENABLED_BY_DEFAULT);
-
 // When enabled, skips registration of RendererCancellationThrottle and instead
 // keeps navigation cancellation behavior by reusing the requester
 // NavigationClient.
diff --git a/content/common/features.h b/content/common/features.h
index 2a2f9ebf..767d3b6 100644
--- a/content/common/features.h
+++ b/content/common/features.h
@@ -212,7 +212,6 @@
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kServiceWorkerClientUrlIsCreationUrl);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kServiceWorkerWindowClientInitiator);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kSkipEarlyCommitPendingForCrashedFrame);
-CONTENT_EXPORT BASE_DECLARE_FEATURE(kSkipRedundantNavigationStateNotification);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kSkipRendererCancellationThrottle);
 #if BUILDFLAG(IS_ANDROID)
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kStrictHighRankProcessLRU);
diff --git a/content/public/android/javatests/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityE2ETest.java b/content/public/android/javatests/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityE2ETest.java
index 6285029..1bf58f8 100644
--- a/content/public/android/javatests/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityE2ETest.java
+++ b/content/public/android/javatests/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityE2ETest.java
@@ -13,6 +13,7 @@
 import android.os.IBinder;
 import android.view.accessibility.AccessibilityEvent;
 
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
@@ -27,6 +28,7 @@
 import org.chromium.base.test.util.Batch;
 import org.chromium.base.test.util.MinAndroidSdkLevel;
 import org.chromium.base.test.util.UrlUtils;
+import org.chromium.ui.accessibility.testservice.EventQueryParams;
 import org.chromium.ui.accessibility.testservice.IAccessibilityTestHelperService;
 
 import java.io.IOException;
@@ -166,10 +168,9 @@
         boolean wscReceived =
                 getAccessibilityHelperService()
                         .waitForEvent(
-                                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
-                                "",
-                                "",
-                                EVENT_TIMEOUT_MS);
+                                new EventQueryParamsBuilder()
+                                        .setEventType(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
+                                        .build());
         Assert.assertTrue("Service did not receive WINDOW_STATE_CHANGED", wscReceived);
     }
 
@@ -186,20 +187,91 @@
         boolean wscReceived =
                 getAccessibilityHelperService()
                         .waitForEvent(
-                                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
-                                "",
-                                "",
-                                EVENT_TIMEOUT_MS);
+                                new EventQueryParamsBuilder()
+                                        .setEventType(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
+                                        .build());
         Assert.assertTrue("Service did not receive WINDOW_STATE_CHANGED", wscReceived);
 
         // Ask the service to wait for a text selection changed on the omnibox.
         boolean tscReceived =
                 getAccessibilityHelperService()
                         .waitForEvent(
-                                AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED,
-                                "android.widget.EditText",
-                                url,
-                                EVENT_TIMEOUT_MS);
+                                new EventQueryParamsBuilder()
+                                        .setEventType(
+                                                AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED)
+                                        .setClassName("android.widget.EditText")
+                                        .setText(url)
+                                        .build());
         Assert.assertTrue("Service did not receive TEXT_SELECTION_CHANGED", tscReceived);
     }
+
+    @Test
+    @SmallTest
+    @MinAndroidSdkLevel(Build.VERSION_CODES.BAKLAVA)
+    public void testAccessibilityServiceReceivesAccessibilityFocusEvent() throws Throwable {
+        // Load a page with a focusable element.
+        mActivityTestRule.launchContentShellWithUrl(
+                UrlUtils.encodeHtmlDataUri("<button>Click Me</button>"));
+
+        // Wait for the page to settle.
+        getAccessibilityHelperService()
+                .waitForEvent(
+                        new EventQueryParamsBuilder()
+                                .setEventType(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED)
+                                .build());
+
+        // Find the button and perform a focus action.
+        boolean actionRes =
+                getAccessibilityHelperService()
+                        .performActionOnNode(
+                                "android.widget.Button",
+                                "Click Me",
+                                AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
+        Assert.assertTrue("Failed to perform accessibility focus action", actionRes);
+
+        // Ask the service to wait for the event.
+        boolean eventReceived =
+                getAccessibilityHelperService()
+                        .waitForEvent(
+                                new EventQueryParamsBuilder()
+                                        .setEventType(
+                                                AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED)
+                                        .setClassName("android.widget.Button")
+                                        .setText("Click Me")
+                                        .build());
+        Assert.assertTrue("Service did not receive accessibility focus event", eventReceived);
+    }
+
+    private static class EventQueryParamsBuilder {
+        private static final long DEFAULT_TIMEOUT_MS = 5000;
+
+        private int mEventType;
+        private String mClassName = "";
+        private String mText = "";
+        private final long mTimeoutMs = DEFAULT_TIMEOUT_MS;
+
+        public EventQueryParamsBuilder setEventType(int eventType) {
+            mEventType = eventType;
+            return this;
+        }
+
+        public EventQueryParamsBuilder setClassName(String className) {
+            mClassName = className;
+            return this;
+        }
+
+        public EventQueryParamsBuilder setText(String text) {
+            mText = text;
+            return this;
+        }
+
+        public EventQueryParams build() {
+            EventQueryParams params = new EventQueryParams();
+            params.eventType = mEventType;
+            params.className = mClassName;
+            params.text = mText;
+            params.timeoutMs = mTimeoutMs;
+            return params;
+        }
+    }
 }
diff --git a/content/public/browser/clipboard_types.cc b/content/public/browser/clipboard_types.cc
index 88498bb..950eb8f2 100644
--- a/content/public/browser/clipboard_types.cc
+++ b/content/public/browser/clipboard_types.cc
@@ -123,46 +123,50 @@
   auto rfh_token = GlobalRenderFrameHostToken::FromPickle(
       base::Pickle::WithData(base::as_byte_span(result)));
 
-  RenderFrameHost* rfh = nullptr;
-  if (rfh_token) {
-    rfh = RenderFrameHost::FromFrameToken(*rfh_token);
-  }
-
-  auto* clipboard = ui::Clipboard::GetForCurrentThread();
-  if (!rfh) {
-    // Fall back to the clipboard source if there is no `seqno` match or RFH, as
-    // `ui::DataTransferEndpoint` can be populated differently based on
-    // platform.
-    std::move(callback).Run(
-        ClipboardEndpoint(clipboard->GetSource(clipboard_buffer)));
-    return;
-  }
-
-  std::optional<ui::DataTransferEndpoint> source_dte;
-  auto clipboard_source_dte = clipboard->GetSource(clipboard_buffer);
-  if (clipboard_source_dte) {
-    if (clipboard_source_dte->IsUrlType()) {
-      source_dte = std::make_optional<ui::DataTransferEndpoint>(
-          *clipboard_source_dte->GetURL(),
-          ui::DataTransferEndpointOptions{
-              .off_the_record = rfh->GetBrowserContext()->IsOffTheRecord()});
-    } else {
-      source_dte = std::move(clipboard_source_dte);
-    }
-  }
-
-  std::move(callback).Run(ClipboardEndpoint(
-      std::move(source_dte),
-      base::BindRepeating(
-          [](GlobalRenderFrameHostToken rfh_token) -> BrowserContext* {
-            auto* rfh = RenderFrameHost::FromFrameToken(rfh_token);
-            if (!rfh) {
-              return nullptr;
+  ui::Clipboard::GetForCurrentThread()->GetSource(
+      clipboard_buffer,
+      base::BindOnce(
+          [](std::optional<GlobalRenderFrameHostToken> rfh_token,
+             base::OnceCallback<void(ClipboardEndpoint)> callback,
+             std::optional<ui::DataTransferEndpoint> clipboard_source_dte) {
+            RenderFrameHost* rfh = nullptr;
+            if (rfh_token) {
+              rfh = RenderFrameHost::FromFrameToken(*rfh_token);
             }
-            return rfh->GetBrowserContext();
+
+            if (!rfh) {
+              std::move(callback).Run(ClipboardEndpoint(clipboard_source_dte));
+              return;
+            }
+
+            std::optional<ui::DataTransferEndpoint> source_dte;
+            if (clipboard_source_dte) {
+              if (clipboard_source_dte->IsUrlType()) {
+                source_dte = std::make_optional<ui::DataTransferEndpoint>(
+                    *clipboard_source_dte->GetURL(),
+                    ui::DataTransferEndpointOptions{
+                        .off_the_record =
+                            rfh->GetBrowserContext()->IsOffTheRecord()});
+              } else {
+                source_dte = std::move(clipboard_source_dte);
+              }
+            }
+
+            std::move(callback).Run(ClipboardEndpoint(
+                std::move(source_dte),
+                base::BindRepeating(
+                    [](GlobalRenderFrameHostToken rfh_token)
+                        -> BrowserContext* {
+                      auto* rfh = RenderFrameHost::FromFrameToken(rfh_token);
+                      if (!rfh) {
+                        return nullptr;
+                      }
+                      return rfh->GetBrowserContext();
+                    },
+                    rfh->GetGlobalFrameToken()),
+                *rfh));
           },
-          rfh->GetGlobalFrameToken()),
-      *rfh));
+          rfh_token, std::move(callback)));
 }
 
 void GetSourceClipboardEndpoint(
diff --git a/content/public/common/content_features.cc b/content/public/common/content_features.cc
index b80a811..3efd3bd 100644
--- a/content/public/common/content_features.cc
+++ b/content/public/common/content_features.cc
@@ -768,11 +768,6 @@
 BASE_FEATURE(kRestrictThreadPoolInBackground,
              base::FEATURE_DISABLED_BY_DEFAULT);
 
-// Feature that stops broadcasting the history index and length when
-// CreateRenderViewForRenderManager() is invoked, and instead passes the
-// information in the CreateViewParams, saving some IPC calls.
-BASE_FEATURE(kSetHistoryInfoOnViewCreation, base::FEATURE_ENABLED_BY_DEFAULT);
-
 // When enabled, sends the spare renderer information when setting the
 // priority of renderers. Currently only Android handles the spare renderer
 // information in priority.
diff --git a/content/public/common/content_features.h b/content/public/common/content_features.h
index f253fa5..aaefff1 100644
--- a/content/public/common/content_features.h
+++ b/content/public/common/content_features.h
@@ -234,7 +234,6 @@
                                           kRendererProcessLimitOnAndroidCount);
 #endif
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kRestrictThreadPoolInBackground);
-CONTENT_EXPORT BASE_DECLARE_FEATURE(kSetHistoryInfoOnViewCreation);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kSpareRendererProcessPriority);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kRetryGetVideoCaptureDeviceInfos);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kSkipIPCChannelPausingForNonGuests);
diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index 9a29777..2cf52eb 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -307,10 +307,6 @@
 
 namespace {
 
-// Feature to combine the UpdateState IPC that's sent during commit time with
-// the DidCommit* IPCs. See: http://crbug.com/424829233
-BASE_FEATURE(kReducePageStateIpcs, base::FEATURE_ENABLED_BY_DEFAULT);
-
 const int kExtraCharsBeforeAndAfterSelection = 100;
 const size_t kMaxURLLogChars = 1024;
 const char kCommitRenderFrame[] = "Navigation.CommitRenderFrame";
@@ -5184,12 +5180,6 @@
     blink::WebHistoryCommitType commit_type,
     ui::PageTransition transition,
     NavigationState* navigation_state) {
-  if (!base::FeatureList::IsEnabled(kReducePageStateIpcs)) {
-    // We need to update the last committed session history entry with state for
-    // the previous page. Do this before updating the current history item.
-    SendUpdateState();
-  }
-
   UpdateNavigationHistory(commit_type, navigation_state);
 
   if (!frame_->Parent()) {  // Only for top frames.
@@ -5236,8 +5226,7 @@
   // UpdateStateForCommit() since that call will update the current history
   // item.
   std::optional<blink::PageState> previous_page_state = std::nullopt;
-  if (base::FeatureList::IsEnabled(kReducePageStateIpcs) &&
-      !GetWebFrame()->GetCurrentHistoryItem().IsNull()) {
+  if (!GetWebFrame()->GetCurrentHistoryItem().IsNull()) {
     previous_page_state = GetWebFrame()->CurrentHistoryItemToPageState();
   }
   UpdateStateForCommit(commit_type, transition, navigation_state);
diff --git a/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt
index b57ff777..c6723ca 100644
--- a/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt
@@ -374,7 +374,9 @@
 [ android android-sm-a137f ] Pixel_SVGHuge [ Slow ]
 [ android android-sm-a236b ] Pixel_SVGHuge [ Slow ]
 # Also applies to Windows arm64.
-[ win qualcomm-0x41333430 ] Pixel_SVGHuge [ Slow ]
+[ win qualcomm-0x41333430 target-cpu-64 ] Pixel_SVGHuge [ Slow ]
+# Also applies to 32-bit Windows
+[ win release target-cpu-32 ] Pixel_SVGHuge [ Slow ]
 # Doesn't hit heartbeat timeouts, but can time out when trying to execute
 # JavaScript after the SVG is loaded.
 [ mac intel release no-asan graphite-disabled ] Pixel_SVGHuge [ Slow ]
diff --git a/device/gamepad/gamepad_pad_state_provider.cc b/device/gamepad/gamepad_pad_state_provider.cc
index 3d66ae4..e636b971 100644
--- a/device/gamepad/gamepad_pad_state_provider.cc
+++ b/device/gamepad/gamepad_pad_state_provider.cc
@@ -85,7 +85,7 @@
 }
 
 void GamepadPadStateProvider::ClearPadState(PadState& state) {
-  UNSAFE_TODO(memset(&state, 0, sizeof(PadState)));
+  state = PadState();
 }
 
 void GamepadPadStateProvider::InitializeDataFetcher(
@@ -100,7 +100,7 @@
   DCHECK(pad);
 
   if (!pad_state->data.connected) {
-    UNSAFE_TODO(memset(pad, 0, sizeof(Gamepad)));
+    *pad = Gamepad();
     return;
   }
 
diff --git a/device/gamepad/gamepad_pad_state_provider.h b/device/gamepad/gamepad_pad_state_provider.h
index 597c0746..b6044f5a 100644
--- a/device/gamepad/gamepad_pad_state_provider.h
+++ b/device/gamepad/gamepad_pad_state_provider.h
@@ -49,37 +49,37 @@
   ~PadState();
 
   // Index of the slot occupied by this gamepad.
-  int pad_index;
+  int pad_index = 0;
 
   // Which data fetcher provided this gamepad's data.
-  GamepadSource source;
+  GamepadSource source = GamepadSource::kNone;
   // Data fetcher-specific identifier for this gamepad.
-  int source_id;
+  int source_id = 0;
 
   // Indicates whether this gamepad is actively receiving input. |is_active| is
   // initialized to false on each polling cycle and must is set to true when new
   // data is received.
-  bool is_active;
+  bool is_active = false;
 
   // True if the gamepad is newly connected but notifications have not yet been
   // sent.
-  bool is_newly_active;
+  bool is_newly_active = false;
 
   // Set by the data fetcher to indicate that one-time initialization for this
   // gamepad has been completed.
-  bool is_initialized;
+  bool is_initialized = false;
 
   // Set by the data fetcher to indicate whether this gamepad's ids are
   // recognized as a specific gamepad. It is then used to prioritize recognized
   // gamepads when finding an empty slot for any new gamepads when activated.
-  bool is_recognized;
+  bool is_recognized = false;
 
   // Gamepad data, unmapped.
   Gamepad data;
 
   // Functions to map from device data to standard layout, if available. May
   // be null if no mapping is available or needed.
-  GamepadStandardMappingFunction mapper;
+  GamepadStandardMappingFunction mapper = nullptr;
 
   // Sanitization masks
   // axis_mask and button_mask are bitfields that represent the reset state of
@@ -90,13 +90,13 @@
   static_assert(Gamepad::kAxesLengthCap <=
                     std::numeric_limits<uint32_t>::digits,
                 "axis_mask is not large enough");
-  uint32_t axis_mask;
+  uint32_t axis_mask = 0;
 
   // If we ever increase the max button count this will need to be updated.
   static_assert(Gamepad::kButtonsLengthCap <=
                     std::numeric_limits<uint32_t>::digits,
                 "button_mask is not large enough");
-  uint32_t button_mask;
+  uint32_t button_mask = 0;
 };
 
 class DEVICE_GAMEPAD_EXPORT GamepadPadStateProvider {
diff --git a/docs/contributing.md b/docs/contributing.md
index e6454ed..b63c4d73 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -455,16 +455,18 @@
 
 Occasionally changes that pass the [commit queue][commit-queue] and get
 submitted into Chromium will later be reverted. If this happens to your change,
-don't be discouraged! This can be a common part of the Chromium development
+**don't be discouraged**! This can be a common part of the Chromium development
 cycle and happens for a variety of reasons, including a conflict with an
 unanticipated change or tests not covered on the commit queue.
 
 If this happens to your change, you're encouraged to pursue a reland. When doing
 so, following these basic steps can streamline the re-review process:
-- **Create the reland**: Click the `CREATE RELAND` button on the original change
+- **Create the reland**: Click the `Create Reland` button on the original change
   in Gerrit. This will create a new change whose diff is identical to the
   original, but has a small paper-trail in the commit message that leads back to
-  the original. This can be useful for sheriffs when debugging regressions.
+  the original. This can be useful for gardeners when debugging regressions. If
+  you encounter an error with that button, try the `Revert` button on the revert
+  CL instead; functionally they should be identical.
 - **Append the fix**: If the reland requires file modifications not present in
   the original change, simply upload these fixes in a subsequent patchset to the
   reland change. By comparing the first patchset with the latest, this gives
diff --git a/docs/windows_build_instructions.md b/docs/windows_build_instructions.md
index ea43731..1d4fbdce 100644
--- a/docs/windows_build_instructions.md
+++ b/docs/windows_build_instructions.md
@@ -23,7 +23,7 @@
 
 ### Visual Studio
 
-Chromium requires [Visual Studio 2022](https://learn.microsoft.com/en-us/visualstudio/releases/2022/release-notes)
+Chromium requires [Visual Studio 2026](https://learn.microsoft.com/en-us/visualstudio/releases/2026/release-notes)
 (>=17.0.0) to build. Visual Studio can also be used to debug Chromium.
 The clang-cl compiler is used but Visual Studio's header files, libraries, and
 some tools are required. Visual Studio Community Edition should work if its
@@ -52,7 +52,7 @@
 Required
 
 * [Windows 11 SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/)
-version 10.0.26100.4654. This can be installed separately or by checking the
+version 10.0.26100.7705. This can be installed separately or by checking the
 appropriate box in the Visual Studio Installer.
 * (Windows 11) SDK Debugging Tools 10.0.26100.3323 or higher. This version of the
 Debugging tools is needed in order to support reading the large-page PDBs that
@@ -138,9 +138,9 @@
 Visual Studio (by default, depot_tools will try to use a google-internal
 version).
 
-You may also have to set variable `vs2022_install` to your installation path of
-Visual Studio 2022, like
-`set vs2022_install=C:\Program Files\Microsoft Visual Studio\2022\Professional`.
+You may also have to set variable `vs2026_install` to your installation path of
+Visual Studio 2026, like
+`set vs2026_install=C:\Program Files\Microsoft Visual Studio\2026\Professional`.
 
 From a cmd.exe shell, run:
 ```shell
@@ -555,7 +555,7 @@
 
 #### Configure git to use fsmonitor
 
-You can significantly speed up git by using [fsmonitor.](https://github.blog/2022-06-29-improve-git-monorepo-performance-with-a-file-system-monitor/)
+You can significantly speed up git by using [fsmonitor.](https://github.blog/2026-06-29-improve-git-monorepo-performance-with-a-file-system-monitor/)
 You should enable fsmonitor in large repos, such as Chromium and v8. Enabling
 it globally will launch many processes and consume excess commit/memory and
 probably isn't worthwhile. The command to enable fsmonitor in the current repo
diff --git a/extensions/common/api/declarative_net_request.webidl b/extensions/common/api/declarative_net_request.webidl
index b838db6c..6028c8d 100644
--- a/extensions/common/api/declarative_net_request.webidl
+++ b/extensions/common/api/declarative_net_request.webidl
@@ -440,7 +440,7 @@
   // relevant if a regex substitution is present and would thus need to be
   // applied onto all matching groups. Equivalent to the "g" flag.
   // Defaults to false.
-  [nodoc] boolean matchAll;
+  boolean matchAll;
 };
 
 dictionary ModifyHeaderInfo {
@@ -635,11 +635,11 @@
 };
 
 [nodoc] dictionary DNRInfo {
-  [nodoc] required sequence<Ruleset> rule_resources;
+  required sequence<Ruleset> rule_resources;
 };
 
 [nodoc] partial dictionary ExtensionManifest {
-  [nodoc] DNRInfo declarative_net_request;
+  DNRInfo declarative_net_request;
 };
 
 dictionary RegexOptions {
diff --git a/extensions/common/manifest_handlers/mime_types_handler_unittest.cc b/extensions/common/manifest_handlers/mime_types_handler_unittest.cc
index 5fbff27..7fc44242 100644
--- a/extensions/common/manifest_handlers/mime_types_handler_unittest.cc
+++ b/extensions/common/manifest_handlers/mime_types_handler_unittest.cc
@@ -65,6 +65,10 @@
 
   EXPECT_THAT(handler->mime_type_set(),
               ElementsAre("application/octet-stream", "text/plain"));
+
+  EXPECT_FALSE(handler->CanHandleMIMEType("text/html"));
+  EXPECT_TRUE(handler->CanHandleMIMEType("text/plain"));
+  EXPECT_TRUE(handler->CanHandleMIMEType("application/octet-stream"));
 }
 
 }  // namespace extensions
diff --git a/extensions/renderer/dispatcher.cc b/extensions/renderer/dispatcher.cc
index a8132146..79f64918 100644
--- a/extensions/renderer/dispatcher.cc
+++ b/extensions/renderer/dispatcher.cc
@@ -535,7 +535,6 @@
     const GURL& service_worker_scope,
     const GURL& script_url,
     const blink::ServiceWorkerToken& service_worker_token) {
-  const base::TimeTicks start_time = base::TimeTicks::Now();
   service_worker_context_state = ServiceWorkerContextState::kInitializing;
 
   // TODO(crbug.com/40626913): We may want to give service workers not
@@ -673,9 +672,6 @@
   WorkerThreadDispatcher::GetServiceWorkerData()->Init();
   g_worker_script_context_set.Get().Insert(base::WrapUnique(context));
 
-  const base::TimeDelta elapsed = base::TimeTicks::Now() - start_time;
-  UMA_HISTOGRAM_TIMES(
-      "Extensions.DidInitializeServiceWorkerContextOnWorkerThread2", elapsed);
   service_worker_context_state = ServiceWorkerContextState::kInitialized;
 }
 
diff --git a/gpu/command_buffer/service/disk_cache_proto.proto b/gpu/command_buffer/service/disk_cache_proto.proto
index 784bf4e..9175007 100644
--- a/gpu/command_buffer/service/disk_cache_proto.proto
+++ b/gpu/command_buffer/service/disk_cache_proto.proto
@@ -43,9 +43,8 @@
   optional string instance_name = 3;
   optional uint32 array_size = 4;
   optional int32 layout = 5;
-  optional bool is_row_major_layout = 6;
-  optional bool static_use = 7;
-  repeated ShaderInterfaceBlockFieldProto fields = 8;
+  optional bool static_use = 6;
+  repeated ShaderInterfaceBlockFieldProto fields = 7;
 }
 
 message ShaderProto {
diff --git a/gpu/command_buffer/service/memory_program_cache.cc b/gpu/command_buffer/service/memory_program_cache.cc
index 5b3eceef..038d9fa 100644
--- a/gpu/command_buffer/service/memory_program_cache.cc
+++ b/gpu/command_buffer/service/memory_program_cache.cc
@@ -95,7 +95,6 @@
   proto->set_instance_name(interfaceBlock.instanceName);
   proto->set_array_size(interfaceBlock.arraySize);
   proto->set_layout(interfaceBlock.layout);
-  proto->set_is_row_major_layout(interfaceBlock.isRowMajorLayout);
   proto->set_static_use(interfaceBlock.staticUse);
   for (size_t ii = 0; ii < interfaceBlock.fields.size(); ++ii) {
     ShaderInterfaceBlockFieldProto* field = proto->add_fields();
@@ -191,7 +190,6 @@
   interface_block.instanceName = proto.instance_name();
   interface_block.arraySize = proto.array_size();
   interface_block.layout = static_cast<sh::BlockLayoutType>(proto.layout());
-  interface_block.isRowMajorLayout = proto.is_row_major_layout();
   interface_block.staticUse = proto.static_use();
   interface_block.fields.resize(proto.fields_size());
   for (int ii = 0; ii < proto.fields_size(); ++ii) {
diff --git a/gpu/command_buffer/service/shader_manager_unittest.cc b/gpu/command_buffer/service/shader_manager_unittest.cc
index 4a6067b..45a892c5 100644
--- a/gpu/command_buffer/service/shader_manager_unittest.cc
+++ b/gpu/command_buffer/service/shader_manager_unittest.cc
@@ -132,7 +132,6 @@
 
   const GLint kInterfaceBlock1Size = 1;
   const sh::BlockLayoutType kInterfaceBlock1Layout = sh::BLOCKLAYOUT_STANDARD;
-  const bool kInterfaceBlock1RowMajor = false;
   const bool kInterfaceBlock1StaticUse = false;
   const char* kInterfaceBlock1Name = "block1";
   const char* kInterfaceBlock1InstanceName = "block1instance";
@@ -216,9 +215,8 @@
   interface_block_map[kInterfaceBlock1Name] =
       TestHelper::ConstructInterfaceBlock(
           kInterfaceBlock1Size, kInterfaceBlock1Layout,
-          kInterfaceBlock1RowMajor, kInterfaceBlock1StaticUse,
-          kInterfaceBlock1Name, kInterfaceBlock1InstanceName,
-          interface_block1_fields);
+          kInterfaceBlock1StaticUse, kInterfaceBlock1Name,
+          kInterfaceBlock1InstanceName, interface_block1_fields);
 
   TestHelper::SetShaderStates(gl_.get(), shader1, true, &kLog,
                               &kTranslatedSource, nullptr, &attrib_map,
@@ -284,7 +282,6 @@
     ASSERT_TRUE(block_info != nullptr);
     EXPECT_EQ(it.second.arraySize, block_info->arraySize);
     EXPECT_EQ(it.second.layout, block_info->layout);
-    EXPECT_EQ(it.second.isRowMajorLayout, block_info->isRowMajorLayout);
     EXPECT_EQ(it.second.staticUse, block_info->staticUse);
     EXPECT_STREQ(it.second.name.c_str(), block_info->name.c_str());
     EXPECT_STREQ(it.second.name.c_str(),
diff --git a/gpu/command_buffer/service/shared_image/shared_image_factory.cc b/gpu/command_buffer/service/shared_image/shared_image_factory.cc
index ec37b09d..217472ba 100644
--- a/gpu/command_buffer/service/shared_image/shared_image_factory.cc
+++ b/gpu/command_buffer/service/shared_image/shared_image_factory.cc
@@ -676,13 +676,7 @@
 bool SharedImageFactory::UpdateSharedImage(
     const Mailbox& mailbox,
     std::unique_ptr<gfx::GpuFence> in_fence) {
-  auto* shared_image = GetFactoryRef(mailbox);
-  if (!shared_image) {
-    LOG(ERROR) << "UpdateSharedImage: Could not find shared image mailbox";
-    return false;
-  }
-  shared_image->Update(std::move(in_fence));
-  return true;
+  return shared_image_manager_->UpdateSharedImage(mailbox, std::move(in_fence));
 }
 
 bool SharedImageFactory::DestroySharedImage(const Mailbox& mailbox) {
@@ -695,16 +689,10 @@
   return true;
 }
 
-bool SharedImageFactory::SetSharedImagePurgeable(const Mailbox& mailbox,
+void SharedImageFactory::SetSharedImagePurgeable(const Mailbox& mailbox,
                                                  bool purgeable) {
-  auto* shared_image = GetFactoryRef(mailbox);
-  if (!shared_image) {
-    LOG(ERROR)
-        << "SetSharedImagePurgeable: Could not find shared image mailbox";
-    return false;
-  }
-  shared_image->SetPurgeable(purgeable);
-  return true;
+  return shared_image_manager_->SetPurgeable(mailbox,
+                                             memory_type_tracker_.get());
 }
 
 void SharedImageFactory::DestroyAllSharedImages(bool have_context) {
diff --git a/gpu/command_buffer/service/shared_image/shared_image_factory.h b/gpu/command_buffer/service/shared_image/shared_image_factory.h
index eb04fcc..78f1225 100644
--- a/gpu/command_buffer/service/shared_image/shared_image_factory.h
+++ b/gpu/command_buffer/service/shared_image/shared_image_factory.h
@@ -98,7 +98,7 @@
   bool UpdateSharedImage(const Mailbox& mailbox,
                          std::unique_ptr<gfx::GpuFence> in_fence);
   bool DestroySharedImage(const Mailbox& mailbox);
-  bool SetSharedImagePurgeable(const Mailbox& mailbox, bool purgeable);
+  void SetSharedImagePurgeable(const Mailbox& mailbox, bool purgeable);
   bool HasImages() const { return !shared_images_.empty(); }
   void DestroyAllSharedImages(bool have_context);
 
diff --git a/gpu/command_buffer/service/shared_image/shared_image_manager.cc b/gpu/command_buffer/service/shared_image/shared_image_manager.cc
index 06c252fe..31a6e2bd 100644
--- a/gpu/command_buffer/service/shared_image/shared_image_manager.cc
+++ b/gpu/command_buffer/service/shared_image/shared_image_manager.cc
@@ -542,6 +542,20 @@
   }
 }
 
+bool SharedImageManager::UpdateSharedImage(
+    const Mailbox& mailbox,
+    std::unique_ptr<gfx::GpuFence> in_fence) {
+  AutoLock autolock(this);
+  auto* backing = GetBacking(mailbox);
+  if (!backing) {
+    LOG(ERROR)
+        << "SharedImageManager::UpdateSharedImage: Non-existent mailbox.";
+    return false;
+  }
+  backing->Update(std::move(in_fence));
+  return true;
+}
+
 void SharedImageManager::SetPurgeable(const Mailbox& mailbox, bool purgeable) {
   AutoLock autolock(this);
   auto* backing = GetBacking(mailbox);
diff --git a/gpu/command_buffer/service/shared_image/shared_image_manager.h b/gpu/command_buffer/service/shared_image/shared_image_manager.h
index 57af166..f246e90 100644
--- a/gpu/command_buffer/service/shared_image/shared_image_manager.h
+++ b/gpu/command_buffer/service/shared_image/shared_image_manager.h
@@ -140,6 +140,9 @@
       MemoryTypeTracker* ref);
 #endif
 
+  bool UpdateSharedImage(const Mailbox& mailbox,
+                         std::unique_ptr<gfx::GpuFence> in_fence);
+
 #if BUILDFLAG(IS_WIN)
   void UpdateExternalFence(const Mailbox& mailbox,
                            scoped_refptr<gfx::D3DSharedFence> external_fence);
diff --git a/gpu/command_buffer/service/shared_image/shared_image_representation.h b/gpu/command_buffer/service/shared_image/shared_image_representation.h
index f189ff2..089fb05b 100644
--- a/gpu/command_buffer/service/shared_image/shared_image_representation.h
+++ b/gpu/command_buffer/service/shared_image/shared_image_representation.h
@@ -41,28 +41,26 @@
 class VulkanImage;
 class VulkanImplementation;
 }  // namespace gpu
-#endif
-
-#if BUILDFLAG(IS_WIN)
-#include "ui/gl/dc_layer_overlay_image.h"
-#endif
-
-#if BUILDFLAG(IS_APPLE)
-#include "ui/gfx/mac/io_surface.h"
-#include "ui/gfx/mac/mtl_shared_event_fence.h"
-#endif
-
-#if BUILDFLAG(IS_ANDROID)
-#include "base/android/scoped_hardware_buffer_fence_sync.h"
-
-extern "C" typedef struct AHardwareBuffer AHardwareBuffer;
-#endif
+#endif  // BUILDFLAG(ENABLE_VULKAN)
 
 #if BUILDFLAG(IS_WIN)
 #include <d3d11.h>
 #include <d3d12.h>
 #include <wrl/client.h>
-#endif
+
+#include "ui/gl/dc_layer_overlay_image.h"
+#endif  // BUILDFLAG(IS_WIN)
+
+#if BUILDFLAG(IS_APPLE)
+#include "ui/gfx/mac/io_surface.h"
+#include "ui/gfx/mac/mtl_shared_event_fence.h"
+#endif  // BUILDFLAG(IS_APPLE)
+
+#if BUILDFLAG(IS_ANDROID)
+#include "base/android/scoped_hardware_buffer_fence_sync.h"
+
+extern "C" typedef struct AHardwareBuffer AHardwareBuffer;
+#endif  // BUILDFLAG(IS_ANDROID)
 
 typedef unsigned int GLenum;
 namespace skgpu {
@@ -193,10 +191,6 @@
   ~SharedImageRepresentationFactoryRef() override;
 
   const Mailbox& mailbox() const { return backing()->mailbox(); }
-  void Update(std::unique_ptr<gfx::GpuFence> in_fence) {
-    backing()->Update(std::move(in_fence));
-  }
-  void SetPurgeable(bool purgeable) { backing()->SetPurgeable(purgeable); }
   bool CopyToGpuMemoryBuffer() { return backing()->CopyToGpuMemoryBuffer(); }
   void CopyToGpuMemoryBufferAsync(base::OnceCallback<void(bool)> callback) {
     backing()->CopyToGpuMemoryBufferAsync(std::move(callback));
diff --git a/gpu/command_buffer/service/test_helper.cc b/gpu/command_buffer/service/test_helper.cc
index ec467afd..8204530 100644
--- a/gpu/command_buffer/service/test_helper.cc
+++ b/gpu/command_buffer/service/test_helper.cc
@@ -1053,7 +1053,6 @@
 sh::InterfaceBlock TestHelper::ConstructInterfaceBlock(
     GLint array_size,
     sh::BlockLayoutType layout,
-    bool is_row_major_layout,
     bool static_use,
     const std::string& name,
     const std::string& instance_name,
@@ -1061,7 +1060,6 @@
   sh::InterfaceBlock var;
   var.arraySize = array_size;
   var.layout = layout;
-  var.isRowMajorLayout = is_row_major_layout;
   var.staticUse = static_use;
   var.name = name;
   var.mappedName = name;  // No name hashing.
diff --git a/gpu/command_buffer/service/test_helper.h b/gpu/command_buffer/service/test_helper.h
index 22c39b1..fcb22a9b 100644
--- a/gpu/command_buffer/service/test_helper.h
+++ b/gpu/command_buffer/service/test_helper.h
@@ -218,7 +218,6 @@
   static sh::InterfaceBlock ConstructInterfaceBlock(
       GLint array_size,
       sh::BlockLayoutType layout,
-      bool is_row_major_layout,
       bool static_use,
       const std::string& name,
       const std::string& instance_name,
diff --git a/infra/config/generated/luci/cr-buildbucket.cfg b/infra/config/generated/luci/cr-buildbucket.cfg
index 0753224..6bccd56 100644
--- a/infra/config/generated/luci/cr-buildbucket.cfg
+++ b/infra/config/generated/luci/cr-buildbucket.cfg
@@ -58077,7 +58077,7 @@
         '  "recipe": "chromium/fuzz"'
         '}'
       priority: 35
-      execution_timeout_secs: 115200
+      execution_timeout_secs: 172800
       build_numbers: YES
       service_account: "chromium-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
       experiments {
diff --git a/infra/config/subprojects/chromium/ci/chromium.coverage.star b/infra/config/subprojects/chromium/ci/chromium.coverage.star
index e35994d..68ca294 100644
--- a/infra/config/subprojects/chromium/ci/chromium.coverage.star
+++ b/infra/config/subprojects/chromium/ci/chromium.coverage.star
@@ -1211,9 +1211,8 @@
             short_name = "lnx-fuzz",
         ),
     ],
-    # TODO(crbug.com/449026537): Remove elevated timeout once performance
-    # improves.
-    execution_timeout = 32 * time.hour,
+    # TODO(crbug.com/449026537): Remove elevated timeout once performance improves.
+    execution_timeout = 48 * time.hour,
     notifies = ["chrome-fuzzing-core"],
     properties = {
         "collect_fuzz_coverage": True,
diff --git a/infra/orchestrator/BUILD.gn b/infra/orchestrator/BUILD.gn
index d44782e7..66f66ee 100644
--- a/infra/orchestrator/BUILD.gn
+++ b/infra/orchestrator/BUILD.gn
@@ -104,8 +104,9 @@
     "//third_party/catapult/tracing/third_party/",
     "//third_party/catapult/tracing/tracing/",
 
-    # This file is read at script run-time.
+    # These files are read at script run-time.
     "//tools/perf/core/perf_dashboard_machine_group_mapping.json",
+    "//tools/perf/core/schedule/",
     "//tools/perf/public_builders.json",
   ]
 }
diff --git a/ios/chrome/browser/ai_prototyping/coordinator/ai_prototyping_mediator.mm b/ios/chrome/browser/ai_prototyping/coordinator/ai_prototyping_mediator.mm
index 992fa448..44b8544 100644
--- a/ios/chrome/browser/ai_prototyping/coordinator/ai_prototyping_mediator.mm
+++ b/ios/chrome/browser/ai_prototyping/coordinator/ai_prototyping_mediator.mm
@@ -6,6 +6,7 @@
 
 #import <string>
 
+#import "base/base64.h"
 #import "base/functional/bind.h"
 #import "base/json/json_reader.h"
 #import "base/logging.h"
@@ -13,6 +14,7 @@
 #import "base/strings/stringprintf.h"
 #import "base/strings/sys_string_conversions.h"
 #import "base/strings/utf_string_conversions.h"
+#import "base/task/thread_pool.h"
 #import "base/values.h"
 #import "components/optimization_guide/optimization_guide_buildflags.h"
 #import "components/optimization_guide/proto/features/actions_data.pb.h"
@@ -35,6 +37,7 @@
 #import "ios/chrome/browser/intelligence/enhanced_calendar/model/enhanced_calendar_service_impl.h"
 #import "ios/chrome/browser/intelligence/proto_wrappers/ios_smart_tab_grouping_request_wrapper.h"
 #import "ios/chrome/browser/intelligence/proto_wrappers/page_context_wrapper.h"
+#import "ios/chrome/browser/intelligence/proto_wrappers/page_context_wrapper_config.h"
 #import "ios/chrome/browser/intelligence/proto_wrappers/tab_organization_request_wrapper.h"
 #import "ios/chrome/browser/intelligence/smart_tab_grouping/model/smart_tab_grouping_service_impl.h"
 #import "ios/chrome/browser/intelligence/smart_tab_grouping/utils/smart_tab_grouping_utils.h"
@@ -149,6 +152,7 @@
 - (void)executeFreeformServerQuery:(NSString*)query
                 systemInstructions:(NSString*)systemInstructions
                 includePageContext:(BOOL)includePageContext
+                    richExtraction:(BOOL)richExtraction
                       uploadToMQLS:(BOOL)uploadToMQLS
                   storePageContext:(BOOL)storePageContext
                        temperature:(float)temperature
@@ -212,9 +216,9 @@
           });
 
   // Populate the PageContext proto and then execute the query.
-  _pageContextWrapper =
-      CreatePageContextWrapper(_webStateList->GetActiveWebState(),
-                               std::move(page_context_completion_callback));
+  _pageContextWrapper = CreatePageContextWrapper(
+      _webStateList->GetActiveWebState(), richExtraction,
+      std::move(page_context_completion_callback));
   PopulatePageContext(_pageContextWrapper, _webStateList->GetActiveWebState());
 }
 
@@ -617,4 +621,86 @@
   }
 }
 
+// Executes the APC extraction for the active WebState using
+// `PageContextWrapper`. The resulting `PageContext` proto is serialized to disk
+// in a background thread, and the file path is displayed in the prototyping
+// menu.
+- (void)executeAPCExtractionWithRichExtraction:(BOOL)useRichExtraction {
+  web::WebState* activeWebState = _webStateList->GetActiveWebState();
+  if (!activeWebState) {
+    [self.consumer updateQueryResult:@"Error: No active web state."
+                          forFeature:AIPrototypingFeature::kAPC];
+    return;
+  }
+
+  PageContextWrapperConfig config = PageContextWrapperConfigBuilder()
+                                        .SetUseRichExtraction(useRichExtraction)
+                                        .Build();
+
+  __weak __typeof(self) weakSelf = self;
+  auto completion = base::BindOnce(^(
+      PageContextWrapperCallbackResponse response) {
+    if (!response.has_value()) {
+      [weakSelf.consumer
+          updateQueryResult:@"Error: Failed to populate PageContext."
+                 forFeature:AIPrototypingFeature::kAPC];
+      return;
+    }
+
+    std::unique_ptr<optimization_guide::proto::PageContext> page_context =
+        std::move(response.value());
+
+    std::string serialized_proto = page_context->SerializeAsString();
+
+    auto write_to_disk_task = base::BindOnce(
+        [](std::unique_ptr<optimization_guide::proto::PageContext> context) {
+          return SaveSerializedPageContextToDisk(*context);
+        },
+        std::move(page_context));
+
+    auto save_to_disk_callback =
+        base::BindOnce(^(SavePageContextResult result) {
+          NSMutableString* outputStr = [NSMutableString string];
+          if (result.success) {
+            [outputStr
+                appendString:@"Instructions:\nFollow the directions at "
+                             @"go/readableapc to view the extracted APC.\n\n"];
+            [outputStr
+                appendFormat:@"Proto saved to:\n%@\n\n",
+                             base::SysUTF8ToNSString(result.file_path.value())];
+          } else {
+            [outputStr appendString:@"Warning: Failed to save to disk.\n\n"];
+          }
+
+          [outputStr appendString:@"Proto Base64 Bytes:\n"];
+
+          // Encode the serialized proto to base64 to prevent corruption when
+          // displayed in the UI.
+          std::string base64_encoded = base::Base64Encode(serialized_proto);
+          NSString* base64Str = base::SysUTF8ToNSString(base64_encoded);
+          [outputStr appendString:base64Str];
+
+          [weakSelf.consumer updateQueryResult:outputStr
+                                    forFeature:AIPrototypingFeature::kAPC];
+          if ([weakSelf.consumer
+                  respondsToSelector:@selector(updateRawBytes:forFeature:)]) {
+            [weakSelf.consumer updateRawBytes:base64Str
+                                   forFeature:AIPrototypingFeature::kAPC];
+          }
+        });
+
+    base::ThreadPool::PostTaskAndReplyWithResult(
+        FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
+        std::move(write_to_disk_task), std::move(save_to_disk_callback));
+  });
+
+  _pageContextWrapper =
+      [[PageContextWrapper alloc] initWithWebState:activeWebState
+                                            config:config
+                                completionCallback:std::move(completion)];
+
+  _pageContextWrapper.shouldGetAnnotatedPageContent = YES;
+  [_pageContextWrapper populatePageContextFieldsAsync];
+}
+
 @end
diff --git a/ios/chrome/browser/ai_prototyping/ui/BUILD.gn b/ios/chrome/browser/ai_prototyping/ui/BUILD.gn
index 52b1289..7444130 100644
--- a/ios/chrome/browser/ai_prototyping/ui/BUILD.gn
+++ b/ios/chrome/browser/ai_prototyping/ui/BUILD.gn
@@ -6,6 +6,8 @@
   sources = [
     "ai_prototyping_actuation_view_controller.h",
     "ai_prototyping_actuation_view_controller.mm",
+    "ai_prototyping_apc_view_controller.h",
+    "ai_prototyping_apc_view_controller.mm",
     "ai_prototyping_calendar_view_controller.h",
     "ai_prototyping_calendar_view_controller.mm",
     "ai_prototyping_consumer.h",
diff --git a/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_apc_view_controller.h b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_apc_view_controller.h
new file mode 100644
index 0000000..1fbf70f
--- /dev/null
+++ b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_apc_view_controller.h
@@ -0,0 +1,21 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CHROME_BROWSER_AI_PROTOTYPING_UI_AI_PROTOTYPING_APC_VIEW_CONTROLLER_H_
+#define IOS_CHROME_BROWSER_AI_PROTOTYPING_UI_AI_PROTOTYPING_APC_VIEW_CONTROLLER_H_
+
+#import <UIKit/UIKit.h>
+
+#import "ios/chrome/browser/ai_prototyping/ui/ai_prototyping_view_controller_protocol.h"
+
+// Page to extract and view the Annotated Page Content(APC) of the current page.
+@interface AIPrototypingAPCViewController
+    : UIViewController <AIPrototypingViewControllerProtocol>
+
+// Use `initWithFeature` from AIPrototypingViewControllerProtocol instead.
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+#endif  // IOS_CHROME_BROWSER_AI_PROTOTYPING_UI_AI_PROTOTYPING_APC_VIEW_CONTROLLER_H_
diff --git a/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_apc_view_controller.mm b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_apc_view_controller.mm
new file mode 100644
index 0000000..103c5c1
--- /dev/null
+++ b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_apc_view_controller.mm
@@ -0,0 +1,153 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/ai_prototyping/ui/ai_prototyping_apc_view_controller.h"
+
+#import <UIKit/UIKit.h>
+
+#import "ios/chrome/browser/ai_prototyping/ui/ai_prototyping_mutator.h"
+#import "ios/chrome/browser/ai_prototyping/utils/ai_prototyping_constants.h"
+#import "ios/chrome/common/ui/colors/semantic_color_names.h"
+
+@implementation AIPrototypingAPCViewController {
+  UIButton* _submitButton;
+  UIButton* _copyButton;
+  UISwitch* _richExtractionSwitch;
+  UITextView* _responseContainer;
+  // The raw bytes of the APC in Base64 format.
+  NSString* _rawBytes;
+}
+
+@synthesize mutator = _mutator;
+@synthesize feature = _feature;
+
+- (instancetype)initForFeature:(AIPrototypingFeature)feature {
+  self = [super init];
+  if (self) {
+    _feature = feature;
+  }
+  return self;
+}
+
+- (void)viewDidLoad {
+  [super viewDidLoad];
+
+  self.sheetPresentationController.detents = @[
+    [UISheetPresentationControllerDetent mediumDetent],
+    [UISheetPresentationControllerDetent largeDetent],
+  ];
+
+  UILabel* label = [[UILabel alloc] init];
+  label.translatesAutoresizingMaskIntoConstraints = NO;
+  label.font = [UIFont preferredFontForTextStyle:UIFontTextStyleTitle2];
+  label.text = @"Annotated Page Content";
+
+  UIColor* primaryColor = [UIColor colorNamed:kTextPrimaryColor];
+
+  _submitButton = [UIButton buttonWithType:UIButtonTypeSystem];
+  _submitButton.backgroundColor = [UIColor colorNamed:kBlueColor];
+  _submitButton.layer.cornerRadius = kCornerRadius;
+  [_submitButton setTitle:@"Extract APC" forState:UIControlStateNormal];
+  [_submitButton setTitleColor:primaryColor forState:UIControlStateNormal];
+  [_submitButton addTarget:self
+                    action:@selector(submitButtonPressed:)
+          forControlEvents:UIControlEventTouchUpInside];
+
+  _copyButton = [UIButton buttonWithType:UIButtonTypeSystem];
+  _copyButton.backgroundColor = [UIColor colorNamed:kBlueColor];
+  _copyButton.layer.cornerRadius = kCornerRadius;
+  [_copyButton setTitle:@"Copy Raw Bytes" forState:UIControlStateNormal];
+  [_copyButton setTitleColor:primaryColor forState:UIControlStateNormal];
+  [_copyButton addTarget:self
+                  action:@selector(copyButtonPressed:)
+        forControlEvents:UIControlEventTouchUpInside];
+
+  UIStackView* buttonsContainer = [[UIStackView alloc]
+      initWithArrangedSubviews:@[ _submitButton, _copyButton ]];
+  buttonsContainer.translatesAutoresizingMaskIntoConstraints = NO;
+  buttonsContainer.axis = UILayoutConstraintAxisHorizontal;
+  buttonsContainer.spacing = kButtonStackViewSpacing;
+  buttonsContainer.distribution = UIStackViewDistributionFillEqually;
+
+  _richExtractionSwitch = [[UISwitch alloc] init];
+  _richExtractionSwitch.translatesAutoresizingMaskIntoConstraints = NO;
+  _richExtractionSwitch.on = YES;
+
+  UILabel* switchLabel = [[UILabel alloc] init];
+  switchLabel.translatesAutoresizingMaskIntoConstraints = NO;
+  switchLabel.numberOfLines = 0;
+  switchLabel.text = @"Use APC V2 (Rich Extraction)";
+
+  UIStackView* switchContainer = [[UIStackView alloc]
+      initWithArrangedSubviews:@[ _richExtractionSwitch, switchLabel ]];
+  switchContainer.translatesAutoresizingMaskIntoConstraints = NO;
+  switchContainer.axis = UILayoutConstraintAxisHorizontal;
+  switchContainer.spacing = kButtonStackViewSpacing;
+  switchContainer.alignment = UIStackViewAlignmentCenter;
+
+  _responseContainer = [UITextView textViewUsingTextLayoutManager:NO];
+  _responseContainer.translatesAutoresizingMaskIntoConstraints = NO;
+  _responseContainer.editable = NO;
+  _responseContainer.layer.cornerRadius = kCornerRadius;
+  _responseContainer.layer.masksToBounds = YES;
+  _responseContainer.layer.borderColor = [primaryColor CGColor];
+  _responseContainer.layer.borderWidth = kBorderWidth;
+  _responseContainer.textContainer.lineBreakMode = NSLineBreakByWordWrapping;
+  _responseContainer.text = @"Press 'Extract APC' to begin.";
+
+  UIStackView* stackView = [[UIStackView alloc] initWithArrangedSubviews:@[
+    label, switchContainer, buttonsContainer, _responseContainer
+  ]];
+  stackView.translatesAutoresizingMaskIntoConstraints = NO;
+  stackView.axis = UILayoutConstraintAxisVertical;
+  stackView.spacing = kMainStackViewSpacing;
+  [self.view addSubview:stackView];
+
+  [NSLayoutConstraint activateConstraints:@[
+    [stackView.topAnchor constraintEqualToAnchor:self.view.topAnchor
+                                        constant:kMainStackTopInset],
+    [stackView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor
+                                            constant:kHorizontalInset],
+    [stackView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor
+                                             constant:-kHorizontalInset],
+    [_responseContainer.heightAnchor
+        constraintGreaterThanOrEqualToAnchor:self.view.heightAnchor
+                                  multiplier:
+                                      kResponseContainerHeightMultiplier],
+  ]];
+}
+
+- (void)submitButtonPressed:(UIButton*)button {
+  _submitButton.enabled = NO;
+  _submitButton.backgroundColor = [UIColor colorNamed:kDisabledTintColor];
+  _responseContainer.text = @"";
+  [self.mutator
+      executeAPCExtractionWithRichExtraction:_richExtractionSwitch.isOn];
+}
+
+- (void)copyButtonPressed:(UIButton*)button {
+  UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
+  if (_rawBytes.length > 0) {
+    pasteboard.string = _rawBytes;
+  } else {
+    pasteboard.string = _responseContainer.text;
+  }
+}
+
+#pragma mark - AIPrototypingViewControllerProtocol
+
+- (void)updateResponseField:(NSString*)response {
+  _responseContainer.text = response;
+}
+
+- (void)updateRawBytes:(NSString*)rawBytes {
+  _rawBytes = rawBytes;
+}
+
+- (void)enableSubmitButtons {
+  _submitButton.enabled = YES;
+  _submitButton.backgroundColor = [UIColor colorNamed:kBlueColor];
+}
+
+@end
diff --git a/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_consumer.h b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_consumer.h
index 7670578..3cb2690 100644
--- a/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_consumer.h
+++ b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_consumer.h
@@ -18,6 +18,10 @@
 // tabs: Array of dictionaries with keys "id", "title", "url".
 - (void)updateTabList:(NSArray<NSDictionary*>*)tabs;
 
+// Updates the raw bytes (in Base64 representation) of the PageContext proto.
+- (void)updateRawBytes:(NSString*)rawBytes
+            forFeature:(AIPrototypingFeature)feature;
+
 @end
 
 #endif  // IOS_CHROME_BROWSER_AI_PROTOTYPING_UI_AI_PROTOTYPING_CONSUMER_H_
diff --git a/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_freeform_view_controller.mm b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_freeform_view_controller.mm
index 0a173a8..623a5811 100644
--- a/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_freeform_view_controller.mm
+++ b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_freeform_view_controller.mm
@@ -29,6 +29,7 @@
   UITextField* _systemInstructionsField;
   UITextField* _queryField;
   UISwitch* _includePageContextSwitch;
+  UISwitch* _richExtractionSwitch;
   UISwitch* _uploadMQLSSwitch;
   UISwitch* _storePageContextSwitch;
   UISlider* _temperatureSlider;
@@ -100,6 +101,25 @@
   switchContainer.spacing = kButtonStackViewSpacing;
   switchContainer.alignment = UIStackViewAlignmentCenter;
 
+  // Rich extraction switch.
+  _richExtractionSwitch = [[UISwitch alloc] init];
+  _richExtractionSwitch.translatesAutoresizingMaskIntoConstraints = NO;
+  _richExtractionSwitch.on = YES;
+
+  UILabel* richExtractionSwitchLabel = [[UILabel alloc] init];
+  richExtractionSwitchLabel.translatesAutoresizingMaskIntoConstraints = NO;
+  richExtractionSwitchLabel.numberOfLines = 0;
+  richExtractionSwitchLabel.text = @"Extract with APC v2";
+
+  UIStackView* richExtractionSwitchContainer =
+      [[UIStackView alloc] initWithArrangedSubviews:@[
+        _richExtractionSwitch, richExtractionSwitchLabel
+      ]];
+  richExtractionSwitchContainer.translatesAutoresizingMaskIntoConstraints = NO;
+  richExtractionSwitchContainer.axis = UILayoutConstraintAxisHorizontal;
+  richExtractionSwitchContainer.spacing = kButtonStackViewSpacing;
+  richExtractionSwitchContainer.alignment = UIStackViewAlignmentCenter;
+
   // MQLS upload switch.
   _uploadMQLSSwitch = [[UISwitch alloc] init];
   _uploadMQLSSwitch.translatesAutoresizingMaskIntoConstraints = NO;
@@ -248,9 +268,9 @@
 
   UIStackView* stackView = [[UIStackView alloc] initWithArrangedSubviews:@[
     label, systemInstructionsFieldContainer, queryFieldContainer,
-    _modelPickerButton, switchContainer, uploadMQLSSwitchContainer,
-    storePageContextSwitchContainer, temperatureContainer, buttonStackView,
-    _responseContainer
+    _modelPickerButton, switchContainer, richExtractionSwitchContainer,
+    uploadMQLSSwitchContainer, storePageContextSwitchContainer,
+    temperatureContainer, buttonStackView, _responseContainer
   ]];
   stackView.translatesAutoresizingMaskIntoConstraints = NO;
   stackView.axis = UILayoutConstraintAxisVertical;
@@ -301,6 +321,7 @@
   [self.mutator executeFreeformServerQuery:_queryField.text
                         systemInstructions:_systemInstructionsField.text
                         includePageContext:_includePageContextSwitch.isOn
+                            richExtraction:_richExtractionSwitch.isOn
                               uploadToMQLS:_uploadMQLSSwitch.isOn
                           storePageContext:_storePageContextSwitch.isOn
                                temperature:_temperatureSlider.value
diff --git a/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_mutator.h b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_mutator.h
index f4165d5d..72a33a2 100644
--- a/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_mutator.h
+++ b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_mutator.h
@@ -24,6 +24,7 @@
 - (void)executeFreeformServerQuery:(NSString*)query
                 systemInstructions:(NSString*)systemInstructions
                 includePageContext:(BOOL)includePageContext
+                    richExtraction:(BOOL)richExtraction
                       uploadToMQLS:(BOOL)uploadToMQLS
                   storePageContext:(BOOL)storePageContext
                        temperature:(float)temperature
@@ -53,6 +54,9 @@
 // Requests a list of current tabs.
 - (void)listTabs;
 
+// Executes an APC extraction request.
+- (void)executeAPCExtractionWithRichExtraction:(BOOL)useRichExtraction;
+
 @end
 
 #endif  // IOS_CHROME_BROWSER_AI_PROTOTYPING_UI_AI_PROTOTYPING_MUTATOR_H_
diff --git a/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_view_controller.mm b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_view_controller.mm
index e841f2b..7a52a00 100644
--- a/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_view_controller.mm
+++ b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_view_controller.mm
@@ -5,6 +5,7 @@
 #import "ios/chrome/browser/ai_prototyping/ui/ai_prototyping_view_controller.h"
 
 #import "ios/chrome/browser/ai_prototyping/ui/ai_prototyping_actuation_view_controller.h"
+#import "ios/chrome/browser/ai_prototyping/ui/ai_prototyping_apc_view_controller.h"
 #import "ios/chrome/browser/ai_prototyping/ui/ai_prototyping_calendar_view_controller.h"
 #import "ios/chrome/browser/ai_prototyping/ui/ai_prototyping_consumer.h"
 #import "ios/chrome/browser/ai_prototyping/ui/ai_prototyping_freeform_view_controller.h"
@@ -44,6 +45,8 @@
                 initForFeature:AIPrototypingFeature::kSmartTabGrouping],
             [[AIPrototypingCalendarViewController alloc]
                 initForFeature:AIPrototypingFeature::kEnhancedCalendar],
+            [[AIPrototypingAPCViewController alloc]
+                initForFeature:AIPrototypingFeature::kAPC],
             _actuationViewController, nil];
   }
   return self;
@@ -88,6 +91,19 @@
   }
 }
 
+- (void)updateRawBytes:(NSString*)rawBytes
+            forFeature:(AIPrototypingFeature)feature {
+  for (UIViewController<AIPrototypingViewControllerProtocol>* viewController in
+           _menuPages) {
+    if (viewController.feature == feature) {
+      if ([viewController respondsToSelector:@selector(updateRawBytes:)]) {
+        [viewController updateRawBytes:rawBytes];
+      }
+      break;
+    }
+  }
+}
+
 - (void)updateTabList:(NSArray<NSDictionary*>*)tabs {
   [_actuationViewController updateTabList:tabs];
 }
diff --git a/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_view_controller_protocol.h b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_view_controller_protocol.h
index c2cc99d..7e4d135 100644
--- a/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_view_controller_protocol.h
+++ b/ios/chrome/browser/ai_prototyping/ui/ai_prototyping_view_controller_protocol.h
@@ -27,6 +27,12 @@
 // Enable submit buttons, and style them accordingly.
 - (void)enableSubmitButtons;
 
+@optional
+
+// Updates the view controller with the raw bytes (in base64) of the
+// PageContext proto, if supported.
+- (void)updateRawBytes:(NSString*)rawBytes;
+
 @end
 
 #endif  // IOS_CHROME_BROWSER_AI_PROTOTYPING_UI_AI_PROTOTYPING_VIEW_CONTROLLER_PROTOCOL_H_
diff --git a/ios/chrome/browser/ai_prototyping/utils/ai_prototyping_constants.h b/ios/chrome/browser/ai_prototyping/utils/ai_prototyping_constants.h
index 5e0ecee5..3c42ec9 100644
--- a/ios/chrome/browser/ai_prototyping/utils/ai_prototyping_constants.h
+++ b/ios/chrome/browser/ai_prototyping/utils/ai_prototyping_constants.h
@@ -33,6 +33,8 @@
   kSmartTabGrouping,
   // Represents the actuation tools feature.
   kActuationTools,
+  // Represents the APC extraction feature.
+  kAPC,
 };
 
 #endif  // IOS_CHROME_BROWSER_AI_PROTOTYPING_UTILS_AI_PROTOTYPING_CONSTANTS_H_
diff --git a/ios/chrome/browser/ai_prototyping/utils/page_context_util.h b/ios/chrome/browser/ai_prototyping/utils/page_context_util.h
index ea80ba7..96b6b39 100644
--- a/ios/chrome/browser/ai_prototyping/utils/page_context_util.h
+++ b/ios/chrome/browser/ai_prototyping/utils/page_context_util.h
@@ -38,6 +38,7 @@
 
 PageContextWrapper* CreatePageContextWrapper(
     web::WebState* web_state,
+    bool rich_extraction,
     base::OnceCallback<void(PageContextWrapperCallbackResponse)>
         completion_callback);
 
diff --git a/ios/chrome/browser/ai_prototyping/utils/page_context_util.mm b/ios/chrome/browser/ai_prototyping/utils/page_context_util.mm
index 7f6f4d10..8c53386 100644
--- a/ios/chrome/browser/ai_prototyping/utils/page_context_util.mm
+++ b/ios/chrome/browser/ai_prototyping/utils/page_context_util.mm
@@ -128,10 +128,15 @@
 
 PageContextWrapper* CreatePageContextWrapper(
     web::WebState* web_state,
+    bool rich_extraction,
     base::OnceCallback<void(PageContextWrapperCallbackResponse)>
         completion_callback) {
+  PageContextWrapperConfig config = PageContextWrapperConfigBuilder()
+                                        .SetUseRichExtraction(rich_extraction)
+                                        .Build();
   PageContextWrapper* page_context_wrapper = [[PageContextWrapper alloc]
         initWithWebState:web_state
+                  config:config
       completionCallback:std::move(completion_callback)];
   [page_context_wrapper setShouldGetAnnotatedPageContent:YES];
   [page_context_wrapper setShouldGetSnapshot:YES];
diff --git a/ios/chrome/browser/autofill/autofill_ai/public/BUILD.gn b/ios/chrome/browser/autofill/autofill_ai/public/BUILD.gn
index a137102..491f291 100644
--- a/ios/chrome/browser/autofill/autofill_ai/public/BUILD.gn
+++ b/ios/chrome/browser/autofill/autofill_ai/public/BUILD.gn
@@ -11,6 +11,7 @@
   deps = [
     "//base",
     "//components/autofill/core/browser",
+    "//components/strings:components_strings__strings_grit",
     "//ios/chrome/app/strings:ios_strings__strings_grit",
   ]
   frameworks = [ "Foundation.framework" ]
diff --git a/ios/chrome/browser/autofill/autofill_ai/public/save_entity_params.mm b/ios/chrome/browser/autofill/autofill_ai/public/save_entity_params.mm
index 850202a..3fa3ae8 100644
--- a/ios/chrome/browser/autofill/autofill_ai/public/save_entity_params.mm
+++ b/ios/chrome/browser/autofill/autofill_ai/public/save_entity_params.mm
@@ -5,6 +5,7 @@
 #import "ios/chrome/browser/autofill/autofill_ai/public/save_entity_params.h"
 
 #import "components/autofill/core/browser/data_model/autofill_ai/entity_type.h"
+#import "components/strings/grit/components_strings.h"
 #import "ios/chrome/grit/ios_strings.h"
 #import "ui/base/l10n/l10n_util.h"
 
@@ -25,10 +26,41 @@
 SaveEntityParams::~SaveEntityParams() = default;
 
 std::u16string SaveEntityParams::GetTitleText() const {
-  return l10n_util::GetStringFUTF16(IsUpdate()
-                                        ? IDS_IOS_AUTOFILL_AI_UPDATE_PROMPT
-                                        : IDS_IOS_AUTOFILL_AI_SAVE_PROMPT,
-                                    new_entity.type().GetNameForI18n());
+  switch (new_entity.type().name()) {
+    case EntityTypeName::kDriversLicense:
+      return l10n_util::GetStringUTF16(
+          IsUpdate()
+              ? IDS_AUTOFILL_AI_UPDATE_DRIVERS_LICENSE_ENTITY_DIALOG_TITLE
+              : IDS_AUTOFILL_AI_SAVE_DRIVERS_LICENSE_ENTITY_DIALOG_TITLE);
+    case EntityTypeName::kNationalIdCard:
+      return l10n_util::GetStringUTF16(
+          IsUpdate()
+              ? IDS_AUTOFILL_AI_UPDATE_NATIONAL_ID_CARD_ENTITY_DIALOG_TITLE
+              : IDS_AUTOFILL_AI_SAVE_NATIONAL_ID_CARD_ENTITY_DIALOG_TITLE);
+    case EntityTypeName::kPassport:
+      return l10n_util::GetStringUTF16(
+          IsUpdate() ? IDS_AUTOFILL_AI_UPDATE_PASSPORT_ENTITY_DIALOG_TITLE
+                     : IDS_AUTOFILL_AI_SAVE_PASSPORT_ENTITY_DIALOG_TITLE);
+    case EntityTypeName::kVehicle:
+      return l10n_util::GetStringUTF16(
+          IsUpdate() ? IDS_AUTOFILL_AI_UPDATE_VEHICLE_ENTITY_DIALOG_TITLE
+                     : IDS_AUTOFILL_AI_SAVE_VEHICLE_ENTITY_DIALOG_TITLE);
+    case EntityTypeName::kKnownTravelerNumber:
+      return l10n_util::GetStringUTF16(
+          IsUpdate()
+              ? IDS_AUTOFILL_AI_UPDATE_KNOWN_TRAVELER_NUMBER_ENTITY_DIALOG_TITLE
+              : IDS_AUTOFILL_AI_SAVE_KNOWN_TRAVELER_NUMBER_ENTITY_DIALOG_TITLE);
+    case EntityTypeName::kRedressNumber:
+      return l10n_util::GetStringUTF16(
+          IsUpdate() ? IDS_AUTOFILL_AI_UPDATE_REDRESS_NUMBER_ENTITY_DIALOG_TITLE
+                     : IDS_AUTOFILL_AI_SAVE_REDRESS_NUMBER_ENTITY_DIALOG_TITLE);
+    case EntityTypeName::kFlightReservation:
+    case EntityTypeName::kOrder:
+      return l10n_util::GetStringFUTF16(IsUpdate()
+                                            ? IDS_IOS_AUTOFILL_AI_UPDATE_PROMPT
+                                            : IDS_IOS_AUTOFILL_AI_SAVE_PROMPT,
+                                        new_entity.type().GetNameForI18n());
+  }
 }
 
 std::u16string SaveEntityParams::GetMessageText() const {
diff --git a/ios/chrome/browser/intelligence/bwg/coordinator/gemini_promo_scene_agent.mm b/ios/chrome/browser/intelligence/bwg/coordinator/gemini_promo_scene_agent.mm
index 3c8f45f..3ebe6b3 100644
--- a/ios/chrome/browser/intelligence/bwg/coordinator/gemini_promo_scene_agent.mm
+++ b/ios/chrome/browser/intelligence/bwg/coordinator/gemini_promo_scene_agent.mm
@@ -10,7 +10,7 @@
 #import "ios/chrome/browser/promos_manager/model/promos_manager.h"
 
 @implementation GeminiPromoSceneAgent {
-  raw_ptr<PromosManager, DanglingUntriaged> _promosManager;
+  raw_ptr<PromosManager> _promosManager;
 }
 
 - (instancetype)initWithPromosManager:(PromosManager*)promosManager {
@@ -21,6 +21,12 @@
   return self;
 }
 
+#pragma mark - ObservingSceneAgent
+
+- (void)sceneStateDidDisableUI:(SceneState*)sceneState {
+  _promosManager = nullptr;
+}
+
 #pragma mark - SceneStateObserver
 
 - (void)sceneState:(SceneState*)sceneState
diff --git a/ios/chrome/browser/intelligence/bwg/coordinator/gemini_promo_scene_agent_unittest.mm b/ios/chrome/browser/intelligence/bwg/coordinator/gemini_promo_scene_agent_unittest.mm
index 6442e061..022df6c 100644
--- a/ios/chrome/browser/intelligence/bwg/coordinator/gemini_promo_scene_agent_unittest.mm
+++ b/ios/chrome/browser/intelligence/bwg/coordinator/gemini_promo_scene_agent_unittest.mm
@@ -39,6 +39,11 @@
         initWithPromosManager:promos_manager_.get()];
 
     agent_.sceneState = scene_state_;
+    scene_state_.UIEnabled = YES;
+  }
+
+  void TearDown() override {
+    scene_state_.UIEnabled = NO;
   }
 
  protected:
diff --git a/ios/chrome/browser/intelligence/proto_wrappers/annotated_page_content_extraction_utils.mm b/ios/chrome/browser/intelligence/proto_wrappers/annotated_page_content_extraction_utils.mm
index c88ea1b..1171bc1 100644
--- a/ios/chrome/browser/intelligence/proto_wrappers/annotated_page_content_extraction_utils.mm
+++ b/ios/chrome/browser/intelligence/proto_wrappers/annotated_page_content_extraction_utils.mm
@@ -26,6 +26,7 @@
 constexpr char kTextStyleKey[] = "textStyle";
 constexpr char kHasEmphasisKey[] = "hasEmphasis";
 constexpr char kTextSizeKey[] = "textSize";
+constexpr char kColorKey[] = "color";
 constexpr char kAnchorDataKey[] = "anchorData";
 constexpr char kUrlKey[] = "url";
 constexpr char kRelKey[] = "rel";
@@ -119,6 +120,13 @@
           ->set_text_size(
               static_cast<optimization_guide::proto::TextSize>(*text_size));
     }
+
+    if (std::optional<int> color = ReadJsNumber(*text_style, kColorKey)) {
+      destination_node->mutable_content_attributes()
+          ->mutable_text_data()
+          ->mutable_text_style()
+          ->set_color(static_cast<uint32_t>(*color));
+    }
   }
 }
 
diff --git a/ios/chrome/browser/intelligence/proto_wrappers/page_context_extractor_java_script_feature_unittest.mm b/ios/chrome/browser/intelligence/proto_wrappers/page_context_extractor_java_script_feature_unittest.mm
index 81a0ed1..90d7b920 100644
--- a/ios/chrome/browser/intelligence/proto_wrappers/page_context_extractor_java_script_feature_unittest.mm
+++ b/ios/chrome/browser/intelligence/proto_wrappers/page_context_extractor_java_script_feature_unittest.mm
@@ -10,6 +10,7 @@
 #import "base/strings/string_util.h"
 #import "base/strings/sys_string_conversions.h"
 #import "base/test/ios/wait_util.h"
+#import "base/test/test_future.h"
 #import "base/test/values_test_util.h"
 #import "base/time/time.h"
 #import "base/values.h"
@@ -372,3 +373,55 @@
                "contentAttributes.textInfo.textContent");
   }
 }
+
+// Test the extraction of the text color.
+TEST_F(PageContextExtractorJavaScriptFeatureTest,
+       ExtractPageContext_RichExtraction_Text_Color) {
+  const std::string html = "<html><body><p style=\"color: rgb(0, 255, "
+                           "0)\">Green Text</p></body></html>";
+  web::test::LoadHtml(base::SysUTF8ToNSString(html),
+                      test_server_.GetURL(kMainPagePath), web_state());
+
+  base::test::TestFuture<base::Value> future;
+  feature()->ExtractPageContext(
+      web_state()->GetPageWorldWebFramesManager()->GetMainWebFrame(),
+      /*include_anchors=*/false, /*include_cross_origin_frame_content=*/false,
+      /*use_apc_v2=*/true, "nonce", base::Seconds(1),
+      base::BindOnce(
+          [](base::OnceCallback<void(base::Value)> callback,
+             const base::Value* value) {
+            std::move(callback).Run(value ? value->Clone() : base::Value());
+          },
+          future.GetCallback()));
+
+  base::Value result_value = future.Take();
+  ASSERT_TRUE(result_value.is_dict())
+      << "Result is not a dictionary. Type: " << result_value.type();
+
+  const base::DictValue& dict = result_value.GetDict();
+  const base::DictValue* root_node = dict.FindDict("rootNode");
+  ASSERT_TRUE(root_node);
+
+  const base::ListValue* children = root_node->FindList("childrenNodes");
+  ASSERT_TRUE(children);
+  ASSERT_EQ(children->size(), 1u);
+
+  // Check Paragraph
+  const base::DictValue& p_node = (*children)[0].GetDict();
+  const base::ListValue* p_children = p_node.FindList("childrenNodes");
+  ASSERT_TRUE(p_children);
+  ASSERT_EQ(p_children->size(), 1u);
+  const base::DictValue& p_text_node = (*p_children)[0].GetDict();
+  const std::string* p_text = p_text_node.FindStringByDottedPath(
+      "contentAttributes.textInfo.textContent");
+  ASSERT_TRUE(p_text);
+  EXPECT_EQ(*p_text, "Green Text");
+
+  // Check Color
+  // Green: (0, 255, 0) -> (0 << 24) | (255 << 16) | (0 << 8) | 255
+  // = 0 | 16711680 | 0 | 255 = 16711935
+  std::optional<double> color = p_text_node.FindDoubleByDottedPath(
+      "contentAttributes.textInfo.textStyle.color");
+  ASSERT_TRUE(color.has_value());
+  EXPECT_EQ(static_cast<uint32_t>(*color), 16711935u);
+}
diff --git a/ios/chrome/browser/intelligence/proto_wrappers/page_context_wrapper_unittest.mm b/ios/chrome/browser/intelligence/proto_wrappers/page_context_wrapper_unittest.mm
index 48f16f5..d81f3b67 100644
--- a/ios/chrome/browser/intelligence/proto_wrappers/page_context_wrapper_unittest.mm
+++ b/ios/chrome/browser/intelligence/proto_wrappers/page_context_wrapper_unittest.mm
@@ -3448,6 +3448,59 @@
   }
 }
 
+// Tests that top layer elements (popovers, dialog modals, fullscreen) are
+// included in the APC tree as generic containers.
+TEST_P(PageContextWrapperTest, PopulatePageContext_GenericContainer_TopLayer) {
+  if (!IsRefactored()) {
+    GTEST_SKIP() << "ApcV2 not supported for the non-refactored APC wrapper";
+  }
+
+  // <dialog> elements shown via showModal() are rendered in the top layer,
+  // making them generic containers that would normally be flattened (no
+  // scrolling, no fixed pos).
+  auto page_structure =
+      HtmlPage("Main", RawHtml("<dialog id='target'>Target</dialog>"));
+  std::string main_html = page_helper_->Build(page_structure);
+  web::test::LoadHtml(base::SysUTF8ToNSString(main_html),
+                      test_server_.GetURL(kMainPagePath), web_state());
+
+  CallJavascript("document.getElementById('target').showModal();");
+
+  PageContextWrapperConfig config =
+      PageContextWrapperConfigBuilder().SetUseRichExtraction(true).Build();
+
+  PageContextWrapperCallbackResponse response = RunPageContextWrapperWithConfig(
+      web_state(), config, ^(PageContextWrapper* wrapper) {
+        wrapper.shouldGetAnnotatedPageContent = YES;
+      });
+
+  ASSERT_TRUE(response.has_value());
+  const auto& apc = response.value()->annotated_page_content();
+
+  // Traverse tree to find the <dialog> node.
+  const auto& root_node = apc.root_node();
+  const optimization_guide::proto::ContentNode* target_node = nullptr;
+
+  // The root node children are usually the un-flattened elements.
+  for (const auto& child : root_node.children_nodes()) {
+    if (child.content_attributes().attribute_type() ==
+        optimization_guide::proto::CONTENT_ATTRIBUTE_CONTAINER) {
+      target_node = &child;
+      break;
+    }
+  }
+
+  ASSERT_TRUE(target_node)
+      << "Dialog node not found as a container in APC tree";
+
+  ASSERT_EQ(target_node->children_nodes_size(), 1);
+  const auto& text_node = target_node->children_nodes(0);
+  EXPECT_EQ(text_node.content_attributes().attribute_type(),
+            optimization_guide::proto::CONTENT_ATTRIBUTE_TEXT);
+  EXPECT_EQ(text_node.content_attributes().text_data().text_content(),
+            "Target");
+}
+
 // Tests that Table Row data is extracted correctly.
 TEST_P(PageContextWrapperTest, PopulatePageContext_RichExtraction_TableRow) {
   if (!IsRefactored()) {
@@ -3883,6 +3936,54 @@
   EXPECT_CHECK_DEATH([wrapper populatePageContextFieldsAsync]);
 }
 
+// Tests that the page context extracts text color with APCv2.
+TEST_P(PageContextWrapperTest, PopulatePageContext_RichExtraction_Text_Color) {
+  if (!IsRefactored()) {
+    return;
+  }
+
+  auto page_structure =
+      HtmlPage("RichExtraction_Text_Color",
+               RawHtml("<p style=\"color: rgb(0, 255, 0)\">Green Text</p>"));
+
+  std::string main_html = page_helper_->Build(page_structure);
+  web::test::LoadHtml(base::SysUTF8ToNSString(main_html),
+                      test_server_.GetURL(kMainPagePath), web_state());
+
+  PageContextWrapperConfig config =
+      PageContextWrapperConfigBuilder().SetUseRichExtraction(true).Build();
+
+  PageContextWrapperCallbackResponse response = RunPageContextWrapperWithConfig(
+      web_state(), config, ^(PageContextWrapper* wrapper) {
+        wrapper.shouldGetAnnotatedPageContent = YES;
+      });
+
+  ASSERT_TRUE(response.has_value());
+  std::unique_ptr<optimization_guide::proto::PageContext> page_context =
+      std::move(response.value());
+
+  ASSERT_TRUE(page_context);
+  ASSERT_TRUE(page_context->has_annotated_page_content());
+
+  const auto& annotated_page_content = page_context->annotated_page_content();
+  const auto& root_node = annotated_page_content.root_node();
+  ASSERT_EQ(root_node.children_nodes_size(), 1);
+
+  // Check Paragraph
+  const auto& p_node = root_node.children_nodes(0);
+  ASSERT_EQ(p_node.children_nodes_size(), 1);
+  const auto& p_text_node = p_node.children_nodes(0);
+  EXPECT_EQ(p_text_node.content_attributes().text_data().text_content(),
+            "Green Text");
+
+  // Check Color
+  // Green: (0, 255, 0) -> (0 << 24) | (255 << 16) | (0 << 8) | 255
+  // = 0 | 16711680 | 0 | 255 = 16711935
+  ASSERT_TRUE(p_text_node.content_attributes().text_data().has_text_style());
+  EXPECT_EQ(p_text_node.content_attributes().text_data().text_style().color(),
+            16711935u);
+}
+
 INSTANTIATE_TEST_SUITE_P(,
                          PageContextWrapperTest,
                          testing::Bool(),
diff --git a/ios/chrome/browser/intelligence/proto_wrappers/resources/annotated_page_content_extraction.ts b/ios/chrome/browser/intelligence/proto_wrappers/resources/annotated_page_content_extraction.ts
index 71f34fd..765daec1 100644
--- a/ios/chrome/browser/intelligence/proto_wrappers/resources/annotated_page_content_extraction.ts
+++ b/ios/chrome/browser/intelligence/proto_wrappers/resources/annotated_page_content_extraction.ts
@@ -70,6 +70,10 @@
 const TAG_H5 = 'H5';
 const TAG_H6 = 'H6';
 
+// Dialog tags and attributes.
+const TAG_DIALOG = 'DIALOG';
+const ATTRIBUTE_OPEN_DIALOG = 'open';
+
 // Tags that should be rejected during the DOM tree walk.
 const TAGS_TO_REJECT = [
   TAG_STYLE,
@@ -144,6 +148,10 @@
   isAdRelated: false,
 };
 
+// Type alias for accessing webkit-specific fullscreen document properties that
+// are not part of the standard Document interface.
+type WebkitDocument = Document&{webkitFullscreenElement?: Element};
+
 /**
  * Maps a tag name to its corresponding PageContentAnnotatedRole.
  *
@@ -359,6 +367,42 @@
 }
 
 /**
+ * Determines if an element is rendered in the top layer.
+ *
+ * @param element The element to check.
+ * @return True if the element is rendered in the top layer, false otherwise.
+ */
+function isRenderedInTopLayer(element: HTMLElement): boolean {
+  // 1. Open popovers. Use CSS.supports for browsers not supporting
+  // :popover-open.
+  if (CSS.supports('selector(:popover-open)') &&
+      element.matches(':popover-open')) {
+    return true;
+  }
+
+  // 2. Dialogs opened as modals.
+  if (CSS.supports('selector(:modal)')) {
+    if (element.matches(':modal')) {
+      return true;
+    }
+  } else {
+    // Fallback: check if it's an open dialog. Note: Without `:modal` support,
+    // we cannot distinguish between `dialog.show()` (normal document flow) and
+    // `dialog.showModal()` (top layer). This is a best-effort approximation.
+    if (element.tagName === TAG_DIALOG &&
+        element.hasAttribute(ATTRIBUTE_OPEN_DIALOG)) {
+      return true;
+    }
+  }
+
+  // 3. Fullscreen element.
+  const doc = element.ownerDocument;
+  return doc &&
+      (doc.fullscreenElement === element ||
+       (doc as WebkitDocument).webkitFullscreenElement === element);
+}
+
+/**
  * Extracts form specific content attributes from a given DOM element.
  *
  * @param form The form element to process.
@@ -375,7 +419,6 @@
   return formData;
 }
 
-
 // TODO(crbug.com/480945289): Complete this function as more data becomes
 // available throughout iterations.
 /**
@@ -417,9 +460,12 @@
     return true;
   }
 
-  // TODO(crbug.com/480945289): Add searches for Top/ViewTransitionLayer,
-  // InteractionInfo (when enabled), and Labels (when enabled).
+  if (isRenderedInTopLayer(element)) {
+    return true;
+  }
 
+  // TODO(crbug.com/480945289): Add searches for InteractionInfo (when enabled),
+  // and Labels (when enabled).
 
   // Scrollable elements act as containers because they create a new
   // visual context for their content, handling overflow.
@@ -484,6 +530,43 @@
 }
 
 /**
+ * Parses a CSS color string and returns it as a packed RGBA uint32.
+ * Supported formats: rgb(r, g, b), rgba(r, g, b, a).
+ * Returns null if the color string is invalid.
+ *
+ * @param colorString The CSS color string to parse.
+ * @return The packed color value ((r << 24) | (g << 16) | (b << 8) | a).
+ */
+function parseCssColor(colorString: string): number | undefined {
+  if (!colorString) {
+    return undefined;
+  }
+
+  // Handle rgb(r, g, b).
+  const rgbMatch = colorString.match(/^rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)$/);
+  if (rgbMatch) {
+    const r = parseInt(rgbMatch[1]!, 10);
+    const g = parseInt(rgbMatch[2]!, 10);
+    const b = parseInt(rgbMatch[3]!, 10);
+    // Fully opaque alpha = 255
+    return (((r & 0xFF) << 24) | ((g & 0xFF) << 16) | ((b & 0xFF) << 8) | 0xFF) >>> 0;
+  }
+
+  // Handle rgba(r, g, b, a).
+  const rgbaMatch =
+    colorString.match(/^rgba\(\s*(\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\s*\)$/);
+  if (rgbaMatch) {
+    const r = parseInt(rgbaMatch[1]!, 10);
+    const g = parseInt(rgbaMatch[2]!, 10);
+    const b = parseInt(rgbaMatch[3]!, 10);
+    const a = Math.round(parseFloat(rgbaMatch[4]!) * 255);
+    return (((r & 0xFF) << 24) | ((g & 0xFF) << 16) | ((b & 0xFF) << 8) | (a & 0xFF)) >>> 0;
+  }
+
+  return undefined;
+}
+
+/**
  * Applies text transform and masking to the text content.
  * Matches Blink's LayoutText::TransformedText() behavior.
  *
@@ -567,6 +650,8 @@
   const textSize = domNode.ownerDocument ?
     getTextSizeCategory(style.fontSize, domNode.ownerDocument) :
     PageContentTextSize.M;
+  const color = parseCssColor(style.color);
+
   return {
     attributeType: PageContentAttributeType.TEXT,
     annotatedRoles: [],
@@ -576,8 +661,7 @@
       textStyle: {
         textSize: textSize,
         hasEmphasis,
-        // TODO(crbug.com/474935853): Add text color extraction.
-        color: 0,
+        color,
       },
     },
   };
diff --git a/ios/chrome/browser/intelligence/proto_wrappers/resources/page_content_types.ts b/ios/chrome/browser/intelligence/proto_wrappers/resources/page_content_types.ts
index 9543e87..5b610b5 100644
--- a/ios/chrome/browser/intelligence/proto_wrappers/resources/page_content_types.ts
+++ b/ios/chrome/browser/intelligence/proto_wrappers/resources/page_content_types.ts
@@ -220,7 +220,7 @@
 export interface PageContentTextStyle {
   textSize: PageContentTextSize;
   hasEmphasis: boolean;
-  color: number;
+  color?: number;
 }
 
 export interface PageContentTextInfo {
diff --git a/ios/chrome/browser/policy/ui_bundled/idle/idle_timeout_policy_scene_agent.mm b/ios/chrome/browser/policy/ui_bundled/idle/idle_timeout_policy_scene_agent.mm
index db2b16db..39e9cf5 100644
--- a/ios/chrome/browser/policy/ui_bundled/idle/idle_timeout_policy_scene_agent.mm
+++ b/ios/chrome/browser/policy/ui_bundled/idle/idle_timeout_policy_scene_agent.mm
@@ -66,7 +66,7 @@
 
   // Service handling IdleTimeout and IdleTimeoutActions policies.
   // IdleTimeoutPolicySceneAgents observe this service.
-  raw_ptr<enterprise_idle::IdleService, DanglingUntriaged> _idleService;
+  raw_ptr<enterprise_idle::IdleService> _idleService;
 
   // Flag indicating whether this dialog is allowed to display the snackbar.
   // This is used to show the snackbar on the same scene that shows the timeout
@@ -106,6 +106,7 @@
   // Tear down objects tied to the scene state before it is deleted.
   [self tearDownObservers];
   _mainBrowser = nullptr;
+  _idleService = nullptr;
   [self stopIdleTimeoutConfirmationCoordinator];
 }
 
diff --git a/ios/chrome/browser/policy/ui_bundled/idle/idle_timeout_policy_scene_agent_unittest.mm b/ios/chrome/browser/policy/ui_bundled/idle/idle_timeout_policy_scene_agent_unittest.mm
index d6acbea..a2a1b450 100644
--- a/ios/chrome/browser/policy/ui_bundled/idle/idle_timeout_policy_scene_agent_unittest.mm
+++ b/ios/chrome/browser/policy/ui_bundled/idle/idle_timeout_policy_scene_agent_unittest.mm
@@ -94,7 +94,7 @@
   }
 
   void TearDown() override {
-    [agent_ sceneStateDidDisableUI:scene_state_];
+    scene_state_.UIEnabled = NO;
     [scene_state_ shutdown];
     scene_state_ = nil;
     PlatformTest::TearDown();
@@ -137,7 +137,7 @@
                     mainBrowser:browser];
 
     agent_.sceneState = scene_state_;
-    [agent_ sceneStateDidEnableUI:scene_state_];
+    scene_state_.UIEnabled = YES;
   }
 
  protected:
diff --git a/ios_internal b/ios_internal
index 7001a5d..bc858cb 160000
--- a/ios_internal
+++ b/ios_internal
@@ -1 +1 @@
-Subproject commit 7001a5dce04cd3827b70be178d4afc858dd49c1a
+Subproject commit bc858cb1047c92e4ed92fe0d82dc28f11b6be51c
diff --git a/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java b/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java
index fe5e178..2bae4f6 100644
--- a/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java
+++ b/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java
@@ -1647,12 +1647,7 @@
                 int extra,
                 byte @Nullable [] data) {
             if (drmSessionId == null) {
-                // Prior to Android M EVENT_PROVISION_REQUIRED was used to signify that provisioning
-                // was required before the session could be created. Unprovisioned errors are
-                // handled elsewhere, so no need to log a message.
-                if (event != MediaDrm.EVENT_PROVISION_REQUIRED) {
-                    Log.e(TAG, "EventListener: No session for event %d.", event);
-                }
+                Log.e(TAG, "EventListener: No session for event %d.", event);
                 return;
             }
 
@@ -1691,11 +1686,8 @@
                         return;
                     }
                     break;
-                case MediaDrm.EVENT_KEY_EXPIRED:
-                    Log.d(TAG, "MediaDrm.EVENT_KEY_EXPIRED for session %s", sessionId);
-                    break;
-                    // (b/271451225) This event is generated during ClearKey implementation in
-                    // Android.
+                // (b/271451225) This event is generated during ClearKey implementation in
+                // Android.
                 case MediaDrm.EVENT_VENDOR_DEFINED:
                     Log.d(TAG, "MediaDrm.EVENT_VENDOR_DEFINED for session %s", sessionId);
                     request =
diff --git a/media/base/media_switches.cc b/media/base/media_switches.cc
index 01bfb9bd..6d59a96d 100644
--- a/media/base/media_switches.cc
+++ b/media/base/media_switches.cc
@@ -1040,6 +1040,10 @@
 BASE_FEATURE(kContextMenuPictureInPictureAndroid,
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+// Enables fullscreen video Picture-in-Picture on Android.
+BASE_FEATURE(kFullscreenVideoPictureInPicture,
+             base::FEATURE_ENABLED_BY_DEFAULT);
+
 // Enables the use of a Surface (ANativeWindow) as the input for the
 // NdkVideoEncodeAccelerator on Android.
 BASE_FEATURE(kSurfaceInputForAndroidVEA, base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/media/base/media_switches.h b/media/base/media_switches.h
index 265ad0b..6802ade 100644
--- a/media/base/media_switches.h
+++ b/media/base/media_switches.h
@@ -364,12 +364,14 @@
 #if BUILDFLAG(IS_ANDROID)
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kAllowDelayedAudioFocusGainAndroid);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kAllowEnhancedPipTransition);
+MEDIA_EXPORT BASE_DECLARE_FEATURE(kAllowMediaCodecSoftwareDecoder);
+MEDIA_EXPORT BASE_DECLARE_FEATURE(kAndroidZeroCopyVideoCapture);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kAutoDocPiPPermissionPromptAndroid);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kAutoPictureInPictureAndroid);
-MEDIA_EXPORT BASE_DECLARE_FEATURE(kEnableAudioMonitoringOnAndroid);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kContextMenuPictureInPictureAndroid);
-MEDIA_EXPORT BASE_DECLARE_FEATURE(kSurfaceInputForAndroidVEA);
-MEDIA_EXPORT BASE_DECLARE_FEATURE(kAndroidZeroCopyVideoCapture);
+MEDIA_EXPORT BASE_DECLARE_FEATURE(kEnableAudioMonitoringOnAndroid);
+MEDIA_EXPORT BASE_DECLARE_FEATURE(kFullscreenVideoPictureInPicture);
+
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kMediaCodecBlockModel);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kMediaCodecLowDelayMode);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kMediaControlsExpandGesture);
@@ -379,10 +381,10 @@
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kMediaDrmGetStatusForPolicy);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kMediaDrmQueryInSeparateProcess);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kRequestSystemAudioFocus);
+MEDIA_EXPORT BASE_DECLARE_FEATURE(kSurfaceInputForAndroidVEA);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kUseAudioLatencyFromHAL);
-MEDIA_EXPORT BASE_DECLARE_FEATURE(kUseSecurityLevelWhenCheckingMediaDrmVersion);
-MEDIA_EXPORT BASE_DECLARE_FEATURE(kAllowMediaCodecSoftwareDecoder);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kUseAudioManagerMaxChannelLayout);
+MEDIA_EXPORT BASE_DECLARE_FEATURE(kUseSecurityLevelWhenCheckingMediaDrmVersion);
 #endif  // BUILDFLAG(IS_ANDROID)
 
 #if BUILDFLAG(USE_LINUX_VIDEO_ACCELERATION)
diff --git a/media/base/video_frame_layout.cc b/media/base/video_frame_layout.cc
index 3d6ff38f..6b88506 100644
--- a/media/base/video_frame_layout.cc
+++ b/media/base/video_frame_layout.cc
@@ -190,6 +190,10 @@
     return false;
   }
 
+  if (planes_.size() != VideoFrame::NumPlanes(format_)) {
+    return false;
+  }
+
   for (size_t plane_idx = 0; plane_idx < planes_.size(); ++plane_idx) {
     const auto& plane = planes_[plane_idx];
     if (plane.offset > data_size || plane.size > data_size) {
@@ -204,11 +208,19 @@
     }
 
     size_t rows = VideoFrame::Rows(plane_idx, format_, coded_size_.height());
-    // Offset + stride * rows: furthermost byte that can be reasonably read
-    // during copying or conversion of the plane.
-    auto read_end = base::CheckMul(plane.stride, rows) + plane.offset;
-    if (!read_end.IsValid() || read_end.ValueOrDie() > data_size) {
-      return false;
+    if (rows > 0) {
+      size_t row_bytes =
+          VideoFrame::RowBytes(plane_idx, format_, coded_size_.width());
+      if (plane.stride < row_bytes) {
+        return false;
+      }
+      // Offset + stride * (rows - 1) + row_bytes: furthermost byte that can be
+      // reasonably read during copying or conversion of the plane.
+      auto read_end =
+          base::CheckMul(plane.stride, rows - 1) + row_bytes + plane.offset;
+      if (!read_end.IsValid() || read_end.ValueOrDie() > data_size) {
+        return false;
+      }
     }
   }
 
diff --git a/media/base/video_frame_layout_unittest.cc b/media/base/video_frame_layout_unittest.cc
index e4351ab..68426c9 100644
--- a/media/base/video_frame_layout_unittest.cc
+++ b/media/base/video_frame_layout_unittest.cc
@@ -358,6 +358,30 @@
   ASSERT_TRUE(layout.has_value());
   EXPECT_FALSE(
       layout->FitsInContiguousBufferOfSize(std::numeric_limits<size_t>::max()));
+
+  // Validate exact footprint calculation (stride > row_bytes).
+  {
+    std::vector<ColorPlaneLayout> exact_planes(3);
+    exact_planes[0].stride = 384;  // row_bytes = 320, rows = 180
+    exact_planes[0].offset = 0;
+    exact_planes[0].size = 384 * 179 + 320;  // 69056
+
+    exact_planes[1].stride = 192;  // row_bytes = 160, rows = 90
+    exact_planes[1].offset = 69056;
+    exact_planes[1].size = 192 * 89 + 160;  // 17248
+
+    exact_planes[2].stride = 192;  // row_bytes = 160, rows = 90
+    exact_planes[2].offset = 69056 + 17248;
+    exact_planes[2].size = 192 * 89 + 160;  // 17248
+
+    auto exact_layout = VideoFrameLayout::CreateWithPlanes(
+        PIXEL_FORMAT_I420, coded_size, exact_planes);
+    ASSERT_TRUE(exact_layout.has_value());
+    size_t exact_data_size = exact_planes[2].offset + exact_planes[2].size;
+    EXPECT_TRUE(exact_layout->FitsInContiguousBufferOfSize(exact_data_size));
+    EXPECT_FALSE(
+        exact_layout->FitsInContiguousBufferOfSize(exact_data_size - 1));
+  }
 }
 
 }  // namespace media
diff --git a/media/filters/source_buffer_stream.cc b/media/filters/source_buffer_stream.cc
index 0a03920..ed142e20 100644
--- a/media/filters/source_buffer_stream.cc
+++ b/media/filters/source_buffer_stream.cc
@@ -11,6 +11,7 @@
 #include <string>
 
 #include "base/compiler_specific.h"
+#include "base/feature_list.h"
 #include "base/functional/bind.h"
 #include "base/trace_event/trace_event.h"
 #include "media/base/demuxer_memory_limit.h"
@@ -22,6 +23,9 @@
 
 namespace {
 
+// TODO(crbug.com/486351442): Kill-switch to be removed after M147 goes stable.
+BASE_FEATURE(kMergeRangesDuringAppend, base::FEATURE_ENABLED_BY_DEFAULT);
+
 // The minimum interbuffer decode timestamp delta (or buffer duration) for use
 // in fudge room for range membership, adjacency and coalescing.
 const int kMinimumInterbufferDistanceInMs = 1;
@@ -407,6 +411,9 @@
 
   SetSelectedRangeIfNeeded(next_buffer_timestamp);
 
+  if (base::FeatureList::IsEnabled(kMergeRangesDuringAppend)) {
+    MergeAllAdjacentRanges();
+  }
   DVLOG(1) << __func__ << " " << GetStreamTypeName()
            << ": done. ranges_=" << RangesToString(ranges_);
   DCHECK(IsRangeListSorted(ranges_));
@@ -2008,4 +2015,8 @@
   return true;
 }
 
+bool SourceBufferStream::IsRangeListSortedForTesting() const {
+  return IsRangeListSorted(ranges_);
+}
+
 }  // namespace media
diff --git a/media/filters/source_buffer_stream.h b/media/filters/source_buffer_stream.h
index b4dfa2f..f89ac7b 100644
--- a/media/filters/source_buffer_stream.h
+++ b/media/filters/source_buffer_stream.h
@@ -390,6 +390,8 @@
   // returns true.  Otherwise returns false.
   bool SetPendingBuffer(scoped_refptr<StreamParserBuffer>* out_buffer);
 
+  bool IsRangeListSortedForTesting() const;
+
   // Used to report log messages that can help the web developer figure out what
   // is wrong with the content.
   raw_ptr<MediaLog> media_log_;
diff --git a/media/filters/source_buffer_stream_unittest.cc b/media/filters/source_buffer_stream_unittest.cc
index acb5f1f..b154eeb 100644
--- a/media/filters/source_buffer_stream_unittest.cc
+++ b/media/filters/source_buffer_stream_unittest.cc
@@ -188,6 +188,8 @@
     stream_->OnStartOfCodedFrameGroup(start_timestamp);
   }
 
+  bool IsRangeListSorted() { return stream_->IsRangeListSortedForTesting(); }
+
   int GetRemovalRangeInMs(int start, int end, int bytes_to_free,
                           int* removal_end) {
     base::TimeDelta removal_end_timestamp = base::Milliseconds(*removal_end);
@@ -5701,4 +5703,48 @@
   SignalStartOfCodedFrameGroup(base::Milliseconds(1000));
 }
 
+TEST_F(SourceBufferStreamTest, OverlappingRangesAfterSplitAndEarlyReturn) {
+  // 1. Append F1 with huge duration.
+  // F1: PTS=1s, duration=25s. (Ends at 26s)
+  SignalStartOfCodedFrameGroup(base::Seconds(1));
+  BufferQueue buffers;
+  scoped_refptr<StreamParserBuffer> f1 =
+      StreamParserBuffer::CopyFrom(kDataA, true, DemuxerStream::VIDEO, 0);
+  f1->set_timestamp(base::Seconds(1));
+  f1->SetDecodeTimestamp(
+      DecodeTimestamp::FromPresentationTime(base::Seconds(1)));
+  f1->set_duration(base::Seconds(25));
+  buffers.push_back(f1);
+  stream_->Append(buffers);
+
+  // 2. Append F2 (keyframe) at PTS=20s.
+  // We append it in a way that it ends up in the same range.
+  // F1 is [1, 26). F2 is at 20.
+  buffers.clear();
+  scoped_refptr<StreamParserBuffer> f2 =
+      StreamParserBuffer::CopyFrom(kDataA, true, DemuxerStream::VIDEO, 0);
+  f2->set_timestamp(base::Seconds(20));
+  f2->SetDecodeTimestamp(
+      DecodeTimestamp::FromPresentationTime(base::Seconds(20)));
+  f2->set_duration(base::Seconds(1));
+  buffers.push_back(f2);
+  stream_->Append(buffers);
+
+  // 3. Append F3 (keyframe) at PTS=2s. This will trigger a split of the
+  // existing range.
+  SignalStartOfCodedFrameGroup(base::Seconds(2));
+  buffers.clear();
+  scoped_refptr<StreamParserBuffer> f3 =
+      StreamParserBuffer::CopyFrom(kDataA, true, DemuxerStream::VIDEO, 0);
+  f3->set_timestamp(base::Seconds(2));
+  f3->SetDecodeTimestamp(
+      DecodeTimestamp::FromPresentationTime(base::Seconds(2)));
+  f3->set_duration(base::Seconds(1));
+  buffers.push_back(f3);
+  stream_->Append(buffers);
+
+  // 4. Check if ranges are sorted.
+  EXPECT_TRUE(IsRangeListSorted());
+}
+
 }  // namespace media
diff --git a/media/filters/symphonia_glue_unittest.rs b/media/filters/symphonia_glue_unittest.rs
index 2b68e73..06d1a7a 100644
--- a/media/filters/symphonia_glue_unittest.rs
+++ b/media/filters/symphonia_glue_unittest.rs
@@ -41,9 +41,7 @@
     let actual_samples = data_u8.chunks_exact(bytes_per_sample as usize).collect::<Vec<_>>();
     expect_eq!(actual_samples.len(), expected_samples.len());
 
-    for (i, (expected, actual_bytes)) in
-        expected_samples.iter().zip(actual_samples.into_iter()).enumerate()
-    {
+    for (i, (expected, actual_bytes)) in expected_samples.iter().zip(actual_samples).enumerate() {
         let expected_bytes = expected.to_ne_bytes();
         expect_eq!(actual_bytes, expected_bytes.as_ref(), "Mismatch at index {i}");
     }
diff --git a/net/base/features.cc b/net/base/features.cc
index f6440b7..46abf5bf 100644
--- a/net/base/features.cc
+++ b/net/base/features.cc
@@ -772,4 +772,6 @@
 BASE_FEATURE(kDrainSpdySessionSynchronouslyOnRemoteEndpointDisconnect,
              base::FEATURE_ENABLED_BY_DEFAULT);
 
+BASE_FEATURE(kLogicalClearHttpCache, base::FEATURE_DISABLED_BY_DEFAULT);
+
 }  // namespace net::features
diff --git a/net/base/features.h b/net/base/features.h
index f97d54d6..8934c940 100644
--- a/net/base/features.h
+++ b/net/base/features.h
@@ -794,6 +794,11 @@
 NET_EXPORT BASE_DECLARE_FEATURE(kUseNSURLDataForGURLConversion);
 #endif  // BUILDFLAG(IS_APPLE)
 
+// Enables logical HTTP cache clearing, which adds a filter to the cache
+// to immediately treat entries as invalid, while they are physically deleted
+// in the background.
+NET_EXPORT BASE_DECLARE_FEATURE(kLogicalClearHttpCache);
+
 // If enabled, SPDY sessions will be synchronously drained when the underlying
 // transport socket is detected to be disconnected in GetRemoteEndpoint().
 NET_EXPORT BASE_DECLARE_FEATURE(
diff --git a/net/http/http_cache.cc b/net/http/http_cache.cc
index 81872a85..15145bb 100644
--- a/net/http/http_cache.cc
+++ b/net/http/http_cache.cc
@@ -1090,14 +1090,40 @@
 scoped_refptr<HttpCache::ActiveEntry> HttpCache::GetActiveEntry(
     const std::string& key) {
   auto it = active_entries_.find(key);
-  return it != active_entries_.end() ? base::WrapRefCounted(&it->second.get())
-                                     : nullptr;
+  if (it == active_entries_.end()) {
+    return nullptr;
+  }
+
+  scoped_refptr<ActiveEntry> entry = base::WrapRefCounted(&it->second.get());
+
+  // Check if the existing active entry has been logically invalidated.
+  // We only check opened entries because newly created (unopened) entries
+  // are guaranteed to be fresh and should not be invalidated by filters
+  // that were registered before their creation.
+  // This ensures that even if an entry is currently in use, a new request
+  // will treat it as a miss if a clear-data request just occurred.
+  if (entry->opened() && IsInvalidated(entry->GetEntry())) {
+    DoomEntry(key, nullptr);
+    return nullptr;
+  }
+  return entry;
 }
 
 scoped_refptr<HttpCache::ActiveEntry> HttpCache::ActivateEntry(
     disk_cache::Entry* disk_entry,
     bool opened) {
   DCHECK(!HasActiveEntry(disk_entry->GetKey()));
+
+  // Intercept entries as they are being activated from the disk backend.
+  // We only check 'opened' (existing) entries. Newly 'created' entries
+  // are bypassing the cache due to a miss and should not be invalidated.
+  // If they match an invalidation filter, we doom them immediately and
+  // return nullptr to signal a cache miss to the transaction.
+  if (opened && IsInvalidated(disk_entry)) {
+    disk_entry->Doom();
+    return nullptr;
+  }
+
   return base::MakeRefCounted<ActiveEntry>(weak_factory_.GetWeakPtr(),
                                            disk_entry, opened);
 }
@@ -1658,6 +1684,13 @@
       DCHECK(pending_op->entry);
       key = pending_op->entry->GetKey();
       entry = ActivateEntry(pending_op->entry, pending_op->entry_opened);
+      if (!entry) {
+        // Entry was invalidated.
+        result = ERR_CACHE_RACE;
+        try_restart_requests = true;
+        pending_op->entry.ExtractAsDangling()->Close();
+        pending_op->entry = nullptr;
+      }
     } else {
       // The writer transaction is gone.
       if (!pending_op->entry_opened) {
@@ -1857,4 +1890,45 @@
   }
 }
 
+HttpCache::InvalidationFilter::InvalidationFilter() = default;
+HttpCache::InvalidationFilter::~InvalidationFilter() = default;
+HttpCache::InvalidationFilter::InvalidationFilter(const InvalidationFilter&) =
+    default;
+HttpCache::InvalidationFilter& HttpCache::InvalidationFilter::operator=(
+    const InvalidationFilter&) = default;
+
+bool HttpCache::InvalidationFilter::Matches(
+    const GURL& url,
+    const disk_cache::Entry* entry) const {
+  if (entry->GetLastUsed() < begin_time || entry->GetLastUsed() >= end_time) {
+    return false;
+  }
+
+  return DoesUrlMatchFilter(filter_type, origins, domains, url);
+}
+
+void HttpCache::AddInvalidationFilter(InvalidationFilter filter) {
+  invalidation_filters_.push_back(std::move(filter));
+}
+
+bool HttpCache::IsInvalidated(disk_cache::Entry* entry) {
+  if (!base::FeatureList::IsEnabled(features::kLogicalClearHttpCache) ||
+      invalidation_filters_.empty()) {
+    return false;
+  }
+
+  std::string url_str = GetResourceURLFromHttpCacheKey(entry->GetKey());
+  GURL url(url_str);
+  if (!url.is_valid()) {
+    return false;
+  }
+
+  for (const auto& filter : invalidation_filters_) {
+    if (filter.Matches(url, entry)) {
+      return true;
+    }
+  }
+  return false;
+}
+
 }  // namespace net
diff --git a/net/http/http_cache.h b/net/http/http_cache.h
index dc797447..ad4d18b0 100644
--- a/net/http/http_cache.h
+++ b/net/http/http_cache.h
@@ -50,6 +50,8 @@
 #include "net/http/http_transaction_factory.h"
 #include "net/http/no_vary_search_cache.h"
 #include "net/http/no_vary_search_cache_storage.h"
+#include "url/gurl.h"
+#include "url/origin.h"
 
 class GURL;
 
@@ -263,6 +265,38 @@
                               base::Time delete_begin,
                               base::Time delete_end);
 
+  // InvalidationFilter represents a request to logically clear parts of the
+  // cache. Instead of waiting for slow disk deletion, any entry matching
+  // these criteria is treated as a cache miss.
+  struct NET_EXPORT InvalidationFilter {
+    InvalidationFilter();
+    ~InvalidationFilter();
+    InvalidationFilter(const InvalidationFilter&);
+    InvalidationFilter& operator=(const InvalidationFilter&);
+
+    // Checks if the given cache entry matches this filter's criteria
+    // (time bounds and URL).
+    bool Matches(const GURL& url, const disk_cache::Entry* entry) const;
+
+    // The time range of entries to invalidate based on their 'LastUsed' time.
+    base::Time begin_time;
+    base::Time end_time;
+
+    // Filter type (e.g., exclude vs include) and the specific origins/domains.
+    UrlFilterType filter_type;
+    base::flat_set<url::Origin> origins;
+    base::flat_set<std::string> domains;
+  };
+
+  // Adds a filter to the logical invalidation list. Any subsequent access
+  // to an entry matching this filter will result in a cache miss.
+  void AddInvalidationFilter(InvalidationFilter filter);
+
+  // Orchestrator for invalidation checks. This provides a fast-path bailout
+  // when no filters are active, decodes the URL from the cache key exactly once,
+  // and iterates over all active filters to see if any match the given entry.
+  bool IsInvalidated(disk_cache::Entry* entry);
+
   // Causes all transactions created after this point to simulate lock timeout
   // and effectively bypass the cache lock whenever there is lock contention.
   void SimulateCacheLockTimeoutForTesting() { bypass_lock_for_test_ = true; }
@@ -832,6 +866,9 @@
   // The set of active entries indexed by cache key.
   ActiveEntriesMap active_entries_;
 
+  // The set of invalidation filters.
+  std::vector<InvalidationFilter> invalidation_filters_;
+
   // The set of doomed entries.
   ActiveEntriesSet doomed_entries_;
 
diff --git a/net/http/http_cache_unittest.cc b/net/http/http_cache_unittest.cc
index 46548c7e..41004188 100644
--- a/net/http/http_cache_unittest.cc
+++ b/net/http/http_cache_unittest.cc
@@ -15429,6 +15429,7 @@
   histogram_tester.ExpectBucketCount("HttpCache.CreateBackendEarly", true, 1);
 }
 
+
 // Tests that encoded body size is preserved across cache reads.
 // When a shared dictionary compressed response is fetched from the network,
 // the encoded (on-the-wire) body size is stored in the cached response info.
@@ -15538,4 +15539,39 @@
   }
 }
 
+TEST_F(HttpCacheTest, InvalidationFilter) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(net::features::kLogicalClearHttpCache);
+
+  MockHttpCache cache;
+
+  // 1. Add an entry to the cache.
+  MockTransaction transaction(kSimpleGET_Transaction);
+  RunTransactionTest(cache.http_cache(), transaction);
+  EXPECT_EQ(1, cache.network_layer()->transaction_count());
+
+  // 2. Verify it's in the cache.
+  RunTransactionTest(cache.http_cache(), transaction);
+  EXPECT_EQ(1, cache.network_layer()->transaction_count());
+
+  // 3. Add an invalidation filter for this origin.
+  HttpCache::InvalidationFilter filter;
+  filter.begin_time = base::Time();
+  filter.end_time = base::Time::Max();
+  filter.filter_type = UrlFilterType::kTrueIfMatches;
+  filter.origins.insert(url::Origin::Create(GURL(transaction.url)));
+  cache.http_cache()->AddInvalidationFilter(std::move(filter));
+
+  // 4. Try to read the entry -> should be a miss (network access).
+  // Note: transaction_count increases more than once due to internal restarts
+  // when an entry is invalidated during the activation phase.
+  RunTransactionTest(cache.http_cache(), transaction);
+  EXPECT_GT(cache.network_layer()->transaction_count(), 1);
+  int count_after_miss = cache.network_layer()->transaction_count();
+
+  // 5. Try again. It will still be a miss because our filter is for all time.
+  RunTransactionTest(cache.http_cache(), transaction);
+  EXPECT_GT(cache.network_layer()->transaction_count(), count_after_miss);
+}
+
 }  // namespace net
diff --git a/remoting/codec/audio_encoder_opus.cc b/remoting/codec/audio_encoder_opus.cc
index 1167daa..249cd73 100644
--- a/remoting/codec/audio_encoder_opus.cc
+++ b/remoting/codec/audio_encoder_opus.cc
@@ -28,6 +28,11 @@
 constexpr AudioPacket::SamplingRate kOpusSamplingRate =
     AudioPacket::SAMPLING_RATE_48000;
 
+// If not using `kOpusSampleRate`, the only other input sample rate we accept is
+// 44.1kHz.
+constexpr AudioPacket::SamplingRate kAltSamplingRate =
+    AudioPacket::SAMPLING_RATE_44100;
+
 // Opus supports frame sizes of 2.5, 5, 10, 20, 40 and 60 ms. We use 20 ms
 // frames to balance latency and efficiency.
 constexpr base::TimeDelta kFrameDuration = base::Milliseconds(20);
@@ -40,18 +45,122 @@
 constexpr AudioPacket::BytesPerSample kBytesPerSample =
     AudioPacket::BYTES_PER_SAMPLE_2;
 
+// Size in frames of the resampler's fill requests.
+constexpr size_t kResamplerRequestSize =
+    media::SincResampler::kDefaultRequestSize;
+
 constexpr bool IsSupportedSampleRate(int rate) {
   return rate == 44100 || rate == 48000;
 }
 
 }  // namespace
 
+class ResamplerFifoImpl : public AudioEncoderOpus::ResamplerFifo {
+ public:
+  explicit ResamplerFifoImpl(size_t size_in_frames, size_t channels)
+      : chunk_size_(kResamplerRequestSize * channels),
+        storage_buffer_(
+            base::AlignedUninit<int16_t>(size_in_frames * channels,
+                                         media::AudioBus::kChannelAlignment)),
+        crossover_buffer_(
+            base::AlignedUninit<int16_t>(chunk_size_,
+                                         media::AudioBus::kChannelAlignment)) {}
+  ~ResamplerFifoImpl() override = default;
+
+  void AddNewSamples(base::span<const int16_t> samples) override {
+    CHECK(new_samples_.empty());
+    new_samples_ = samples;
+  }
+
+  [[nodiscard]] base::span<const int16_t> TakeChunk() override {
+    CHECK_GE(remaining_samples(), chunk_size_);
+
+    if (saved_samples_.empty()) {
+      return new_samples_.take_first(chunk_size_);
+    }
+
+    if (saved_samples_.size() > chunk_size_) {
+      return saved_samples_.take_first(chunk_size_);
+    }
+
+    // If we're here, we have to combine some of the previous samples with the
+    // new ones. To return a continuous span, we have to make a temporary copy
+    // into `crossover_buffer_`.
+    const size_t new_samples_needed = chunk_size_ - saved_samples_.size();
+    auto [saved_dest, new_dest] =
+        base::span(crossover_buffer_).split_at(saved_samples_.size());
+
+    saved_dest.copy_from_nonoverlapping(saved_samples_);
+    saved_samples_ = {};
+
+    new_dest.copy_from_nonoverlapping(
+        new_samples_.take_first(new_samples_needed));
+
+    // Return merged samples.
+    return crossover_buffer_;
+  }
+
+  void SaveNewSamples() override {
+    CHECK_LE(remaining_samples(), storage_buffer_.size());
+
+    // No other saved samples. Copy directly.
+    if (saved_samples_.empty()) {
+      auto save_dest = storage_buffer_.first(new_samples_.size());
+      save_dest.copy_from_nonoverlapping(new_samples_);
+      new_samples_ = {};
+      saved_samples_ = save_dest;
+      return;
+    }
+
+    // There are some saved samples remaining. Copy them to the front of
+    // `storage_buffer_` first, and then append the new samples.
+    const size_t total_size = remaining_samples();
+    auto storage = storage_buffer_.first(total_size);
+    auto [previous_dest, new_dest] = storage.split_at(saved_samples_.size());
+
+    previous_dest.copy_from(saved_samples_);
+    new_dest.copy_from_nonoverlapping(new_samples_);
+
+    new_samples_ = {};
+    saved_samples_ = storage;
+  }
+
+  size_t remaining_samples() const override {
+    return saved_samples_.size() + new_samples_.size();
+  }
+
+  size_t GetChunkSizeForTesting() const override { return chunk_size_; }
+
+ private:
+  const size_t chunk_size_;
+
+  // Location where `new_samples_` are copied, during `SaveNewSamples()`;
+  base::AlignedHeapArray<int16_t> storage_buffer_;
+
+  // Temporary location where `saved_samples_` and `new_samples_` are stored,
+  // when there aren't enough saved samples to completely fill one chunk.
+  base::AlignedHeapArray<int16_t> crossover_buffer_;
+
+  // Portion of `storage_buffer_` which contains unused samples.
+  base::raw_span<const int16_t> saved_samples_;
+
+  // Points towards external, unowned memory.
+  base::raw_span<const int16_t> new_samples_;
+};
+
 AudioEncoderOpus::AudioEncoderOpus() = default;
 
 AudioEncoderOpus::~AudioEncoderOpus() {
   DestroyEncoder();
 }
 
+// static
+std::unique_ptr<AudioEncoderOpus::ResamplerFifo>
+AudioEncoderOpus::GetEmptyFifoForTesting(size_t size_in_frames,
+                                         size_t channels) {
+  return std::make_unique<ResamplerFifoImpl>(size_in_frames, channels);
+}
+
 void AudioEncoderOpus::InitEncoder() {
   DCHECK(!encoder_);
   int error;
@@ -64,30 +173,31 @@
 
   opus_encoder_ctl(encoder_.get(), OPUS_SET_BITRATE(kOutputBitrateBps));
 
-  frame_size_ =
-      media::AudioTimestampHelper::TimeToFrames(kFrameDuration, sampling_rate_);
+  needs_resampling_ = sampling_rate_ != kOpusSamplingRate;
 
-  if (sampling_rate_ != kOpusSamplingRate) {
-    size_t total_samples =
-        base::CheckMul(kOpusFrameCount, channels_).ValueOrDie<size_t>();
-    resample_buffer_ = base::AlignedUninit<int16_t>(
-        total_samples, media::AudioBus::kChannelAlignment);
-    // TODO(sergeyu): Figure out the right buffer size to use per packet instead
-    // of using media::SincResampler::kDefaultRequestSize.
+  // Drop any previous samples.
+  leftover_encoder_samples_ = {};
+
+  if (needs_resampling_) {
+    CHECK_EQ(sampling_rate_, kAltSamplingRate);
     resampler_ = std::make_unique<media::MultiChannelResampler>(
         channels_, sampling_rate_ / double{kOpusSamplingRate},
-        media::SincResampler::kDefaultRequestSize,
+        kResamplerRequestSize,
         base::BindRepeating(&AudioEncoderOpus::FetchBytesToResample,
                             base::Unretained(this)));
+
+    const size_t min_input_frames_needed =
+        resampler_->GetMaxInputFramesRequested(kOpusFrameCount);
+
+    resampling_samples_needed_ = min_input_frames_needed * channels_;
+
+    resampler_fifo_ = std::make_unique<ResamplerFifoImpl>(
+        resampling_samples_needed_, channels_);
     resampler_bus_ = media::AudioBus::Create(channels_, kOpusFrameCount);
   }
 
-  // Drop leftover data because it's for different sampling rate.
-  leftover_frames_ = 0;
-  leftover_samples_size_in_frames_ =
-      frame_size_ + media::SincResampler::kDefaultRequestSize;
-  leftover_samples_.reset(
-      new int16_t[leftover_samples_size_in_frames_ * channels_]);
+  encoder_samples_needed_ = kOpusFrameCount * channels_;
+  encoder_input_ = base::AlignedUninit<int16_t>(encoder_samples_needed_);
 }
 
 void AudioEncoderOpus::DestroyEncoder() {
@@ -123,17 +233,12 @@
 
 void AudioEncoderOpus::FetchBytesToResample(int resampler_frame_delay,
                                             media::AudioBus* audio_bus) {
-  DCHECK(resampling_data_);
-  int samples_left = (resampling_data_size_ - resampling_data_pos_) /
-                     kBytesPerSample / channels_;
-  DCHECK_LE(audio_bus->frames(), samples_left);
+  CHECK(needs_resampling_);
+  CHECK_GE(resampler_fifo_->remaining_samples(),
+           static_cast<size_t>(audio_bus->frames() * channels_));
   static_assert(kBytesPerSample == 2, "FromInterleaved expects 2 bytes.");
   audio_bus->FromInterleaved<media::SignedInt16SampleTypeTraits>(
-      reinterpret_cast<const int16_t*>(
-          UNSAFE_TODO(resampling_data_ + resampling_data_pos_)),
-      audio_bus->frames());
-  resampling_data_pos_ += audio_bus->frames() * kBytesPerSample * channels_;
-  DCHECK_LE(resampling_data_pos_, static_cast<int>(resampling_data_size_));
+      resampler_fifo_->TakeChunk());
 }
 
 int AudioEncoderOpus::GetBitrate() {
@@ -151,90 +256,91 @@
     return nullptr;
   }
 
-  int frames_in_packet = packet->data(0).size() / kBytesPerSample / channels_;
-  const int16_t* next_sample =
-      UNSAFE_TODO(reinterpret_cast<const int16_t*>(packet->data(0).data()));
+  base::span<const uint8_t> byte_input = base::as_byte_span(packet->data(0));
+  CHECK_EQ(byte_input.size() % kBytesPerSample, 0u);
+  CHECK(base::IsAligned(byte_input.data(), sizeof(int16_t)));
 
+  // SAFETY: This data is coming from an external source, but we've CHECK'ed
+  // that there are the right number of bytes, and that they have the proper
+  // alignment.
+  base::span<const int16_t> input_samples = UNSAFE_BUFFERS(
+      base::span(reinterpret_cast<const int16_t*>(byte_input.data()),
+                 byte_input.size() / sizeof(int16_t)));
+
+  if (needs_resampling_) {
+    return EncodeInternalWithResampling(input_samples);
+  }
+
+  return EncodeInternal(input_samples);
+}
+
+bool AudioEncoderOpus::EncodeData(base::span<const int16_t> samples,
+                                  AudioPacket* destination) {
+  CHECK_EQ(samples.size(), encoder_samples_needed_);
+
+  // Initialize output buffer.
+  std::string* data = destination->add_data();
+  data->resize(encoder_samples_needed_ * kBytesPerSample);
+
+  // Encode.
+  unsigned char* buffer = reinterpret_cast<unsigned char*>(std::data(*data));
+  int result = opus_encode(encoder_, samples.data(), kOpusFrameCount, buffer,
+                           data->length());
+  if (result < 0) {
+    LOG(ERROR) << "opus_encode() failed with error code: " << result;
+    return false;
+  }
+
+  CHECK_LE(result, static_cast<int>(data->length()));
+  data->resize(result);
+  return true;
+}
+
+std::unique_ptr<AudioPacket> AudioEncoderOpus::CreatePacket() {
   // Create a new packet of encoded data.
   auto encoded_packet = std::make_unique<AudioPacket>();
   encoded_packet->set_encoding(AudioPacket::ENCODING_OPUS);
   encoded_packet->set_sampling_rate(kOpusSamplingRate);
   encoded_packet->set_channels(channels_);
+  return encoded_packet;
+}
 
-  const int prefetch_frames =
-      resampler_.get() ? media::SincResampler::kDefaultRequestSize : 0;
-  int frames_wanted = frame_size_ + prefetch_frames;
+std::unique_ptr<AudioPacket> AudioEncoderOpus::EncodeInternal(
+    base::span<const int16_t> input_samples) {
+  CHECK(!needs_resampling_);
 
-  while (leftover_frames_ + frames_in_packet >= frames_wanted) {
-    const int16_t* pcm_buffer = nullptr;
+  // Create a new packet of encoded data.
+  auto encoded_packet = CreatePacket();
 
-    // Combine the packet with the leftover samples, if any.
-    if (leftover_frames_ > 0) {
-      pcm_buffer = leftover_samples_.get();
-      const int frames_to_copy = frames_wanted - leftover_frames_;
-      UNSAFE_TODO(memcpy(leftover_samples_.get() + leftover_frames_ * channels_,
-                         next_sample,
-                         frames_to_copy * kBytesPerSample * channels_));
-    } else {
-      pcm_buffer = next_sample;
+  while (leftover_encoder_samples_.size() + input_samples.size() >=
+         encoder_samples_needed_) {
+    // If there are no leftover frames, encode directly.
+    if (leftover_encoder_samples_.empty()) {
+      EncodeData(input_samples.take_first(encoder_samples_needed_),
+                 encoded_packet.get());
+      continue;
     }
 
-    // Resample data if necessary.
-    int frames_consumed = 0;
-    if (resampler_.get()) {
-      resampling_data_ = reinterpret_cast<const char*>(pcm_buffer);
-      resampling_data_pos_ = 0;
-      resampling_data_size_ = frames_wanted * channels_ * kBytesPerSample;
-      resampler_->Resample(kOpusFrameCount, resampler_bus_.get());
-      resampling_data_ = nullptr;
-      frames_consumed = resampling_data_pos_ / channels_ / kBytesPerSample;
+    // Fill `encoder_input_` completely.
+    const size_t free_space_size =
+        encoder_samples_needed_ - leftover_encoder_samples_.size();
 
-      static_assert(kBytesPerSample == 2, "ToInterleaved expects 2 bytes.");
-      resampler_bus_->ToInterleaved<media::SignedInt16SampleTypeTraits>(
-          resample_buffer_);
-      pcm_buffer = resample_buffer_.data();
-    } else {
-      frames_consumed = frame_size_;
-    }
+    encoder_input_.subspan(leftover_encoder_samples_.size())
+        .copy_from_nonoverlapping(input_samples.take_first(free_space_size));
 
-    // Initialize output buffer.
-    std::string* data = encoded_packet->add_data();
-    data->resize(kOpusFrameCount * kBytesPerSample * channels_);
-
-    // Encode.
-    unsigned char* buffer = reinterpret_cast<unsigned char*>(std::data(*data));
-    int result = opus_encode(encoder_, pcm_buffer, kOpusFrameCount, buffer,
-                             data->length());
-    if (result < 0) {
-      LOG(ERROR) << "opus_encode() failed with error code: " << result;
-      return nullptr;
-    }
-
-    DCHECK_LE(result, static_cast<int>(data->length()));
-    data->resize(result);
-
-    // Cleanup leftover buffer.
-    if (frames_consumed >= leftover_frames_) {
-      frames_consumed -= leftover_frames_;
-      leftover_frames_ = 0;
-      UNSAFE_TODO(next_sample += frames_consumed * channels_);
-      frames_in_packet -= frames_consumed;
-    } else {
-      leftover_frames_ -= frames_consumed;
-      UNSAFE_TODO(memmove(leftover_samples_.get(),
-                          leftover_samples_.get() + frames_consumed * channels_,
-                          leftover_frames_ * channels_ * kBytesPerSample));
-    }
+    // Encode the samples. All clean leftovers, as they already encoded.
+    EncodeData(encoder_input_, encoded_packet.get());
+    leftover_encoder_samples_ = {};
   }
 
-  // Store the leftover samples.
-  if (frames_in_packet > 0) {
-    DCHECK_LE(leftover_frames_ + frames_in_packet,
-              leftover_samples_size_in_frames_);
-    UNSAFE_TODO(memmove(leftover_samples_.get() + leftover_frames_ * channels_,
-                        next_sample,
-                        frames_in_packet * kBytesPerSample * channels_));
-    leftover_frames_ += frames_in_packet;
+  // Copy unused samples into `encoder_input_`.
+  if (!input_samples.empty()) {
+    CHECK_LT(input_samples.size(), encoder_samples_needed_);
+    const size_t used_space = leftover_encoder_samples_.size();
+    encoder_input_.subspan(used_space, input_samples.size())
+        .copy_from_nonoverlapping(input_samples);
+    leftover_encoder_samples_ =
+        encoder_input_.first(used_space + input_samples.size());
   }
 
   // Return nullptr if there's nothing in the packet.
@@ -245,4 +351,38 @@
   return encoded_packet;
 }
 
+std::unique_ptr<AudioPacket> AudioEncoderOpus::EncodeInternalWithResampling(
+    base::span<const int16_t> input_samples) {
+  CHECK(needs_resampling_);
+  CHECK(resampler_fifo_);
+
+  // We always encode the full `encoder_samples_needed_` samples in
+  // `encoder_input_` at once. Leftover samples are stored in `resampling_fifo_`
+  // instead, at their original `kAltSamplingRate`.
+  CHECK(leftover_encoder_samples_.empty());
+
+  // Create a new packet of encoded data.
+  auto encoded_packet = CreatePacket();
+
+  // Add a reference to the incoming samples without copying them.
+  resampler_fifo_->AddNewSamples(input_samples);
+
+  while (resampler_fifo_->remaining_samples() >= resampling_samples_needed_) {
+    resampler_->Resample(kOpusFrameCount, resampler_bus_.get());
+    resampler_bus_->ToInterleaved<media::SignedInt16SampleTypeTraits>(
+        encoder_input_);
+    EncodeData(encoder_input_, encoded_packet.get());
+  }
+
+  // Save unused samples.
+  resampler_fifo_->SaveNewSamples();
+
+  // Return nullptr if there's nothing in the packet.
+  if (encoded_packet->data_size() == 0) {
+    return nullptr;
+  }
+
+  return encoded_packet;
+}
+
 }  // namespace remoting
diff --git a/remoting/codec/audio_encoder_opus.h b/remoting/codec/audio_encoder_opus.h
index b048334..d0b90fe 100644
--- a/remoting/codec/audio_encoder_opus.h
+++ b/remoting/codec/audio_encoder_opus.h
@@ -7,6 +7,7 @@
 
 #include "base/memory/aligned_memory.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_span.h"
 #include "remoting/codec/audio_encoder.h"
 #include "remoting/proto/audio.pb.h"
 
@@ -23,6 +24,28 @@
 
 class AudioEncoderOpus : public AudioEncoder {
  public:
+  // Helper class which segments samples into chunks, while minimizing copies.
+  // Exposed as an interface here for ease of testing.
+  class ResamplerFifo {
+   public:
+    virtual ~ResamplerFifo() = default;
+
+    // Add samples to the FIFO, without copying them.
+    virtual void AddNewSamples(base::span<const int16_t> samples) = 0;
+
+    // Copies unused samples added by `AddNewSamples()` to internal storage.
+    virtual void SaveNewSamples() = 0;
+
+    // Consumes samples from the FIFO.
+    virtual base::span<const int16_t> TakeChunk() = 0;
+
+    // Returns the number of samples currently in the FIFO, saved or not.
+    virtual size_t remaining_samples() const = 0;
+
+    // Returns the size of each chunk returned by `TakeChunk()`.
+    virtual size_t GetChunkSizeForTesting() const = 0;
+  };
+
   AudioEncoderOpus();
 
   AudioEncoderOpus(const AudioEncoderOpus&) = delete;
@@ -35,32 +58,53 @@
       std::unique_ptr<AudioPacket> packet) override;
   int GetBitrate() override;
 
+  static std::unique_ptr<ResamplerFifo> GetEmptyFifoForTesting(
+      size_t size_in_frames,
+      size_t channels);
+
  private:
   void InitEncoder();
   void DestroyEncoder();
   bool ResetForPacket(AudioPacket* packet);
 
+  std::unique_ptr<AudioPacket> CreatePacket();
+
+  std::unique_ptr<AudioPacket> EncodeInternal(base::span<const int16_t> data);
+  std::unique_ptr<AudioPacket> EncodeInternalWithResampling(
+      base::span<const int16_t> data);
+
   void FetchBytesToResample(int resampler_frame_delay,
                             media::AudioBus* audio_bus);
 
+  bool EncodeData(base::span<const int16_t> data, AudioPacket* destination);
+
+  bool needs_resampling_ = false;
+
+  // Holds samples (always at 48kHz) that have not yet been encoded.
+  base::AlignedHeapArray<int16_t> encoder_input_;
+
+  // The portion of `encoder_input_` which contains samples that have not yet
+  // been encoded.
+  // Unused when `needs_resampling_` is true, since extra samples will be stored
+  // in `resampler_fifo_` instead.
+  base::raw_span<int16_t> leftover_encoder_samples_;
+
+  // Number of samples needed to encode a single "Opus frame".
+  size_t encoder_samples_needed_ = 0;
+
+  // Manages samples which have not been resampled yet.
+  std::unique_ptr<ResamplerFifo> resampler_fifo_;
+
+  // The minimum number of samples needed to guarantee to have enough for
+  // one resample call.
+  size_t resampling_samples_needed_ = 0;
+
   int sampling_rate_ = 0;
   AudioPacket::Channels channels_ = AudioPacket::CHANNELS_STEREO;
   raw_ptr<OpusEncoder, DanglingUntriaged> encoder_ = nullptr;
 
-  int frame_size_ = 0;
   std::unique_ptr<media::MultiChannelResampler> resampler_;
-  base::AlignedHeapArray<int16_t> resample_buffer_;
   std::unique_ptr<media::AudioBus> resampler_bus_;
-
-  // Used to pass packet to the FetchBytesToResampler() callback.
-  const char* resampling_data_ = nullptr;
-  int resampling_data_size_ = 0;
-  int resampling_data_pos_ = 0;
-
-  // Left-over unencoded samples from the previous AudioPacket.
-  std::unique_ptr<int16_t[]> leftover_samples_;
-  int leftover_samples_size_in_frames_ = 0;
-  int leftover_frames_ = 0;
 };
 
 }  // namespace remoting
diff --git a/remoting/codec/audio_encoder_opus_unittest.cc b/remoting/codec/audio_encoder_opus_unittest.cc
index 0e50ab86..055b7f45 100644
--- a/remoting/codec/audio_encoder_opus_unittest.cc
+++ b/remoting/codec/audio_encoder_opus_unittest.cc
@@ -24,8 +24,6 @@
 // Maximum value that can be encoded in a 16-bit signed sample.
 const int kMaxSampleValue = 32767;
 
-const int kChannels = 2;
-
 // Phase shift between left and right channels.
 const double kChannelPhaseShift = 2 * std::numbers::pi / 3;
 
@@ -71,31 +69,34 @@
   std::unique_ptr<AudioPacket> CreatePacket(int samples,
                                             AudioPacket::SamplingRate rate,
                                             double frequency_hz,
-                                            int pos) {
-    std::vector<int16_t> data(samples * kChannels);
+                                            int pos,
+                                            int channels) {
+    std::vector<int16_t> data(samples * channels);
     for (int i = 0; i < samples; ++i) {
-      data[i * kChannels] = GetSampleValue(rate, frequency_hz, i + pos, 0);
-      data[i * kChannels + 1] = GetSampleValue(rate, frequency_hz, i + pos, 1);
+      for (int j = 0; j < channels; ++j) {
+        data[i * channels + j] = GetSampleValue(rate, frequency_hz, i + pos, j);
+      }
     }
 
     std::unique_ptr<AudioPacket> packet(new AudioPacket());
-    packet->add_data(reinterpret_cast<char*>(&(data[0])),
-                     samples * kChannels * sizeof(int16_t));
+    packet->add_data(reinterpret_cast<char*>(data.data()),
+                     samples * channels * sizeof(int16_t));
     packet->set_encoding(AudioPacket::ENCODING_RAW);
     packet->set_sampling_rate(rate);
     packet->set_bytes_per_sample(AudioPacket::BYTES_PER_SAMPLE_2);
-    packet->set_channels(AudioPacket::CHANNELS_STEREO);
+    packet->set_channels(static_cast<AudioPacket::Channels>(channels));
     return packet;
   }
 
   // Decoded data is normally shifted in phase relative to the original signal.
   // This function returns the approximate shift in samples by finding the first
   // point when signal goes from negative to positive.
-  double EstimateSignalShift(const std::vector<int16_t>& received_data) {
+  double EstimateSignalShift(const std::vector<int16_t>& received_data,
+                             int channels) {
     for (size_t i = kSkippedFirstSamples;
-         i < received_data.size() / kChannels - 1; i++) {
-      int16_t this_sample = received_data[i * kChannels];
-      int16_t next_sample = received_data[(i + 1) * kChannels];
+         i < received_data.size() / channels - 1; i++) {
+      int16_t this_sample = received_data[i * channels];
+      int16_t next_sample = received_data[(i + 1) * channels];
       if (this_sample < 0 && next_sample > 0) {
         return i +
                static_cast<double>(-this_sample) / (next_sample - this_sample);
@@ -110,17 +111,17 @@
   void ValidateReceivedData(int samples,
                             AudioPacket::SamplingRate rate,
                             double frequency_hz,
-                            const std::vector<int16_t>& received_data) {
-    double shift = EstimateSignalShift(received_data);
+                            const std::vector<int16_t>& received_data,
+                            int channels) {
+    double shift = EstimateSignalShift(received_data, channels);
     double diff_sqare_sum = 0;
-    for (size_t i = kSkippedFirstSamples; i < received_data.size() / kChannels;
+    for (size_t i = kSkippedFirstSamples; i < received_data.size() / channels;
          i++) {
-      double d = received_data[i * kChannels] -
-                 GetSampleValue(rate, frequency_hz, i - shift, 0);
-      diff_sqare_sum += d * d;
-      d = received_data[i * kChannels + 1] -
-          GetSampleValue(rate, frequency_hz, i - shift, 1);
-      diff_sqare_sum += d * d;
+      for (int j = 0; j < channels; ++j) {
+        double d = received_data[i * channels + j] -
+                   GetSampleValue(rate, frequency_hz, i - shift, j);
+        diff_sqare_sum += d * d;
+      }
     }
     double deviation =
         std::sqrt(diff_sqare_sum / received_data.size()) / kMaxSampleValue;
@@ -130,7 +131,8 @@
 
   void TestEncodeDecode(int packet_size,
                         double frequency_hz,
-                        AudioPacket::SamplingRate rate) {
+                        AudioPacket::SamplingRate rate,
+                        int channels = 2) {
     const int kTotalTestSamples = 24000;
 
     encoder_ = std::make_unique<AudioEncoderOpus>();
@@ -140,7 +142,7 @@
     int pos = 0;
     for (; pos < kTotalTestSamples; pos += packet_size) {
       std::unique_ptr<AudioPacket> source_packet =
-          CreatePacket(packet_size, rate, frequency_hz, pos);
+          CreatePacket(packet_size, rate, frequency_hz, pos, channels);
       std::unique_ptr<AudioPacket> encoded =
           encoder_->Encode(std::move(source_packet));
       if (encoded.get()) {
@@ -159,11 +161,11 @@
 
     // Verify that at most kMaxLatencyMs worth of samples is buffered inside
     // |encoder_| and |decoder_|.
-    EXPECT_GE(static_cast<int>(received_data.size()) / kChannels,
+    EXPECT_GE(static_cast<int>(received_data.size()) / channels,
               pos - rate * kMaxLatencyMs / 1000);
 
     ValidateReceivedData(packet_size, kDefaultSamplingRate, frequency_hz,
-                         received_data);
+                         received_data, channels);
   }
 
  protected:
@@ -191,4 +193,202 @@
   TestEncodeDecode(5000, 3000, AudioPacket::SAMPLING_RATE_44100);
 }
 
+TEST_F(OpusAudioEncoderTest, Mono) {
+  TestEncodeDecode(2000, 3000, AudioPacket::SAMPLING_RATE_48000, 1);
+}
+
+TEST_F(OpusAudioEncoderTest, DynamicConfigChange) {
+  encoder_ = std::make_unique<AudioEncoderOpus>();
+  decoder_ = std::make_unique<AudioDecoderOpus>();
+
+  auto test_config = [&](int samples, AudioPacket::SamplingRate rate,
+                         int channels) {
+    std::unique_ptr<AudioPacket> source_packet =
+        CreatePacket(samples, rate, 3000, 0, channels);
+    std::unique_ptr<AudioPacket> encoded =
+        encoder_->Encode(std::move(source_packet));
+    // It might take multiple packets to get output due to buffering,
+    // but here we just want to ensure it doesn't crash and eventually
+    // produces something or handles the reset.
+    if (encoded) {
+      std::unique_ptr<AudioPacket> decoded =
+          decoder_->Decode(std::move(encoded));
+      EXPECT_EQ(kDefaultSamplingRate, decoded->sampling_rate());
+      EXPECT_EQ(channels, decoded->channels());
+    }
+  };
+
+  // Switch between various configs.
+  test_config(2000, AudioPacket::SAMPLING_RATE_48000, 2);
+  test_config(2000, AudioPacket::SAMPLING_RATE_44100, 2);
+  test_config(2000, AudioPacket::SAMPLING_RATE_48000, 1);
+  test_config(2000, AudioPacket::SAMPLING_RATE_44100, 1);
+}
+
+TEST_F(OpusAudioEncoderTest, UnsupportedParameters) {
+  encoder_ = std::make_unique<AudioEncoderOpus>();
+
+  // 3 channels (Unsupported).
+  auto packet =
+      CreatePacket(2000, AudioPacket::SAMPLING_RATE_48000, 3000, 0, 3);
+  EXPECT_FALSE(encoder_->Encode(std::move(packet)));
+}
+
+TEST_F(OpusAudioEncoderTest, SmallPackets) {
+  encoder_ = std::make_unique<AudioEncoderOpus>();
+
+  // Send 10ms of 48kHz audio (480 samples). Opus frame is 20ms.
+  auto packet = CreatePacket(480, AudioPacket::SAMPLING_RATE_48000, 3000, 0, 2);
+  auto encoded = encoder_->Encode(std::move(packet));
+  EXPECT_FALSE(encoded);  // Should be buffered.
+
+  // Send another 10ms.
+  packet = CreatePacket(480, AudioPacket::SAMPLING_RATE_48000, 3000, 480, 2);
+  encoded = encoder_->Encode(std::move(packet));
+  EXPECT_TRUE(encoded);  // Now we should have a full 20ms frame.
+}
+
+TEST_F(OpusAudioEncoderTest, SmallPacketsWithResampling) {
+  encoder_ = std::make_unique<AudioEncoderOpus>();
+
+  // Send 10ms of 44.1kHz audio (441 samples). Opus frame is 20ms.
+  auto packet = CreatePacket(441, AudioPacket::SAMPLING_RATE_44100, 3000, 0, 2);
+  auto encoded = encoder_->Encode(std::move(packet));
+  EXPECT_FALSE(encoded);  // Should be buffered.
+
+  // Send another 30ms (1323 samples). Total 40ms.
+  packet = CreatePacket(1323, AudioPacket::SAMPLING_RATE_44100, 3000, 441, 2);
+  encoded = encoder_->Encode(std::move(packet));
+  EXPECT_TRUE(encoded);
+  // We should have at least one encoded chunk.
+  EXPECT_GE(encoded->data_size(), 1);
+}
+
+TEST_F(OpusAudioEncoderTest, GetBitrate) {
+  encoder_ = std::make_unique<AudioEncoderOpus>();
+  EXPECT_EQ(encoder_->GetBitrate(), 160 * 1024);
+}
+
+// Makes sure that the FIFO returns spans from the new samples, without copying
+// data.
+TEST_F(OpusAudioEncoderTest, ResamplerFifo_TakeChunkDirect) {
+  auto fifo = AudioEncoderOpus::GetEmptyFifoForTesting(2048, 2);
+  ASSERT_TRUE(fifo);
+  const size_t chunk_size = fifo->GetChunkSizeForTesting();
+
+  std::vector<int16_t> samples(chunk_size, 42);
+  fifo->AddNewSamples(samples);
+  EXPECT_EQ(fifo->remaining_samples(), chunk_size);
+
+  auto chunk = fifo->TakeChunk();
+  // Make sure we haven't copied any data internally.
+  EXPECT_EQ(chunk.data(), samples.data());
+
+  EXPECT_EQ(chunk.size(), chunk_size);
+  EXPECT_EQ(chunk[0], 42);
+  EXPECT_EQ(fifo->remaining_samples(), 0u);
+
+  fifo.reset();
+}
+
+// Make sure that `SaveNewSamples()` copies new samples to internal storage.
+TEST_F(OpusAudioEncoderTest, ResamplerFifo_SaveNewSamples) {
+  auto fifo = AudioEncoderOpus::GetEmptyFifoForTesting(2048, 2);
+  const size_t chunk_size = fifo->GetChunkSizeForTesting();
+
+  {
+    std::vector<int16_t> data = {1, 2, 3, 4};
+    fifo->AddNewSamples(data);
+    EXPECT_EQ(fifo->remaining_samples(), 4u);
+
+    // This copies `data` into the FIFO's internal storage.
+    fifo->SaveNewSamples();
+  }
+  // `data` is now destroyed.
+
+  EXPECT_EQ(fifo->remaining_samples(), 4u);
+
+  // Verify the data is still there by completing a chunk and taking it.
+  std::vector<int16_t> part2(chunk_size - 4, 7);
+  fifo->AddNewSamples(part2);
+  auto chunk = fifo->TakeChunk();
+  EXPECT_EQ(chunk.size(), chunk_size);
+  EXPECT_EQ(chunk[0], 1);
+  EXPECT_EQ(chunk[3], 4);
+  EXPECT_EQ(chunk[4], 7);
+
+  fifo.reset();
+}
+
+// Make sure that we can receive a mix of saved and new samples.
+TEST_F(OpusAudioEncoderTest, ResamplerFifo_TakeChunkCrossover) {
+  auto fifo = AudioEncoderOpus::GetEmptyFifoForTesting(2048, 2);
+  const size_t chunk_size = fifo->GetChunkSizeForTesting();
+
+  // Add some samples and compact.
+  std::vector<int16_t> samples_to_save = {1, 2, 3, 4};
+  fifo->AddNewSamples(samples_to_save);
+  fifo->SaveNewSamples();
+
+  // Add more samples to complete a chunk.
+  std::vector<int16_t> large_chunks(chunk_size * 2, 7);
+  fifo->AddNewSamples(large_chunks);
+
+  // TakeChunk should now use the crossover buffer.
+  auto chunk = fifo->TakeChunk();
+  EXPECT_EQ(chunk.size(), chunk_size);
+  EXPECT_EQ(chunk[0], 1);
+  EXPECT_EQ(chunk[3], 4);
+  EXPECT_EQ(chunk[4], 7);
+
+  // Make sure we can pull remaining samples from the remaining `large_chunks`.
+  auto chunk_from_new_samples = fifo->TakeChunk();
+  EXPECT_EQ(chunk_from_new_samples.size(), chunk_size);
+
+  fifo.reset();
+}
+
+TEST_F(OpusAudioEncoderTest, ResamplerFifo_TakeChunkFromSavedSamples) {
+  auto fifo = AudioEncoderOpus::GetEmptyFifoForTesting(2048, 2);
+  const size_t chunk_size = fifo->GetChunkSizeForTesting();
+
+  // Add more than a chunk and compact.
+  std::vector<int16_t> data(chunk_size + 10, 42);
+  fifo->AddNewSamples(data);
+  fifo->SaveNewSamples();
+
+  // TakeChunk should pull from the compacted buffer.
+  auto chunk = fifo->TakeChunk();
+  EXPECT_EQ(chunk.size(), chunk_size);
+  EXPECT_EQ(fifo->remaining_samples(), 10u);
+
+  fifo.reset();
+}
+
+// Makes sure that
+TEST_F(OpusAudioEncoderTest, ResamplerFifo_SaveLargeChunks) {
+  const size_t kFifoSize = 2048;
+  auto fifo = AudioEncoderOpus::GetEmptyFifoForTesting(kFifoSize, 2);
+
+  // Completely fill the FIFO.
+  std::vector<int16_t> max_capacity(kFifoSize, 42);
+  fifo->AddNewSamples(max_capacity);
+  fifo->SaveNewSamples();
+
+  // Add more samples that can fit
+  std::vector<int16_t> huge_chunk(kFifoSize * 2, 42);
+  fifo->AddNewSamples(huge_chunk);
+
+  EXPECT_GT(fifo->remaining_samples(), kFifoSize);
+
+  while (fifo->remaining_samples() > kFifoSize) {
+    std::ignore = fifo->TakeChunk();
+  }
+
+  // Make sure we can save the data after consuming enough of it.
+  fifo->SaveNewSamples();
+
+  fifo.reset();
+}
+
 }  // namespace remoting
diff --git a/services/device/test/usb_test_gadget_impl.cc b/services/device/test/usb_test_gadget_impl.cc
index bc907c1..d325a94 100644
--- a/services/device/test/usb_test_gadget_impl.cc
+++ b/services/device/test/usb_test_gadget_impl.cc
@@ -11,10 +11,9 @@
 #include <vector>
 
 #include "base/command_line.h"
-#include "base/compiler_specific.h"
 #include "base/containers/span.h"
-#include "base/files/file.h"
 #include "base/files/file_path.h"
+#include "base/files/file_util.h"
 #include "base/functional/bind.h"
 #include "base/location.h"
 #include "base/logging.h"
@@ -25,7 +24,6 @@
 #include "base/process/process_handle.h"
 #include "base/run_loop.h"
 #include "base/scoped_observation.h"
-#include "base/stl_util.h"
 #include "base/strings/escape.h"
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
@@ -87,36 +85,12 @@
     {UsbTestGadget::ECHO, "/echo/configure", 0x58F4},
 });
 
-bool ReadFile(const base::FilePath& file_path, std::string* content) {
-  base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
-  if (!file.IsValid()) {
-    LOG(ERROR) << "Cannot open " << file_path.MaybeAsASCII() << ": "
-               << base::File::ErrorToString(file.error_details());
-    return false;
-  }
-
-  base::STLClearObject(content);
-  int rv;
-  do {
-    char buf[4096];
-    rv = UNSAFE_TODO(file.ReadAtCurrentPos(buf, sizeof buf));
-    if (rv == -1) {
-      LOG(ERROR) << "Cannot read " << file_path.MaybeAsASCII() << ": "
-                 << base::File::ErrorToString(file.error_details());
-      return false;
-    }
-    content->append(buf, rv);
-  } while (rv > 0);
-
-  return true;
-}
-
 bool ReadLocalVersion(std::string* version) {
   base::FilePath file_path;
   CHECK(base::PathService::Get(base::DIR_EXE, &file_path));
   file_path = file_path.AppendASCII("usb_gadget.zip.md5");
 
-  return ReadFile(file_path, version);
+  return base::ReadFileToString(file_path, version);
 }
 
 bool ReadLocalPackage(std::string* package) {
@@ -124,7 +98,7 @@
   CHECK(base::PathService::Get(base::DIR_EXE, &file_path));
   file_path = file_path.AppendASCII("usb_gadget.zip");
 
-  return ReadFile(file_path, package);
+  return base::ReadFileToString(file_path, package);
 }
 
 class URLRequestContextGetter : public net::URLRequestContextGetter {
diff --git a/services/network/network_context.cc b/services/network/network_context.cc
index 3253353c0..4bbe1bb 100644
--- a/services/network/network_context.cc
+++ b/services/network/network_context.cc
@@ -1377,6 +1377,43 @@
                                     base::Time end_time,
                                     mojom::ClearDataFilterPtr filter,
                                     ClearHttpCacheCallback callback) {
+  if (base::FeatureList::IsEnabled(net::features::kLogicalClearHttpCache)) {
+    net::HttpCache* cache =
+        url_request_context_->http_transaction_factory()->GetCache();
+    if (cache) {
+      // Step 1: Add a logical filter to the HttpCache. This is near-instant
+      // and ensures that subsequent requests won't see invalidated data.
+      net::HttpCache::InvalidationFilter invalidation_filter;
+      invalidation_filter.begin_time = start_time;
+      // Cap the end_time to Now() so we don't accidentally invalidate future
+      // cache entries if the caller passes Time::Max().
+      invalidation_filter.end_time = std::min(end_time, base::Time::Now());
+      if (filter) {
+        invalidation_filter.filter_type =
+            ConvertClearDataFilterType(filter->type);
+        invalidation_filter.origins = base::flat_set<url::Origin>(
+            filter->origins.begin(), filter->origins.end());
+        invalidation_filter.domains = base::flat_set<std::string>(
+            filter->domains.begin(), filter->domains.end());
+      } else {
+        invalidation_filter.filter_type = net::UrlFilterType::kFalseIfMatches;
+      }
+      cache->AddInvalidationFilter(std::move(invalidation_filter));
+    }
+
+    // Step 2: Trigger the slow physical cleanup in the background. We use a
+    // no-op callback because the logical invalidation already satisfies
+    // the consistency requirements of the caller.
+    http_cache_data_removers_.push_back(HttpCacheDataRemover::CreateAndStart(
+        url_request_context_, std::move(filter), start_time, end_time,
+        base::BindOnce(&NetworkContext::OnHttpCacheCleared,
+                       base::Unretained(this), base::DoNothing())));
+
+    // Step 3: Respond to the caller immediately.
+    std::move(callback).Run();
+    return;
+  }
+
   // It's safe to use Unretained below as the HttpCacheDataRemover is owned by
   // |this| and guarantees it won't call its callback if deleted.
   http_cache_data_removers_.push_back(HttpCacheDataRemover::CreateAndStart(
diff --git a/services/network/network_context_unittest.cc b/services/network/network_context_unittest.cc
index 3005b6c..0497b98 100644
--- a/services/network/network_context_unittest.cc
+++ b/services/network/network_context_unittest.cc
@@ -2228,6 +2228,25 @@
   // If all the callbacks were invoked, we should terminate.
 }
 
+TEST_F(NetworkContextTest, LogicalClearHttpCache) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(net::features::kLogicalClearHttpCache);
+
+  mojom::NetworkContextParamsPtr context_params =
+      CreateNetworkContextParamsForTesting();
+  context_params->http_cache_enabled = true;
+
+  std::unique_ptr<NetworkContext> network_context =
+      CreateContextWithParams(std::move(context_params));
+
+  base::test::TestFuture<void> future;
+  network_context->ClearHttpCache(base::Time(), base::Time(), nullptr,
+                                  future.GetCallback());
+
+  // The callback should be called immediately.
+  EXPECT_TRUE(future.Wait());
+}
+
 #if BUILDFLAG(ENTERPRISE_CACHE_ENCRYPTION)
 // Verifies that the simple backend is always used when encrypting the cache.
 TEST_F(NetworkContextTest, EncryptedHttpCacheForcesSimpleBackend) {
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 227ca78..7e08751 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -3740,6 +3740,28 @@
             ]
         }
     ],
+    "BackForwardCachePauseMicrotasks": [
+        {
+            "platforms": [
+                "android",
+                "android_webview",
+                "chromeos",
+                "fuchsia",
+                "ios",
+                "linux",
+                "mac",
+                "windows"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "BackForwardCachePauseMicrotasks"
+                    ]
+                }
+            ]
+        }
+    ],
     "BackNavigationMenuIPH": [
         {
             "platforms": [
@@ -8596,27 +8618,6 @@
             ]
         }
     ],
-    "DeviceBoundSessionsStandardLaunch": [
-        {
-            "platforms": [
-                "windows"
-            ],
-            "experiments": [
-                {
-                    "name": "Enabled_20260130",
-                    "params": {
-                        "RequireOriginTrialTokens": "false",
-                        "SchemaVersion": "3"
-                    },
-                    "enable_features": [
-                        "DeviceBoundSessions",
-                        "DeviceBoundSessionsFederatedRegistration",
-                        "PersistDeviceBoundSessions"
-                    ]
-                }
-            ]
-        }
-    ],
     "DexFixer": [
         {
             "platforms": [
@@ -10967,6 +10968,21 @@
             ]
         }
     ],
+    "FullscreenVideoPictureInPicture": [
+        {
+            "platforms": [
+                "android"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "FullscreenVideoPictureInPicture"
+                    ]
+                }
+            ]
+        }
+    ],
     "FusedLocationProviderTuning": [
         {
             "platforms": [
@@ -25633,40 +25649,6 @@
             ]
         }
     ],
-    "VizDirectCompositorThreadIpcAndroid": [
-        {
-            "platforms": [
-                "android"
-            ],
-            "experiments": [
-                {
-                    "name": "EnabledFSM",
-                    "enable_features": [
-                        "VizDirectCompositorThreadIpcFrameSinkManager"
-                    ],
-                    "disable_features": [
-                        "VizDirectCompositorThreadIpcNonRoot"
-                    ]
-                },
-                {
-                    "name": "EnabledNonRoot",
-                    "enable_features": [
-                        "VizDirectCompositorThreadIpcNonRoot"
-                    ],
-                    "disable_features": [
-                        "VizDirectCompositorThreadIpcFrameSinkManager"
-                    ]
-                },
-                {
-                    "name": "Control",
-                    "disable_features": [
-                        "VizDirectCompositorThreadIpcFrameSinkManager",
-                        "VizDirectCompositorThreadIpcNonRoot"
-                    ]
-                }
-            ]
-        }
-    ],
     "VizWithIoMessagePump": [
         {
             "platforms": [
diff --git a/third_party/angle b/third_party/angle
index 83a9513..f4d3129 160000
--- a/third_party/angle
+++ b/third_party/angle
@@ -1 +1 @@
-Subproject commit 83a9513da745fe45774bab896af6937d0cd00b64
+Subproject commit f4d31298079144b856580892285f4a1244168325
diff --git a/third_party/blink/public/mojom/ai/ai_common.mojom b/third_party/blink/public/mojom/ai/ai_common.mojom
index 65a4da8..104f3168 100644
--- a/third_party/blink/public/mojom/ai/ai_common.mojom
+++ b/third_party/blink/public/mojom/ai/ai_common.mojom
@@ -41,5 +41,8 @@
   // The expected language is not supported.
   kUnsupportedLanguage = 3,
 
+  // The expected performance preference is not supported.
+  kUnsupportedPerformancePreference = 4,
+
   // Append new line here
 };
diff --git a/third_party/blink/public/mojom/ai/ai_manager.mojom b/third_party/blink/public/mojom/ai/ai_manager.mojom
index f33949b..435d9a2 100644
--- a/third_party/blink/public/mojom/ai/ai_manager.mojom
+++ b/third_party/blink/public/mojom/ai/ai_manager.mojom
@@ -70,6 +70,8 @@
   // The on-device model is not available because the enterprise policy
   // disables the feature.
   kUnavailableEnterprisePolicyDisabled = 20,
+  // The model cannot be created because the preference is not supported.
+  kUnavailableUnsupportedPerformancePreference = 21,
 
   // Append new line here
 };
diff --git a/third_party/blink/public/mojom/ai/ai_summarizer.mojom b/third_party/blink/public/mojom/ai/ai_summarizer.mojom
index 9d0bb66..01412028 100644
--- a/third_party/blink/public/mojom/ai/ai_summarizer.mojom
+++ b/third_party/blink/public/mojom/ai/ai_summarizer.mojom
@@ -31,12 +31,20 @@
   kLong,
 };
 
+// The preference of the model.
+enum PerformancePreference {
+  kAuto,
+  kSpeed,
+  kCapability,
+};
+
 // This is used when creating a new AISummarizer.
 struct AISummarizerCreateOptions {
   string? shared_context;
   AISummarizerType type;
   AISummarizerFormat format;
   AISummarizerLength length;
+  PerformancePreference preference;
 
   // Creation fails if a model is not available for specified languages.
   array<AILanguageCode> expected_input_languages;
diff --git a/third_party/blink/renderer/bindings/core/v8/v8_microtasks_scope.h b/third_party/blink/renderer/bindings/core/v8/v8_microtasks_scope.h
index a6f04809..492d1f2 100644
--- a/third_party/blink/renderer/bindings/core/v8/v8_microtasks_scope.h
+++ b/third_party/blink/renderer/bindings/core/v8/v8_microtasks_scope.h
@@ -9,6 +9,7 @@
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
 #include "third_party/blink/renderer/platform/bindings/script_state.h"
+#include "third_party/blink/renderer/platform/scheduler/public/event_loop.h"
 #include "v8/include/v8-microtask-queue.h"
 
 namespace blink {
@@ -21,19 +22,26 @@
 class V8MicrotasksScope {
  public:
   explicit V8MicrotasksScope(ScriptState* script_state)
-      : scope_(script_state->GetIsolate(),
-               ToMicrotaskQueue(script_state),
-               kMode) {}
+      : V8MicrotasksScope(ExecutionContext::From(script_state)) {}
 
   explicit V8MicrotasksScope(ExecutionContext* execution_context)
       : scope_(execution_context->GetIsolate(),
                ToMicrotaskQueue(execution_context),
-               kMode) {}
+               EffectiveMode(execution_context)) {}
 
   V8MicrotasksScope(const V8MicrotasksScope&) = delete;
   V8MicrotasksScope& operator=(const V8MicrotasksScope&) = delete;
 
  private:
+  static v8::MicrotasksScope::Type EffectiveMode(
+      ExecutionContext* execution_context) {
+    if constexpr (kMode == MicrotasksScopeMode::kDoNotRunMicrotasks) {
+      return MicrotasksScopeMode::kDoNotRunMicrotasks;
+    }
+    return ToEventLoop(execution_context).AreMicrotasksPaused()
+               ? MicrotasksScopeMode::kDoNotRunMicrotasks
+               : MicrotasksScopeMode::kRunMicrotasks;
+  }
   v8::MicrotasksScope scope_;
 };
 
diff --git a/third_party/blink/renderer/core/frame/local_frame_back_forward_cache_test.cc b/third_party/blink/renderer/core/frame/local_frame_back_forward_cache_test.cc
index e79c06cc..6e00a1d 100644
--- a/third_party/blink/renderer/core/frame/local_frame_back_forward_cache_test.cc
+++ b/third_party/blink/renderer/core/frame/local_frame_back_forward_cache_test.cc
@@ -87,8 +87,11 @@
 // JavaScript execution at a microtask. Eviction is necessary to ensure that the
 // frame state is immutable when the frame is in the bfcache.
 // (https://www.chromestatus.com/feature/5815270035685376).
-// TODO(469686890): feature speculatively disabled now.
-TEST_F(LocalFrameBackForwardCacheTest, DISABLED_PauseMicrotaskExecution) {
+TEST_F(LocalFrameBackForwardCacheTest, PauseMicrotaskExecution) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndEnableFeature(
+      features::kBackForwardCachePauseMicrotasks);
+
   frame_test_helpers::TestWebFrameClient web_frame_client;
   TestLocalFrameBackForwardCacheClient frame_host(
       web_frame_client.GetRemoteNavigationAssociatedInterfaces());
diff --git a/third_party/blink/renderer/core/html/canvas/canvas_rendering_context.cc b/third_party/blink/renderer/core/html/canvas/canvas_rendering_context.cc
index 4413d8a..5e32bd69 100644
--- a/third_party/blink/renderer/core/html/canvas/canvas_rendering_context.cc
+++ b/third_party/blink/renderer/core/html/canvas/canvas_rendering_context.cc
@@ -150,31 +150,11 @@
     return builder.ToString();
   };
 
-  if (!RuntimeEnabledFeatures::CanvasDrawElementInSubtreeEnabled()) {
-    if (element->parentElement() != canvas_element) {
-      exception_state.ThrowTypeError(
-          build_error("Only immediate children of the <canvas> element can be "
-                      "passed to %s."));
-      return false;
-    }
-  } else {
-    if (!element->IsDescendantOf(canvas_element)) {
-      exception_state.ThrowTypeError(build_error(
-          "Only descendants of the <canvas> element can be passed to %s."));
-      return false;
-    }
-    // TODO(pdr): Update these checks to point to the updated spec. These are
-    // currently copied from element capture, which has similar paint reqs:
-    // https://screen-share.github.io/element-capture/#elements-eligible-for-restriction
-    auto* object = element->GetLayoutObject();
-    if (!object || !object->IsStackingContext() || !object->CreatesGroup() ||
-        !object->IsBox() ||
-        To<LayoutBox>(object)->PhysicalFragmentCount() > 1) {
-      exception_state.ThrowTypeError(
-          build_error("Only elements with certain requirements (stacking "
-                      "context, etc) can be passed to %s."));
-      return false;
-    }
+  if (element->parentElement() != canvas_element) {
+    exception_state.ThrowTypeError(
+        build_error("Only immediate children of the <canvas> element can be "
+                    "passed to %s."));
+    return false;
   }
 
   if (!canvas_element->layoutSubtree()) {
@@ -183,14 +163,6 @@
     return false;
   }
 
-  if (!element->GetLayoutObject()) {
-    exception_state.ThrowTypeError(build_error(
-        "The canvas and element used with %s must have been laid "
-        "out. Detached canvases are not supported, nor canvas or children that "
-        "are `display: none`."));
-    return false;
-  }
-
   return true;
 }
 
@@ -209,9 +181,6 @@
     std::optional<uint32_t> height,
     const String& func_name,
     ExceptionState& exception_state) {
-  element->GetDocument().View()->UpdateAllLifecyclePhasesExceptPaint(
-      DocumentUpdateReason::kCanvasDrawElementImage);
-
   if (!IsDrawElementImageEligible(element, func_name, exception_state)) {
     return nullptr;
   }
diff --git a/third_party/blink/renderer/core/html/canvas/html_canvas_element.cc b/third_party/blink/renderer/core/html/canvas/html_canvas_element.cc
index f5293b64..9916520 100644
--- a/third_party/blink/renderer/core/html/canvas/html_canvas_element.cc
+++ b/third_party/blink/renderer/core/html/canvas/html_canvas_element.cc
@@ -932,15 +932,15 @@
   }
 }
 
-gfx::Vector2dF HTMLCanvasElement::PhysicalPixelToCanvasGridScaleFactor() {
-  if (!GetDocument().View()) {
-    return {1., 1.};
-  }
+void HTMLCanvasElement::TakeGridScaleFactorSnapshot() {
+  CHECK(GetDocument().Lifecycle().GetState() == DocumentLifecycle::kInPaint);
 
-  GetDocument().View()->UpdateAllLifecyclePhasesExceptPaint(
-      DocumentUpdateReason::kCanvasDrawElementImage);
-  if (!GetLayoutBox()) {
-    return {1., 1.};
+  grid_scale_factor_snapshot_ = {1.f, 1.f};
+  if (!RuntimeEnabledFeatures::CanvasDrawElementEnabled()) {
+    return;
+  }
+  if (!GetDocument().View() || !GetLayoutBox()) {
+    return;
   }
 
   // As a special case, if the canvas is sized to its devicePixelContentBox,
@@ -953,7 +953,7 @@
                       GetLayoutBox()->ContentLogicalHeight()),
           *GetLayoutBox(), GetLayoutBox()->StyleRef());
   if (canvas_size == device_pixel_content_box) {
-    return gfx::Vector2dF(1., 1.);
+    return;
   }
 
   PhysicalRect content_rect;
@@ -962,8 +962,9 @@
   } else {
     content_rect = GetLayoutBox()->PhysicalContentBoxRect();
   }
-  return gfx::Vector2dF(canvas_size.width() / content_rect.Width().ToFloat(),
-                        canvas_size.height() / content_rect.Height().ToFloat());
+  grid_scale_factor_snapshot_ = {
+      canvas_size.width() / content_rect.Width().ToFloat(),
+      canvas_size.height() / content_rect.Height().ToFloat()};
 }
 
 namespace {
@@ -1007,7 +1008,10 @@
   // T_css = S_canvas_to_css * T_canvas * S_canvas_to_css-1
   gfx::Vector2dF physical_to_canvas_grid =
       PhysicalPixelToCanvasGridScaleFactor();
-  float physical_to_css = 1.0f / element->ComputedStyleRef().EffectiveZoom();
+  float physical_to_css = 1.0f;
+  if (element->GetComputedStyle()) {
+    physical_to_css = 1.0f / element->ComputedStyleRef().EffectiveZoom();
+  }
   float canvas_grid_to_css_x = physical_to_css / physical_to_canvas_grid.x();
   float canvas_grid_to_css_y = physical_to_css / physical_to_canvas_grid.y();
   result->scaleSelf(canvas_grid_to_css_x, canvas_grid_to_css_y);
diff --git a/third_party/blink/renderer/core/html/canvas/html_canvas_element.h b/third_party/blink/renderer/core/html/canvas/html_canvas_element.h
index 939f18b..f847e9af 100644
--- a/third_party/blink/renderer/core/html/canvas/html_canvas_element.h
+++ b/third_party/blink/renderer/core/html/canvas/html_canvas_element.h
@@ -338,7 +338,10 @@
 
   void ResetLayer();
 
-  gfx::Vector2dF PhysicalPixelToCanvasGridScaleFactor();
+  gfx::Vector2dF PhysicalPixelToCanvasGridScaleFactor() {
+    return grid_scale_factor_snapshot_;
+  }
+  void TakeGridScaleFactorSnapshot();
 
   // If `element` is drawn into the canvas's coordinate system with
   // `draw_transform`, this returns the transform that can be applied to
@@ -419,6 +422,9 @@
   bool origin_clean_;
   bool needs_unbuffered_input_ = false;
   bool style_is_visible_ = false;
+  // Snapshot of the scale factor from physical pixels to the canvas grid,
+  // recorded during the most recent paint update.
+  gfx::Vector2dF grid_scale_factor_snapshot_{1.f, 1.f};
 
   // Used for OffscreenCanvas that controls this HTML canvas element
   // and for low latency mode.
diff --git a/third_party/blink/renderer/core/input/keyboard_event_manager.cc b/third_party/blink/renderer/core/input/keyboard_event_manager.cc
index 554fdb8..a32b9cb3 100644
--- a/third_party/blink/renderer/core/input/keyboard_event_manager.cc
+++ b/third_party/blink/renderer/core/input/keyboard_event_manager.cc
@@ -459,7 +459,7 @@
     }
     if (event->keyCode() == last_scrolling_keycode_) {
       if (scrollend_event_target_ && has_pending_scrollend_on_key_up_) {
-        scrollend_event_target_->OnScrollFinished(true);
+        scrollend_event_target_->OnScrollFinished(/*enqueue_scrollend=*/true);
       }
       scrollend_event_target_.Clear();
       last_scrolling_keycode_ = VKEY_UNKNOWN;
diff --git a/third_party/blink/renderer/core/layout/layout_box.cc b/third_party/blink/renderer/core/layout/layout_box.cc
index 01ac583..3be2a7c 100644
--- a/third_party/blink/renderer/core/layout/layout_box.cc
+++ b/third_party/blink/renderer/core/layout/layout_box.cc
@@ -501,7 +501,11 @@
 
 PaintLayerType LayoutBox::LayerTypeRequired() const {
   NOT_DESTROYED();
-  if (IsStacked() || HasHiddenBackface()) {
+  if (IsStacked() || HasHiddenBackface() ||
+      // A normal flow replaced stacking context is not stacked by itself,
+      // but needs a PaintLayer to manage stacked children.
+      (RuntimeEnabledFeatures::StackingContextIsNotStackedEnabled() &&
+       IsReplacedNormalFlowStackingContext(StyleRef()))) {
     return kNormalPaintLayer;
   }
 
diff --git a/third_party/blink/renderer/core/layout/layout_object.h b/third_party/blink/renderer/core/layout/layout_object.h
index a2cf14d..7d19b0ae 100644
--- a/third_party/blink/renderer/core/layout/layout_object.h
+++ b/third_party/blink/renderer/core/layout/layout_object.h
@@ -710,6 +710,11 @@
             IsEligibleForPaintOrLayoutContainment());
   }
 
+  virtual bool IsReplacedNormalFlowStackingContext(const ComputedStyle&) const {
+    NOT_DESTROYED();
+    return false;
+  }
+
   inline bool IsStacked() const {
     NOT_DESTROYED();
     return IsStacked(StyleRef());
@@ -717,7 +722,9 @@
   inline bool IsStacked(const ComputedStyle& style) const {
     NOT_DESTROYED();
     return style.GetPosition() != EPosition::kStatic ||
-           IsStackingContext(style);
+           (IsStackingContext(style) &&
+            (!RuntimeEnabledFeatures::StackingContextIsNotStackedEnabled() ||
+             !IsReplacedNormalFlowStackingContext(style)));
   }
 
   // Returns true if the LayoutObject is rendered in the top layer or the layer
diff --git a/third_party/blink/renderer/core/layout/svg/layout_svg_foreign_object.h b/third_party/blink/renderer/core/layout/svg/layout_svg_foreign_object.h
index 7187a4f..11ea6375 100644
--- a/third_party/blink/renderer/core/layout/svg/layout_svg_foreign_object.h
+++ b/third_party/blink/renderer/core/layout/svg/layout_svg_foreign_object.h
@@ -59,6 +59,10 @@
     NOT_DESTROYED();
     return true;
   }
+  bool IsReplacedNormalFlowStackingContext(const ComputedStyle&) const final {
+    NOT_DESTROYED();
+    return true;
+  }
   bool IsChildAllowed(LayoutObject* child,
                       const ComputedStyle& style) const override;
   gfx::RectF ObjectBoundingBox() const override;
diff --git a/third_party/blink/renderer/core/loader/document_loader.cc b/third_party/blink/renderer/core/loader/document_loader.cc
index b285828c..2960f24d 100644
--- a/third_party/blink/renderer/core/loader/document_loader.cc
+++ b/third_party/blink/renderer/core/loader/document_loader.cc
@@ -1845,8 +1845,15 @@
         base::Milliseconds(100);
     cross_origin_parent_load_event_task_ = PostDelayedCancellableTask(
         *frame_->GetTaskRunner(TaskType::kInternalLoading), FROM_HERE,
-        BindOnce([](FrameOwner* owner) { owner->DispatchLoad(); },
-                 WrapWeakPersistent(frame_->Owner())),
+        BindOnce(
+            [](Frame* frame) {
+              // The delay might mean the frame is no longer attached to the
+              // owner (e.g., iframe detach).
+              if (auto* owner = frame->Owner()) {
+                owner->DispatchLoad();
+              }
+            },
+            WrapWeakPersistent(frame_.Get())),
         cross_origin_load_event_delay);
   }
 
diff --git a/third_party/blink/renderer/core/paint/box_paint_invalidator.cc b/third_party/blink/renderer/core/paint/box_paint_invalidator.cc
index a1b4a63..e950492f 100644
--- a/third_party/blink/renderer/core/paint/box_paint_invalidator.cc
+++ b/third_party/blink/renderer/core/paint/box_paint_invalidator.cc
@@ -463,6 +463,10 @@
   if (!RuntimeEnabledFeatures::CSSGapDecorationEnabled()) {
     return false;
   }
+  if (!box_.StyleRef().IsGapDecorationsContainer() ||
+      !box_.StyleRef().HasGapRule()) {
+    return false;
+  }
   for (const PhysicalBoxFragment& fragment : box_.PhysicalFragments()) {
     if (fragment.GetGapGeometry()) {
       return true;
@@ -475,6 +479,10 @@
   if (!RuntimeEnabledFeatures::CSSGapDecorationEnabled()) {
     return;
   }
+  if (!box_.StyleRef().IsGapDecorationsContainer() ||
+      !box_.StyleRef().HasGapRule()) {
+    return;
+  }
 
   const auto* previous = box_.PreviousGapGeometries();
 
diff --git a/third_party/blink/renderer/core/paint/box_paint_invalidator_test.cc b/third_party/blink/renderer/core/paint/box_paint_invalidator_test.cc
index 0f72d77..596d9af2 100644
--- a/third_party/blink/renderer/core/paint/box_paint_invalidator_test.cc
+++ b/third_party/blink/renderer/core/paint/box_paint_invalidator_test.cc
@@ -440,4 +440,34 @@
   UpdateAllLifecyclePhasesForTest();
 }
 
+// Verify that multicol column-rule invalidation works correctly with
+// CSSGapDecoration enabled (BoxPaintInvalidator handles gap decoration
+// invalidation via per-fragment geometry comparison).
+TEST_P(BoxPaintInvalidatorTest, GapDecorationMulticolColumnRuleInvalidation) {
+  ScopedPaintUnderInvalidationCheckingForTest under_invalidation_checking(true);
+  ScopedCSSGapDecorationForTest scoped_gap_decoration(true);
+  SetBodyInnerHTML(R"HTML(
+    <style>
+      #multicol {
+        columns: 2;
+        column-fill: auto;
+        width: 200px;
+        height: 100px;
+        column-rule: 2px solid black;
+      }
+    </style>
+    <div id="multicol">
+      <div style="height: 300px;"></div>
+    </div>
+  )HTML");
+  UpdateAllLifecyclePhasesForTest();
+
+  // Changing column-rule should not cause under-invalidation.
+  auto* multicol = GetDocument().getElementById(AtomicString("multicol"));
+  ASSERT_TRUE(multicol);
+  multicol->setAttribute(html_names::kStyleAttr,
+                         AtomicString("column-rule: 4px solid red"));
+  UpdateAllLifecyclePhasesForTest();
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/paint/html_canvas_painter.cc b/third_party/blink/renderer/core/paint/html_canvas_painter.cc
index 583c0ae..c9567e6 100644
--- a/third_party/blink/renderer/core/paint/html_canvas_painter.cc
+++ b/third_party/blink/renderer/core/paint/html_canvas_painter.cc
@@ -44,6 +44,8 @@
         .MarkFirstContentfulPaint();
   }
 
+  canvas->TakeGridScaleFactorSnapshot();
+
   if (auto* layer = canvas->ContentsCcLayer()) {
     // TODO(crbug.com/705019): For a texture layer canvas, setting the layer
     // background color to an opaque color will cause the layer to be treated as
diff --git a/third_party/blink/renderer/core/paint/paint_layer.cc b/third_party/blink/renderer/core/paint/paint_layer.cc
index 25b4c46c..58023ad0 100644
--- a/third_party/blink/renderer/core/paint/paint_layer.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer.cc
@@ -629,11 +629,12 @@
 }
 
 PaintLayer::PaintingContainerType PaintLayer::GetPaintingContainerType() const {
-  // TODO(crbug.com/40208685): Remove this condition after we make IsStacked()
-  // correct (returning false) for IsReplacedNormalFlowStacking().
-  if (IsReplacedNormalFlowStacking()) {
-    return PaintingContainerType::kParent;
+  if (!RuntimeEnabledFeatures::StackingContextIsNotStackedEnabled()) {
+    if (IsReplacedNormalFlowStackingContext()) {
+      return PaintingContainerType::kParent;
+    }
   }
+
   if (GetLayoutObject().IsStacked()) {
     return PaintingContainerType::kStackingContext;
   }
@@ -1321,7 +1322,7 @@
 
   // We can only reach an SVG foreign object's PaintLayer from
   // LayoutSVGForeignObject::NodeAtFloatPoint (because
-  // IsReplacedNormalFlowStacking() true for LayoutSVGForeignObject),
+  // IsReplacedNormalFlowStackingContext() true for LayoutSVGForeignObject),
   // where the hit_test_rect has already been transformed to local coordinates.
   bool use_transform = false;
   if (!layout_object.IsSVGForeignObject() &&
@@ -1804,8 +1805,13 @@
   return true;
 }
 
-bool PaintLayer::IsReplacedNormalFlowStacking() const {
-  return GetLayoutObject().IsSVGForeignObject();
+bool PaintLayer::IsReplacedNormalFlowStackingContext() const {
+  if (!RuntimeEnabledFeatures::StackingContextIsNotStackedEnabled()) {
+    return GetLayoutObject().IsSVGForeignObject();
+  }
+
+  return GetLayoutObject().IsReplacedNormalFlowStackingContext(
+      GetLayoutObject().StyleRef());
 }
 
 PaintLayer* PaintLayer::HitTestChildren(
@@ -1847,8 +1853,9 @@
   auto hit_test_child =
       [&](PaintLayer* child_layer, bool overflow_controls_only,
           const HitTestRecursionData& recursion_data) -> bool {
-    if (child_layer->IsReplacedNormalFlowStacking())
+    if (child_layer->IsReplacedNormalFlowStackingContext()) {
       return false;
+    }
 
     bool is_scoped_transition_pseudo =
         !GetLayoutObject().IsViewTransitionRoot() &&
diff --git a/third_party/blink/renderer/core/paint/paint_layer.h b/third_party/blink/renderer/core/paint/paint_layer.h
index 00357e6..7af4e674 100644
--- a/third_party/blink/renderer/core/paint/paint_layer.h
+++ b/third_party/blink/renderer/core/paint/paint_layer.h
@@ -521,7 +521,7 @@
   // See
   // https://chromium.googlesource.com/chromium/src.git/+/main/third_party/blink/renderer/core/paint/README.md
   // for the definition of a replaced normal-flow stacking element.
-  bool IsReplacedNormalFlowStacking() const;
+  bool IsReplacedNormalFlowStackingContext() const;
 
 #if DCHECK_IS_ON()
   bool LayerListMutationAllowed() const { return layer_list_mutation_allowed_; }
diff --git a/third_party/blink/renderer/core/paint/paint_layer_painter.cc b/third_party/blink/renderer/core/paint/paint_layer_painter.cc
index b1cc551..8f4bc71 100644
--- a/third_party/blink/renderer/core/paint/paint_layer_painter.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer_painter.cc
@@ -604,8 +604,9 @@
 
   PaintLayerPaintOrderIterator iterator(&paint_layer_, children_to_visit);
   while (PaintLayer* child = iterator.Next()) {
-    if (child->IsReplacedNormalFlowStacking())
+    if (child->IsReplacedNormalFlowStackingContext()) {
       continue;
+    }
 
     if (!layout_object.IsViewTransitionRoot() &&
         ViewTransitionUtils::IsViewTransitionRoot(child->GetLayoutObject())) {
diff --git a/third_party/blink/renderer/core/paint/paint_layer_test.cc b/third_party/blink/renderer/core/paint/paint_layer_test.cc
index 9707cfd5..f8deb329 100644
--- a/third_party/blink/renderer/core/paint/paint_layer_test.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer_test.cc
@@ -2308,6 +2308,22 @@
   }
 }
 
+TEST_P(PaintLayerTest, ReplacedNormalFlowStackingForeignObjectIsNotStacked) {
+  SetBodyInnerHTML(R"HTML(
+    <svg width="200" height="200">
+      <foreignObject id="foreignObject" width="100" height="100"></foreignObject>
+    </svg>
+  )HTML");
+
+  PaintLayer* foreign_object = GetPaintLayerByElementId("foreignObject");
+  LayoutBoxModelObject& layout_object = foreign_object->GetLayoutObject();
+
+  EXPECT_TRUE(foreign_object->IsReplacedNormalFlowStackingContext());
+  EXPECT_TRUE(layout_object.IsStackingContext());
+  EXPECT_FALSE(layout_object.IsStacked());
+  EXPECT_TRUE(layout_object.HasLayer());
+}
+
 TEST_P(PaintLayerTest, AddLayerNeedsRepaintAndCullRectUpdate) {
   SetBodyInnerHTML(R"HTML(
     <div id="parent" style="opacity: 0.9">
diff --git a/third_party/blink/renderer/core/paint/pre_paint_tree_walk.cc b/third_party/blink/renderer/core/paint/pre_paint_tree_walk.cc
index f64a2314..07204b2 100644
--- a/third_party/blink/renderer/core/paint/pre_paint_tree_walk.cc
+++ b/third_party/blink/renderer/core/paint/pre_paint_tree_walk.cc
@@ -14,6 +14,7 @@
 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
 #include "third_party/blink/renderer/core/frame/pagination_state.h"
 #include "third_party/blink/renderer/core/frame/visual_viewport.h"
+#include "third_party/blink/renderer/core/html/canvas/html_canvas_element.h"
 #include "third_party/blink/renderer/core/intersection_observer/intersection_observer_controller.h"
 #include "third_party/blink/renderer/core/layout/block_break_token.h"
 #include "third_party/blink/renderer/core/layout/fragmentation_utils.h"
diff --git a/third_party/blink/renderer/core/paint/svg_foreign_object_painter.cc b/third_party/blink/renderer/core/paint/svg_foreign_object_painter.cc
index fbbebbc..ca24d0c 100644
--- a/third_party/blink/renderer/core/paint/svg_foreign_object_painter.cc
+++ b/third_party/blink/renderer/core/paint/svg_foreign_object_painter.cc
@@ -28,7 +28,7 @@
     return;
 
   // <foreignObject> is a replaced normal-flow stacking element.
-  // See IsReplacedNormalFlowStacking in paint_layer_painter.cc.
+  // See IsReplacedNormalFlowStackingContext in paint_layer_painter.cc.
   PaintLayerPainter(*layout_svg_foreign_object_.Layer())
       .Paint(paint_info.context, paint_info.GetPaintFlags());
 }
diff --git a/third_party/blink/renderer/core/scroll/scrollable_area.cc b/third_party/blink/renderer/core/scroll/scrollable_area.cc
index 2f82ff8..f5edf5b 100644
--- a/third_party/blink/renderer/core/scroll/scrollable_area.cc
+++ b/third_party/blink/renderer/core/scroll/scrollable_area.cc
@@ -345,8 +345,9 @@
                        "SetScrollOffset", TRACE_EVENT_SCOPE_THREAD, "behavior",
                        behavior);
 
-  if (behavior == mojom::blink::ScrollBehavior::kAuto)
+  if (behavior == mojom::blink::ScrollBehavior::kAuto) {
     behavior = ScrollBehaviorStyle();
+  }
 
   gfx::Vector2d animation_adjustment = gfx::ToRoundedVector2d(clamped_offset) -
                                        gfx::ToRoundedVector2d(previous_offset);
@@ -361,44 +362,39 @@
     case mojom::blink::ScrollType::kCompositor:
       ScrollOffsetChanged(clamped_offset, scroll_type, source_type);
       break;
+
     case mojom::blink::ScrollType::kClamping:
       DCHECK_EQ(source_type, cc::ScrollSourceType::kStationaryScroll);
       ScrollOffsetChanged(clamped_offset, scroll_type,
                           cc::ScrollSourceType::kNone);
       GetScrollAnimator().AdjustAnimation(animation_adjustment);
       break;
+
     case mojom::blink::ScrollType::kAnchoring:
       DCHECK_EQ(source_type, cc::ScrollSourceType::kStationaryScroll);
       ScrollOffsetChanged(clamped_offset, scroll_type, source_type);
       GetScrollAnimator().AdjustAnimation(animation_adjustment);
       pending_scroll_anchor_adjustment_ += clamped_offset - previous_offset;
       break;
+
     case mojom::blink::ScrollType::kScrollStart:
       DCHECK_EQ(source_type, cc::ScrollSourceType::kAbsoluteScroll);
       ScrollOffsetChanged(clamped_offset, scroll_type, source_type);
       GetScrollAnimator().AdjustAnimation(animation_adjustment);
       break;
+
     case mojom::blink::ScrollType::kProgrammatic:
-      if (ProgrammaticScrollHelper(clamped_offset, behavior,
-                                   animation_adjustment, source_type)) {
-        if (behavior == mojom::blink::ScrollBehavior::kSmooth) {
-          active_smooth_scroll_type_ = scroll_type;
-        }
-        return true;
-      }
-      return false;
+      return InitiateScrollAnimation(clamped_offset, scroll_type, behavior,
+                                     animation_adjustment, source_type);
+
     case mojom::blink::ScrollType::kUser:
       if (behavior == mojom::blink::ScrollBehavior::kSmooth) {
-        if (ProgrammaticScrollHelper(clamped_offset, behavior,
-                                     animation_adjustment, source_type)) {
-          active_smooth_scroll_type_ = scroll_type;
-          return true;
-        }
-        return false;
-      } else {
-        UserScrollHelper(clamped_offset, behavior, source_type);
-        break;
+        return InitiateScrollAnimation(clamped_offset, scroll_type, behavior,
+                                       animation_adjustment, source_type);
       }
+      UserScrollHelper(clamped_offset, behavior, source_type);
+      break;
+
     default:
       NOTREACHED();
   }
@@ -501,8 +497,9 @@
                   cc::ScrollSourceType::kRelativeScroll, behavior);
 }
 
-bool ScrollableArea::ProgrammaticScrollHelper(
+bool ScrollableArea::InitiateScrollAnimation(
     const ScrollOffset& offset,
+    mojom::blink::ScrollType scroll_type,
     mojom::blink::ScrollBehavior scroll_behavior,
     gfx::Vector2d animation_adjustment,
     cc::ScrollSourceType source_type) {
@@ -547,10 +544,17 @@
     // cancel) a user scroll animation already in progress (crbug.com/1264266).
     GetScrollAnimator().AdjustAnimation(animation_adjustment);
 
-    if (callback)
+    if (callback) {
       std::move(callback).Run(ScrollCompletionMode::kFinished);
+    }
   }
   UpdateScrollMarkers();
+
+  // TODO(mustaq@chromium.org): It is not clear why the `if` condition below
+  // does not rely on `should_use_animation` instead.
+  if (scroll_behavior == mojom::blink::ScrollBehavior::kSmooth) {
+    active_smooth_scroll_type_ = scroll_type;
+  }
   return true;
 }
 
@@ -1134,12 +1138,12 @@
                                           element_id_namespace);
 }
 
-void ScrollableArea::OnScrollFinished(bool scroll_did_end) {
+void ScrollableArea::OnScrollFinished(bool enqueue_scrollend) {
   if (!GetLayoutBox()) {
     return;
   }
 
-  if (scroll_did_end) {
+  if (enqueue_scrollend) {
     active_smooth_scroll_type_.reset();
     UpdateSnappedTargetsAndEnqueueScrollSnapChange();
     if (Node* node = EventTargetNode()) {
diff --git a/third_party/blink/renderer/core/scroll/scrollable_area.h b/third_party/blink/renderer/core/scroll/scrollable_area.h
index 5d921f2..a366739 100644
--- a/third_party/blink/renderer/core/scroll/scrollable_area.h
+++ b/third_party/blink/renderer/core/scroll/scrollable_area.h
@@ -717,10 +717,11 @@
 
   void SetScrollbarsHiddenIfOverlayInternal(bool);
 
-  bool ProgrammaticScrollHelper(const ScrollOffset&,
-                                mojom::blink::ScrollBehavior,
-                                gfx::Vector2d animation_adjustment,
-                                cc::ScrollSourceType);
+  bool InitiateScrollAnimation(const ScrollOffset&,
+                               mojom::blink::ScrollType,
+                               mojom::blink::ScrollBehavior,
+                               gfx::Vector2d animation_adjustment,
+                               cc::ScrollSourceType);
   void UserScrollHelper(const ScrollOffset&,
                         mojom::blink::ScrollBehavior,
                         cc::ScrollSourceType);
diff --git a/third_party/blink/renderer/modules/ai/ai_utils.cc b/third_party/blink/renderer/modules/ai/ai_utils.cc
index e9463a4..058ee24 100644
--- a/third_party/blink/renderer/modules/ai/ai_utils.cc
+++ b/third_party/blink/renderer/modules/ai/ai_utils.cc
@@ -16,6 +16,7 @@
 #include "third_party/blink/renderer/bindings/modules/v8/v8_language_model_message_type.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_language_model_tool_call.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_language_model_tool_call_init.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_performance_preference.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_union_language_model_message_value.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
@@ -50,6 +51,18 @@
 
 namespace {
 
+mojom::blink::PerformancePreference ToMojoSummarizerPreference(
+    V8PerformancePreference preference) {
+  switch (preference.AsEnum()) {
+    case V8PerformancePreference::Enum::kAuto:
+      return mojom::blink::PerformancePreference::kAuto;
+    case V8PerformancePreference::Enum::kSpeed:
+      return mojom::blink::PerformancePreference::kSpeed;
+    case V8PerformancePreference::Enum::kCapability:
+      return mojom::blink::PerformancePreference::kCapability;
+  }
+}
+
 mojom::blink::AISummarizerType ToMojoSummarizerType(V8SummarizerType type) {
   switch (type.AsEnum()) {
     case V8SummarizerType::Enum::kTldr:
@@ -156,6 +169,7 @@
       shared_context, ToMojoSummarizerType(options->type()),
       ToMojoSummarizerFormat(options->format()),
       ToMojoSummarizerLength(options->length()),
+      ToMojoSummarizerPreference(options->preference()),
       ToMojoLanguageCodes(options->getExpectedInputLanguagesOr({})),
       ToMojoLanguageCodes(options->getExpectedContextLanguagesOr({})),
       mojom::blink::AILanguageCode::New(
diff --git a/third_party/blink/renderer/modules/ai/ai_writing_assistance_create_client.h b/third_party/blink/renderer/modules/ai/ai_writing_assistance_create_client.h
index b8d7f2e..4045a2a 100644
--- a/third_party/blink/renderer/modules/ai/ai_writing_assistance_create_client.h
+++ b/third_party/blink/renderer/modules/ai/ai_writing_assistance_create_client.h
@@ -158,6 +158,12 @@
             kExceptionMessageUnsupportedLanguages);
         break;
       }
+      case AIManagerCreateClientError::kUnsupportedPerformancePreference: {
+        this->GetResolver()->RejectWithDOMException(
+            DOMExceptionCode::kNotSupportedError,
+            kExceptionMessageUnsupportedPerformancePreference);
+        break;
+      }
     }
   }
 
diff --git a/third_party/blink/renderer/modules/ai/availability.cc b/third_party/blink/renderer/modules/ai/availability.cc
index 98bb323..569289b 100644
--- a/third_party/blink/renderer/modules/ai/availability.cc
+++ b/third_party/blink/renderer/modules/ai/availability.cc
@@ -46,6 +46,8 @@
     case ModelAvailabilityCheckResult::kUnavailableInsufficientDiskSpace:
     case ModelAvailabilityCheckResult::kUnavailableTranslationNotEligible:
     case ModelAvailabilityCheckResult::kUnavailableEnterprisePolicyDisabled:
+    case ModelAvailabilityCheckResult::
+        kUnavailableUnsupportedPerformancePreference:
       return Availability::kUnavailable;
   }
 }
diff --git a/third_party/blink/renderer/modules/ai/exception_helpers.cc b/third_party/blink/renderer/modules/ai/exception_helpers.cc
index 19e1bfa..88c5e0e 100644
--- a/third_party/blink/renderer/modules/ai/exception_helpers.cc
+++ b/third_party/blink/renderer/modules/ai/exception_helpers.cc
@@ -66,6 +66,9 @@
     "initialPrompts.";
 const char kExceptionMessageUnsupportedLanguages[] =
     "The specified languages are not supported.";
+// TODO(crbug.com/488092645): Update the message once speed is supported.
+const char kExceptionMessageUnsupportedPerformancePreference[] =
+    "The 'speed' preference is not supported yet.";
 const char kExceptionMessageInvalidResponseJsonSchema[] =
     "Response json schema is invalid - it should be an object that can be "
     "stringified into a JSON string.";
@@ -321,6 +324,9 @@
         kUnavailableEnterprisePolicyDisabled:
       return "The on-device model is not available because the enterprise "
              "policy disables the feature.";
+    case mojom::blink::ModelAvailabilityCheckResult::
+        kUnavailableUnsupportedPerformancePreference:
+      return "The specified performance preference is not supported yet.";
     case mojom::blink::ModelAvailabilityCheckResult::kAvailable:
     case mojom::blink::ModelAvailabilityCheckResult::kDownloadable:
     case mojom::blink::ModelAvailabilityCheckResult::kDownloading:
diff --git a/third_party/blink/renderer/modules/ai/exception_helpers.h b/third_party/blink/renderer/modules/ai/exception_helpers.h
index 565afe02..c11d23a 100644
--- a/third_party/blink/renderer/modules/ai/exception_helpers.h
+++ b/third_party/blink/renderer/modules/ai/exception_helpers.h
@@ -28,6 +28,7 @@
 extern const char kExceptionMessageRequestAborted[];
 extern const char kExceptionMessagePromptWithSystemRoleIsNotTheFirst[];
 extern const char kExceptionMessageUnsupportedLanguages[];
+extern const char kExceptionMessageUnsupportedPerformancePreference[];
 extern const char kExceptionMessageInvalidResponseJsonSchema[];
 extern const char kExceptionMessagePermissionPolicy[];
 extern const char kExceptionMessageUserActivationRequired[];
diff --git a/third_party/blink/renderer/modules/ai/language_model_create_client.cc b/third_party/blink/renderer/modules/ai/language_model_create_client.cc
index 98cc5b8..30aa6ae1 100644
--- a/third_party/blink/renderer/modules/ai/language_model_create_client.cc
+++ b/third_party/blink/renderer/modules/ai/language_model_create_client.cc
@@ -500,6 +500,12 @@
           kExceptionMessageUnsupportedLanguages);
       break;
     }
+    case AIManagerCreateClientError::kUnsupportedPerformancePreference: {
+      GetResolver()->RejectWithDOMException(
+          DOMExceptionCode::kNotSupportedError,
+          kExceptionMessageUnsupportedPerformancePreference);
+      break;
+    }
   }
   Cleanup();
 }
diff --git a/third_party/blink/renderer/modules/media_controls/media_controls_impl.cc b/third_party/blink/renderer/modules/media_controls/media_controls_impl.cc
index ded6d94..530a144 100644
--- a/third_party/blink/renderer/modules/media_controls/media_controls_impl.cc
+++ b/third_party/blink/renderer/modules/media_controls/media_controls_impl.cc
@@ -34,7 +34,9 @@
 #include "third_party/blink/public/platform/task_type.h"
 #include "third_party/blink/public/platform/user_metrics_action.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_mutation_observer_init.h"
+#include "third_party/blink/renderer/core/css/css_property_names.h"
 #include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/css_value_keywords.h"
 #include "third_party/blink/renderer/core/dom/element_traversal.h"
 #include "third_party/blink/renderer/core/dom/events/event_dispatch_forbidden_scope.h"
 #include "third_party/blink/renderer/core/dom/mutation_observer.h"
@@ -1035,6 +1037,7 @@
   panel_->SetIsWanted(true);
   panel_->SetIsDisplayed(true);
 
+  UpdateContainerDisplay();
   UpdateCurrentTimeDisplay();
 
   if (overlay_play_button_ && !is_paused_for_scrubbing_)
@@ -1085,6 +1088,31 @@
   // when the media element is connected.
   if (MediaElement().isConnected())
     UpdateActingAsAudioControls();
+
+  UpdateContainerDisplay();
+}
+
+void MediaControlsImpl::UpdateContainerDisplay() {
+  if (!RuntimeEnabledFeatures::HideVideoControlsWhenUnneededEnabled()) {
+    return;
+  }
+
+  // When native controls are not shown and no overlay needs the container, hide
+  // it entirely to avoid creating layers that interfere with hit-test ordering.
+  bool should_hide =
+      !MediaElement().ShouldShowControls() && !overlay_cast_button_->IsWanted();
+  bool is_hidden = InlineStyle() && InlineStyle()->GetPropertyValue(
+                                        CSSPropertyID::kDisplay) == "none";
+
+  if (should_hide == is_hidden) {
+    return;
+  }
+
+  if (should_hide) {
+    SetInlineStyleProperty(CSSPropertyID::kDisplay, CSSValueID::kNone);
+  } else {
+    RemoveInlineStyleProperty(CSSPropertyID::kDisplay);
+  }
 }
 
 bool MediaControlsImpl::IsVisible() const {
@@ -1096,6 +1124,16 @@
     overlay_play_button_->SetIsDisplayed(true);
 }
 
+void MediaControlsImpl::MaybeShowOverlayCastButton() {
+  if (RuntimeEnabledFeatures::HideVideoControlsWhenUnneededEnabled()) {
+    // Ensure the container is visible before TryShowOverlay(), which needs
+    // layout to determine if the button is covered by another element.
+    RemoveInlineStyleProperty(CSSPropertyID::kDisplay);
+  }
+
+  overlay_cast_button_->TryShowOverlay();
+}
+
 void MediaControlsImpl::MakeOpaque() {
   ShowCursor();
   panel_->MakeOpaque();
@@ -1311,6 +1349,7 @@
   if (!ShouldShowCastButton(MediaElement())) {
     cast_button_->SetIsWanted(false);
     overlay_cast_button_->SetIsWanted(false);
+    UpdateContainerDisplay();
     return;
   }
 
@@ -1331,19 +1370,22 @@
     // non-cast changes (e.g., resize) occur.  If the panel button
     // is shown, however, compute...() will take control of the
     // overlay cast button if it needs to hide it from the panel.
-      overlay_cast_button_->TryShowOverlay();
+    MaybeShowOverlayCastButton();
   } else {
     overlay_cast_button_->SetIsWanted(false);
   }
+  UpdateContainerDisplay();
 }
 
 void MediaControlsImpl::ShowOverlayCastButtonIfNeeded() {
   if (!ShouldShowCastOverlayButton(MediaElement())) {
     overlay_cast_button_->SetIsWanted(false);
+    UpdateContainerDisplay();
     return;
   }
 
-  overlay_cast_button_->TryShowOverlay();
+  MaybeShowOverlayCastButton();
+  UpdateContainerDisplay();
   ResetHideMediaControlsTimer();
 }
 
diff --git a/third_party/blink/renderer/modules/media_controls/media_controls_impl.h b/third_party/blink/renderer/modules/media_controls/media_controls_impl.h
index d361eb6..2bace1dd 100644
--- a/third_party/blink/renderer/modules/media_controls/media_controls_impl.h
+++ b/third_party/blink/renderer/modules/media_controls/media_controls_impl.h
@@ -265,8 +265,10 @@
   void MakeTransparent();
   bool IsVisible() const;
 
-  // If the overlay play button is present then make sure it is displayed.
+  // If the overlay play/cast buttons are present then make
+  // sure they are displayed.
   void MaybeShowOverlayPlayButton();
+  void MaybeShowOverlayCastButton();
 
   void UpdatePlayState();
 
@@ -342,6 +344,11 @@
   bool IsOnLeftSide(Event*);
   void TapTimerFired(TimerBase*);
 
+  // Hides or shows the container depending on whether the native controls
+  // or the overlay cast button need to be shown, to prevent shadow DOM paint
+  // layers from interfering with hit-test ordering.
+  void UpdateContainerDisplay();
+
   // Internal cast related methods.
   void RemotePlaybackStateChanged();
   void RefreshCastButtonVisibility();
diff --git a/third_party/blink/renderer/modules/webaudio/periodic_wave.cc b/third_party/blink/renderer/modules/webaudio/periodic_wave.cc
index acef7e4..d494d3a 100644
--- a/third_party/blink/renderer/modules/webaudio/periodic_wave.cc
+++ b/third_party/blink/renderer/modules/webaudio/periodic_wave.cc
@@ -102,8 +102,9 @@
 
   PeriodicWave* periodic_wave =
       MakeGarbageCollected<PeriodicWave>(context.sampleRate());
-  periodic_wave->impl()->CreateBandLimitedTables(
-      real.data(), imag.data(), real.size(), disable_normalization);
+  periodic_wave->impl()->CreateBandLimitedTables(base::span<const float>(real),
+                                                 base::span<const float>(imag),
+                                                 disable_normalization);
   return periodic_wave;
 }
 
@@ -408,10 +409,10 @@
 // Convert into time-domain wave buffers.  One table is created for each range
 // for non-aliasing playback at different playback rates.  Thus, higher ranges
 // have more high-frequency partials culled out.
-void PeriodicWaveImpl::CreateBandLimitedTables(const float* real_data,
-                                               const float* imag_data,
-                                               unsigned number_of_components,
-                                               bool disable_normalization) {
+void PeriodicWaveImpl::CreateBandLimitedTables(
+    base::span<const float> real_data,
+    base::span<const float> imag_data,
+    bool disable_normalization) {
   // The default scale factor for when normalization is disabled.
   float normalization_scale = 0.5;
 
@@ -419,7 +420,8 @@
   unsigned half_size = fft_size / 2;
   unsigned i;
 
-  number_of_components = std::min(number_of_components, half_size);
+  unsigned number_of_components =
+      std::min(static_cast<unsigned>(real_data.size()), half_size);
 
   band_limited_tables_.reserve(NumberOfRanges());
 
@@ -437,11 +439,11 @@
     // arrays.  Need to scale the data by fftSize to remove the scaling that the
     // inverse IFFT would do.
     float scale = fft_size;
-    vector_math::Vsmul(
-        real_data, 1, &scale, real.Data(), 1, number_of_components);
+    vector_math::Vsmul(real_data.data(), 1, &scale, real.Data(), 1,
+                       number_of_components);
     scale = -scale;
-    vector_math::Vsmul(
-        imag_data, 1, &scale, imag.Data(), 1, number_of_components);
+    vector_math::Vsmul(imag_data.data(), 1, &scale, imag.Data(), 1,
+                       number_of_components);
 
     // Find the starting bin where we should start culling.  We need to clear
     // out the highest frequencies to band-limit the waveform.
@@ -496,12 +498,10 @@
 
   AudioFloatArray real(half_size);
   AudioFloatArray imag(half_size);
-  float* real_p = real.Data();
-  float* imag_p = imag.Data();
 
   // Clear DC and Nyquist.
-  real_p[0] = 0;
-  imag_p[0] = 0;
+  real[0] = 0;
+  imag[0] = 0;
 
   for (unsigned n = 1; n < half_size; ++n) {
     float pi_factor = 2 / (n * kPiFloat);
@@ -562,11 +562,11 @@
         NOTREACHED();
     }
 
-    UNSAFE_TODO(real_p[n]) = 0;
-    UNSAFE_TODO(imag_p[n]) = b;
+    real[n] = 0;
+    imag[n] = b;
   }
 
-  CreateBandLimitedTables(real_p, imag_p, half_size, false);
+  CreateBandLimitedTables(real.as_span(), imag.as_span(), false);
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/webaudio/periodic_wave.h b/third_party/blink/renderer/modules/webaudio/periodic_wave.h
index a162761..bbd85a8 100644
--- a/third_party/blink/renderer/modules/webaudio/periodic_wave.h
+++ b/third_party/blink/renderer/modules/webaudio/periodic_wave.h
@@ -144,9 +144,8 @@
   unsigned NumberOfPartialsForRange(unsigned range_index) const;
 
   // Creates tables based on numberOfComponents Fourier coefficients.
-  void CreateBandLimitedTables(const float* real,
-                               const float* imag,
-                               unsigned number_of_components,
+  void CreateBandLimitedTables(base::span<const float> real,
+                               base::span<const float> imag,
                                bool disable_normalization);
   Vector<std::unique_ptr<AudioFloatArray>> band_limited_tables_;
 
diff --git a/third_party/blink/renderer/platform/audio/audio_channel.cc b/third_party/blink/renderer/platform/audio/audio_channel.cc
index 160c8ab..853303f 100644
--- a/third_party/blink/renderer/platform/audio/audio_channel.cc
+++ b/third_party/blink/renderer/platform/audio/audio_channel.cc
@@ -57,10 +57,9 @@
 
   if (source_channel->IsSilent()) {
     Zero();
-    return;
+  } else {
+    MutableSpan().copy_from(source_channel->Span().first(length()));
   }
-  UNSAFE_TODO(memcpy(MutableData(), source_channel->Data(),
-                     base::CheckMul(sizeof(float), length()).ValueOrDie()));
 }
 
 void AudioChannel::CopyFromRange(const AudioChannel* source_channel,
@@ -79,19 +78,16 @@
   size_t range_length = end_frame - start_frame;
   DCHECK_LE(range_length, length());
 
-  const float* source = source_channel->Data();
-  float* destination = MutableData();
-
-  const size_t safe_length =
-      base::CheckMul(sizeof(float), range_length).ValueOrDie();
   if (source_channel->IsSilent()) {
     if (range_length == length()) {
       Zero();
     } else {
-      UNSAFE_TODO(memset(destination, 0, safe_length));
+      std::ranges::fill(MutableSpan().first(range_length), 0.f);
     }
   } else {
-    UNSAFE_TODO(memcpy(destination, source + start_frame, safe_length));
+    MutableSpan()
+        .first(range_length)
+        .copy_from(source_channel->Span().subspan(start_frame, range_length));
   }
 }
 
diff --git a/third_party/blink/renderer/platform/audio/audio_channel.h b/third_party/blink/renderer/platform/audio/audio_channel.h
index a4ce77c..815aa03 100644
--- a/third_party/blink/renderer/platform/audio/audio_channel.h
+++ b/third_party/blink/renderer/platform/audio/audio_channel.h
@@ -108,17 +108,11 @@
 
   // Zeroes out all sample values in buffer.
   void Zero() {
-    if (silent_) {
-      return;
-    }
-
-    silent_ = true;
-
-    if (mem_buffer_.get()) {
-      mem_buffer_->Zero();
-    } else {
-      UNSAFE_TODO(memset(raw_pointer_, 0,
-                         base::CheckMul(sizeof(float), length_).ValueOrDie()));
+    if (!silent_) {
+      std::ranges::fill(MutableSpan(), 0.f);
+      // Set silent flag after calling `MutableSpan()` so that it is not cleared
+      // again.
+      silent_ = true;
     }
   }
 
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index e199223..46d2f176 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -3032,6 +3032,13 @@
       status: "experimental",
     },
     {
+      // Kill switch for setting `display: none` on the media controls
+      // container when it is not needed.
+      // Lands in M147 and can be removed in M149. https://crbug.com/40591804
+      name: "HideVideoControlsWhenUnneeded",
+      status: "stable",
+    },
+    {
       name: "HighlightPointerEvents",
     },
     {
@@ -5290,6 +5297,12 @@
       base_feature: "none",
     },
     {
+      // Kill switch for falsifying IsStacked() for non-stacked
+      // stacking contexts. https://crbug.com/40208685
+      name: "StackingContextIsNotStacked",
+      status: "stable",
+    },
+    {
       // See https://github.com/w3c/csswg-drafts/issues/9398
       name: "StandardizedBrowserZoom",
       status: "stable",
diff --git a/third_party/blink/renderer/platform/scheduler/common/event_loop.cc b/third_party/blink/renderer/platform/scheduler/common/event_loop.cc
index 7f1228c..34be9c7 100644
--- a/third_party/blink/renderer/platform/scheduler/common/event_loop.cc
+++ b/third_party/blink/renderer/platform/scheduler/common/event_loop.cc
@@ -17,10 +17,10 @@
 namespace blink {
 namespace scheduler {
 
-EventLoop::PauseMicrotasksHandle::PauseMicrotasksHandle(
-    v8::Isolate* isolate,
-    v8::MicrotaskQueue* queue)
-    : scope_(isolate, queue) {}
+EventLoop::PauseMicrotasksHandle::~PauseMicrotasksHandle() {
+  CHECK_GT(loop_->microtasks_pause_count_, 0);
+  --loop_->microtasks_pause_count_;
+}
 
 EventLoop::EventLoop(Delegate* delegate,
                      v8::Isolate* isolate,
@@ -74,8 +74,9 @@
 }
 
 void EventLoop::PerformMicrotaskCheckpoint() {
-  if (ScriptForbiddenScope::IsScriptForbidden())
+  if (AreMicrotasksPaused() || ScriptForbiddenScope::IsScriptForbidden()) {
     return;
+  }
 
   microtask_queue_->PerformCheckpoint(isolate_);
 }
@@ -102,8 +103,7 @@
 }
 
 std::unique_ptr<EventLoop::PauseMicrotasksHandle> EventLoop::PauseMicrotasks() {
-  return base::WrapUnique(
-      new PauseMicrotasksHandle(isolate_, microtask_queue_.get()));
+  return base::WrapUnique(new PauseMicrotasksHandle(this));
 }
 
 // static
diff --git a/third_party/blink/renderer/platform/scheduler/public/event_loop.h b/third_party/blink/renderer/platform/scheduler/public/event_loop.h
index 4066b809..fc1e02c9 100644
--- a/third_party/blink/renderer/platform/scheduler/public/event_loop.h
+++ b/third_party/blink/renderer/platform/scheduler/public/event_loop.h
@@ -98,21 +98,24 @@
 
   bool IsSchedulerAttachedForTest(FrameOrWorkerScheduler*);
 
-  class PauseMicrotasksHandle {
+  class PLATFORM_EXPORT PauseMicrotasksHandle {
    public:
-    ~PauseMicrotasksHandle() = default;
+    ~PauseMicrotasksHandle();
 
    private:
     friend class EventLoop;
-    PauseMicrotasksHandle(v8::Isolate* isolate, v8::MicrotaskQueue* queue);
-
-    v8::Isolate::SuppressMicrotaskExecutionScope scope_;
+    explicit PauseMicrotasksHandle(scoped_refptr<EventLoop> loop)
+        : loop_(std::move(loop)) {
+      ++loop_->microtasks_pause_count_;
+    }
+    scoped_refptr<EventLoop> loop_;
   };
 
   // Suppresses microtask execution for the lifetime of the returned handle.
   // Pending microtasks would be executed as soon as all issued handles go
   // out of scope.
   [[nodiscard]] std::unique_ptr<PauseMicrotasksHandle> PauseMicrotasks();
+  bool AreMicrotasksPaused() const { return !!microtasks_pause_count_; }
 
  private:
   friend class RefCounted<EventLoop>;
@@ -127,7 +130,8 @@
   static void RunEndOfCheckpointTasks(v8::Isolate* isolat, void* data);
 
   WeakPersistent<Delegate> delegate_;
-  raw_ptr<v8::Isolate> isolate_;
+  const raw_ptr<v8::Isolate> isolate_;
+  int microtasks_pause_count_ = 0;
   bool loop_enabled_ = true;
   Deque<base::OnceClosure> pending_microtasks_;
   Vector<base::OnceClosure> end_of_checkpoint_tasks_;
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/multicol/multicol-gap-decorations-repaint-on-content-resize-ref.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/multicol/multicol-gap-decorations-repaint-on-content-resize-ref.html
new file mode 100644
index 0000000..766bd2f4
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/multicol/multicol-gap-decorations-repaint-on-content-resize-ref.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>CSS Gap Decorations: repaint when multicol content height changes (reference)</title>
+  <link rel="author" title="Kevin Babbitt" href="mailto:kbabbitt@microsoft.com">
+  <style>
+    .multicol-container {
+      columns: 2;
+      column-fill: auto;
+      column-gap: 20px;
+      width: 160px;
+      height: 50px;
+      background: red;
+      column-rule: 20px solid green;
+    }
+
+    .content {
+      background: green;
+      /* Final height after change */
+      height: 100px;
+    }
+  </style>
+</head>
+<body>
+  <p>Test passes if there is a filled green rectangle and <strong>no red</strong>.</p>
+  <div class="multicol-container">
+    <div class="content"></div>
+  </div>
+</body>
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/multicol/multicol-gap-decorations-repaint-on-content-resize.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/multicol/multicol-gap-decorations-repaint-on-content-resize.html
new file mode 100644
index 0000000..71ee2331
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/multicol/multicol-gap-decorations-repaint-on-content-resize.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+  <title>CSS Gap Decorations: repaint when multicol content height changes</title>
+  <link rel="help" href="https://drafts.csswg.org/css-gaps-1/">
+  <link rel="match" href="multicol-gap-decorations-repaint-on-content-resize-ref.html">
+  <link rel="author" title="Kevin Babbitt" href="mailto:kbabbitt@microsoft.com">
+  <meta name="assert" content="Gap decorations repaint correctly when multicol content height changes, causing column rule positions to update">
+  <style>
+    .multicol-container {
+      columns: 2;
+      column-fill: auto;
+      column-gap: 20px;
+      width: 160px;
+      height: 50px;
+      background: red;
+      column-rule: 20px solid green;
+    }
+
+    .content {
+      background: green;
+      /* Start at 30px (fits in one column), will change to 100px (overflows
+         into second column). The column rule should repaint at the correct
+         position between the two columns. */
+      height: 30px;
+    }
+  </style>
+</head>
+<body>
+  <p>Test passes if there is a filled green rectangle and <strong>no red</strong>.</p>
+  <div class="multicol-container">
+    <div id="content" class="content"></div>
+  </div>
+  <script>
+    // Use double requestAnimationFrame to ensure style is computed and painted.
+    requestAnimationFrame(() => {
+      requestAnimationFrame(() => {
+        // Change the content height so it overflows into a second column.
+        // The column rule should repaint between the two columns.
+        document.getElementById('content').style.height = '100px';
+        document.documentElement.classList.remove("reftest-wait");
+      });
+    });
+  </script>
+</body>
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/html/rendering/replaced-elements/embedded-content/video-stacking-hit-test.html b/third_party/blink/web_tests/external/wpt/html/rendering/replaced-elements/embedded-content/video-stacking-hit-test.html
new file mode 100644
index 0000000..26fb86f
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/rendering/replaced-elements/embedded-content/video-stacking-hit-test.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<link rel="author" title="jj" href="mailto:jj@imput.net">
+<link rel="help" href="https://issues.chromium.org/issues/40591804">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+button { position: absolute; width: 200px; height: 100px }
+video { background: red; width: 300px; height: 300px }
+</style>
+<button>Button over video</button>
+<video></video>
+<script>
+test(() => {
+    const button = document.querySelector('button');
+    const { x, y } = button.getBoundingClientRect();
+
+    const hit = document.elementFromPoint(x + 1, y + 1);
+    assert_equals(hit, button, "Hit test in the button area should hit the button");
+});
+</script>
diff --git a/third_party/blink/web_tests/platform/ios/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt b/third_party/blink/web_tests/platform/ios/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
index 35d15777..7d0c47ba 100644
--- a/third_party/blink/web_tests/platform/ios/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
+++ b/third_party/blink/web_tests/platform/ios/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
@@ -243,12 +243,6 @@
       "transform": 1
     },
     {
-      "name": "LayoutFlexibleBox DIV class='sizing-small phase-pre-ready state-no-source'",
-      "bounds": [150, 60],
-      "drawsContent": false,
-      "transform": 1
-    },
-    {
       "name": "VerticalScrollbar",
       "position": [796, 0],
       "bounds": [4, 600]
diff --git a/third_party/blink/web_tests/platform/mac/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt b/third_party/blink/web_tests/platform/mac/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
index 71af47a..4d2eaea 100644
--- a/third_party/blink/web_tests/platform/mac/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
@@ -243,12 +243,6 @@
       "transform": 1
     },
     {
-      "name": "LayoutFlexibleBox DIV class='sizing-small phase-pre-ready state-no-source'",
-      "bounds": [150, 60],
-      "drawsContent": false,
-      "transform": 1
-    },
-    {
       "name": "VerticalScrollbar",
       "position": [785, 0],
       "bounds": [15, 600],
diff --git a/third_party/blink/web_tests/platform/win/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt b/third_party/blink/web_tests/platform/win/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
index ad18344b..23521d2 100644
--- a/third_party/blink/web_tests/platform/win/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
+++ b/third_party/blink/web_tests/platform/win/paint/invalidation/image/canvas-composite-repaint-by-all-imagesource-expected.txt
@@ -64,12 +64,6 @@
       "transform": 1
     },
     {
-      "name": "LayoutFlexibleBox DIV class='sizing-small phase-pre-ready state-no-source'",
-      "bounds": [150, 60],
-      "drawsContent": false,
-      "transform": 1
-    },
-    {
       "name": "VerticalScrollbar",
       "position": [785, 0],
       "bounds": [15, 600],
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/basic-rect-zoom.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/basic-rect-zoom.html
index 39055a9..fcfe79a 100644
--- a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/basic-rect-zoom.html
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/basic-rect-zoom.html
@@ -27,15 +27,19 @@
 </canvas>
 
 <script>
-function runTest() {
+async function runTest() {
+  await new Promise(requestAnimationFrame);
+  await new Promise(setTimeout);
   const rectangle = canvas.getBoundingClientRect();
   canvas.width = rectangle.width * devicePixelRatio;
   canvas.height = rectangle.height * devicePixelRatio;
+  await new Promise(requestAnimationFrame);
+  await new Promise(setTimeout);
   canvas.getContext("2d").drawElementImage(child, 20 * devicePixelRatio, 30 * devicePixelRatio);
   takeScreenshot();
 }
 
-onload = () => requestAnimationFrame(() => setTimeout(runTest));
+onload = runTest;
 
 </script>
 </html>
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/draw-element-image-detached.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/draw-element-image-detached.html
index d6ef7c1..ef6233d 100644
--- a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/draw-element-image-detached.html
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/draw-element-image-detached.html
@@ -24,9 +24,9 @@
     let ctx = canvas.getContext('2d');
     ctx.fillStyle = 'rgb(3, 4, 5)';
     ctx.fillRect(0, 0, 200, 200);
-    assert_throws_js(TypeError,
+    assert_throws_dom(
+      "InvalidStateError",
       () => ctx.drawElementImage(child, 20, 30),
       "Can't draw into a detached canvas.");
-
   }, 'canvas drawElementImage throws for a detached canvas due to lack of layout');
 </script>
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/draw-element-image-display-none.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/draw-element-image-display-none.html
index 74f0a07..5dd3610 100644
--- a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/draw-element-image-display-none.html
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/draw-element-image-display-none.html
@@ -31,13 +31,18 @@
 
 
 <script>
-promise_test(async () => {
-  assert_throws_js(TypeError,
-    () => canvas1.getContext("2d").drawElementImage(child1, 20, 30),
-    "Can't draw a display: none child.");
-
-  assert_throws_js(TypeError,
-    () => canvas2.getContext("2d").drawElementImage(child2, 20, 30),
-    "Can't draw into a display-none <canvas>.");
-}, "Canvas drawElementImage() should throw when not laid out.");
+onload = () => {
+  promise_test(async (t) => {
+    await new Promise(requestAnimationFrame);
+    await new Promise(setTimeout);
+    assert_throws_dom(
+      "InvalidStateError",
+      () => canvas1.getContext("2d").drawElementImage(child1, 20, 30),
+      "Can't draw a display: none child.");
+    assert_throws_dom(
+      "InvalidStateError",
+      () => canvas2.getContext("2d").drawElementImage(child2, 20, 30),
+      "Can't draw into a display-none <canvas>.");
+  }, "Canvas drawElementImage() should throw when drawn child is not laid out.");
+};
 </script>
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/draw-element-image-returned-matrix.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/draw-element-image-returned-matrix.html
index 4954fcc..fa0756f 100644
--- a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/draw-element-image-returned-matrix.html
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/draw-element-image-returned-matrix.html
@@ -61,10 +61,12 @@
   }, 'Draw transform should account for the current transform matrix with translation');
 
   promise_test(async function(t) {
-    t.add_cleanup(() => {
+    t.add_cleanup(async () => {
       canvas.style.zoom = '';
+      await waitOneFrame();
     });
     canvas.style.zoom = '1.5';
+    await waitOneFrame();
     let draw_transform = canvas.getContext('2d').drawElementImage(element, 10, 10);
     let expected = new DOMMatrix().translateSelf(10, 10);
     assert_array_equals(draw_transform.toFloat64Array(), expected.toFloat64Array());
@@ -112,12 +114,13 @@
   }, 'Draw transform should account for canvas pixel scale');
 
   promise_test(async function(t) {
-    t.add_cleanup(() => {
+    t.add_cleanup(async () => {
       canvas.width = 100;
       canvas.height = 100;
       canvas.style.zoom = '';
       canvas.getContext('2d').reset();
       element.style.transformOrigin = '0 0';
+      await waitOneFrame();
     });
     canvas.width = 200;
     canvas.height = 50;
@@ -126,6 +129,7 @@
     canvas.getContext('2d').rotate(Math.PI / 4);
     element.style.transformOrigin = '5px 5px';
     element.style.transformOrigin = '5px 5px';
+    await waitOneFrame();
     let draw_transform = canvas.getContext('2d').drawElementImage(element, 13, -9);
     let expected = new DOMMatrix()
       .translateSelf(-5, -5)
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/drawing-display-none-fails.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/drawing-display-none-fails.html
index e030cfb..390ee63 100644
--- a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/drawing-display-none-fails.html
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/drawing-display-none-fails.html
@@ -19,11 +19,10 @@
     await new Promise(requestAnimationFrame);
     await new Promise(setTimeout);
     const context_2d = canvas_2d.getContext('2d');
-    assert_throws_js(
-      TypeError,
+    assert_throws_dom(
+      "InvalidStateError",
       () => context_2d.drawElementImage(canvas_2d_div, 0, 0),
-      'drawElementImage cannot draw display:none elements.'
-    );
+      'drawElementImage cannot draw display:none elements.');
   }, 'drawElementImage with display:none element');
 
   promise_test(async function() {
@@ -38,12 +37,11 @@
     const internalformat = context_gl.RGBA;
     const format = context_gl.RGBA;
     const type = context_gl.UNSIGNED_BYTE;
-    assert_throws_js(
-      TypeError,
+    assert_throws_dom(
+      "InvalidStateError",
         () => context_gl.texElementImage2D(
             context_gl.TEXTURE_2D, level, internalformat, format, type,
             canvas_3d_div),
-      'texElementImage2D cannot draw display:none elements.'
-    );
+      'texElementImage2D cannot draw display:none elements.');
   }, 'texElementImage2D with display:none element');
 </script>
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/error-conditions.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/error-conditions.html
index c51f578..04103c1 100644
--- a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/error-conditions.html
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/error-conditions.html
@@ -42,6 +42,8 @@
     "Can't draw canvas children if layoutsubtree is not specified.");
 
   canvas.toggleAttribute("layoutsubtree"); // Add attribute.
+  await new Promise(requestAnimationFrame);
+  await new Promise(setTimeout);
   // Should not throw an exception.
   canvas.getContext("2d").drawElementImage(child, 20, 30);
 
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/get-element-transform.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/get-element-transform.html
index 77faeb8..d6cb835 100644
--- a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/get-element-transform.html
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/get-element-transform.html
@@ -8,21 +8,23 @@
   <div id="element" style="width: 10px; height: 10px;"></div>
 </canvas>
 <script>
-  test(() => {
+  promise_test(() => {
     let draw_transform = new DOMMatrix();
     let element_transform = canvas.getElementTransform(element, draw_transform);
     let expected = new DOMMatrix();
     assert_array_equals(element_transform.toFloat64Array(), expected.toFloat64Array());
+    return Promise.resolve();
   }, 'Element transform for identity draw transform should be an identity matrix');
 
-  test(() => {
+  promise_test(() => {
     let draw_transform = new DOMMatrix().translateSelf(17, -9);
     let element_transform = canvas.getElementTransform(element, draw_transform);
     let expected = new DOMMatrix().translateSelf(17, -9);
     assert_array_equals(element_transform.toFloat64Array(), expected.toFloat64Array());
+    return Promise.resolve();
   }, 'Element transform should account for translation in the input transform');
 
-  test((t) => {
+  promise_test((t) => {
     t.add_cleanup(() => {
       element.style.transformOrigin = '';
     });
@@ -31,9 +33,10 @@
     let element_transform = canvas.getElementTransform(element, draw_transform);
     let expected = new DOMMatrix().scaleSelf(2, 0.5);
     assert_array_equals(element_transform.toFloat64Array(), expected.toFloat64Array());
+    return Promise.resolve();
   }, 'Element transform should account for scale in the input transform');
 
-  test((t) => {
+  promise_test((t) => {
     t.add_cleanup(() => {
       element.style.transform = '';
     });
@@ -42,9 +45,10 @@
     let element_transform = canvas.getElementTransform(element, draw_transform);
     let expected = new DOMMatrix();
     assert_array_equals(element_transform.toFloat64Array(), expected.toFloat64Array());
+    return Promise.resolve();
   }, 'Element transform should not include transform on the element');
 
-  test((t) => {
+  promise_test((t) => {
     t.add_cleanup(() => {
       canvas.getContext('2d').reset();
     });
@@ -53,20 +57,26 @@
     let element_transform = canvas.getElementTransform(element, ctm);
     let expected = new DOMMatrix().translateSelf(7, 11);
     assert_array_equals(element_transform.toFloat64Array(), expected.toFloat64Array());
+    return Promise.resolve();
   }, 'Element transform should not automatically include the current transform matrix, but should pass it through');
 
-  test((t) => {
-    t.add_cleanup(() => {
+  promise_test(async (t) => {
+    t.add_cleanup(async () => {
       canvas.style.zoom = '';
+      await new Promise(requestAnimationFrame);
+      await new Promise(setTimeout);
     });
     canvas.style.zoom = '1.5';
+    await new Promise(requestAnimationFrame);
+    await new Promise(setTimeout);
     let draw_transform = new DOMMatrix().translateSelf(7, 11);
     let element_transform = canvas.getElementTransform(element, draw_transform);
     let expected = new DOMMatrix().translateSelf(7, 11);
     assert_array_equals(element_transform.toFloat64Array(), expected.toFloat64Array());
+    return Promise.resolve();
   }, 'Element transform should not be affected by zoom on the canvas');
 
-  test((t) => {
+  promise_test((t) => {
     t.add_cleanup(() => {
       element.style.transformOrigin = '';
     });
@@ -78,15 +88,20 @@
       .rotateSelf(45)
       .translateSelf(5, 5);
     assert_array_approx_equals(element_transform.toFloat64Array(), expected.toFloat64Array(), 0.001);
+    return Promise.resolve();
   }, 'Element transform should account for transform-origin');
 
-  test((t) => {
-    t.add_cleanup(() => {
+  promise_test(async (t) => {
+    t.add_cleanup(async () => {
       canvas.width = 100;
       canvas.height = 100;
+      await new Promise(requestAnimationFrame);
+      await new Promise(setTimeout);
     });
     canvas.width = 200;
     canvas.height = 50;
+    await new Promise(requestAnimationFrame);
+    await new Promise(setTimeout);
     let draw_transform = new DOMMatrix().translateSelf(7, 11);
     let element_transform = canvas.getElementTransform(element, draw_transform);
     let expected = new DOMMatrix()
@@ -94,16 +109,21 @@
       .translateSelf(7, 11)
       .scaleSelf(2, 0.5);
     assert_array_equals(element_transform.toFloat64Array(), expected.toFloat64Array());
+    return Promise.resolve();
   }, 'Element transform should account for canvas pixel scale');
 
-  test((t) => {
-    t.add_cleanup(() => {
+  promise_test(async(t) => {
+    t.add_cleanup(async() => {
+      await new Promise(requestAnimationFrame);
+      await new Promise(setTimeout);
       canvas.width = 100;
       canvas.height = 100;
       element.style.transformOrigin = '0 0';
     });
     canvas.width = 200;
     canvas.height = 50;
+    await new Promise(requestAnimationFrame);
+    await new Promise(setTimeout);
     let draw_transform = new DOMMatrix()
       .translateSelf(42, 17)
       .rotateSelf(45);
@@ -117,6 +137,7 @@
       .scaleSelf(2, 0.5)
       .translateSelf(5, 5);
     assert_array_approx_equals(element_transform.toFloat64Array(), expected.toFloat64Array(), 0.001);
+    return Promise.resolve();
   }, 'Element transform should account for canvas pixel scale, transform-origin, and a complex draw transform');
 </script>
 </html>
diff --git a/third_party/boringssl/src b/third_party/boringssl/src
index d0fcf49..16d1a81 160000
--- a/third_party/boringssl/src
+++ b/third_party/boringssl/src
@@ -1 +1 @@
-Subproject commit d0fcf49574b06de7090cba4ad8124df638a0134e
+Subproject commit 16d1a81e475be0b3f859eb2264642bc1b6501f6e
diff --git a/third_party/crabbyavif/BUILD.gn b/third_party/crabbyavif/BUILD.gn
index d6858d3..ff961a6 100644
--- a/third_party/crabbyavif/BUILD.gn
+++ b/third_party/crabbyavif/BUILD.gn
@@ -214,6 +214,7 @@
     "//third_party/libyuv",
     "//third_party/rust/libc/v0_2:lib",
   ]
+  no_clippy = true  # TODO(https://crbug.com/472355480)
 }
 
 source_set("header_files") {
diff --git a/third_party/dawn b/third_party/dawn
index 56b2aa2..ebc5ca4 160000
--- a/third_party/dawn
+++ b/third_party/dawn
@@ -1 +1 @@
-Subproject commit 56b2aa287dbb7ce4f73aace630ace091db714f96
+Subproject commit ebc5ca4df7447bb12bce11975a9f9fed8e379131
diff --git a/third_party/devtools-frontend/src b/third_party/devtools-frontend/src
index eb79da0..0d56199 160000
--- a/third_party/devtools-frontend/src
+++ b/third_party/devtools-frontend/src
@@ -1 +1 @@
-Subproject commit eb79da005f6e25c84235e6f077d25a12d8533a57
+Subproject commit 0d56199f472ca4a345cc7e2d364b241c7ec01a66
diff --git a/third_party/lens_server_proto/modality_chip_props.proto b/third_party/lens_server_proto/modality_chip_props.proto
index 35591dd..64f7dac 100644
--- a/third_party/lens_server_proto/modality_chip_props.proto
+++ b/third_party/lens_server_proto/modality_chip_props.proto
@@ -21,6 +21,9 @@
 
     // Thumbnail - in a bitmap format.
     bytes thumbnail_bitmap = 13;
+
+    // Icon identifier for the chip.
+    IconType icon_id = 16;
   }
 
   AddedInput added_input = 10;
@@ -30,3 +33,17 @@
 
   reserved 3 to 9, 12;
 }
+
+// Next ID: 157
+enum IconType {
+  ICON_TYPE_UNSPECIFIED = 0;
+  ICON_TYPE_ADD = 9;
+  ICON_TYPE_CHECK = 30;
+  ICON_TYPE_FORMAT_QUOTE_FILLED = 79;
+  ICON_TYPE_IMAGE = 84;
+  ICON_TYPE_DRIVE_PDF = 101;
+
+  // Reserved values are icon types that have been defined on the server but
+  // have not yet been added to Chrome. Unreserve when adding a new icon type.
+  reserved 1 to 8, 10 to 29, 31 to 78, 80 to 83, 85 to 100, 102 to 156;
+}
diff --git a/third_party/libvpx/README.chromium b/third_party/libvpx/README.chromium
index eaaa4be..7486cc3 100644
--- a/third_party/libvpx/README.chromium
+++ b/third_party/libvpx/README.chromium
@@ -1,7 +1,7 @@
 Name: libvpx
 URL: https://chromium.googlesource.com/webm/libvpx
 Version: N/A
-Revision: 4fcebeabe58e79255d291acb4cead4ed7953149e
+Revision: 9a2d3d1f46afbdfa9b9820a9fd3aacb084e65e2f
 Update Mechanism: Manual
 CPEPrefix: cpe:/a:webmproject:libvpx:1.16.0
 License: BSD-3-Clause, Patent
diff --git a/third_party/libvpx/source/config/vpx_version.h b/third_party/libvpx/source/config/vpx_version.h
index b4157eff..1c17b42 100644
--- a/third_party/libvpx/source/config/vpx_version.h
+++ b/third_party/libvpx/source/config/vpx_version.h
@@ -4,8 +4,8 @@
 #define VERSION_MAJOR  1
 #define VERSION_MINOR  16
 #define VERSION_PATCH  0
-#define VERSION_EXTRA  "56-g4fcebeabe"
+#define VERSION_EXTRA  "58-g9a2d3d1f4"
 #define VERSION_PACKED ((VERSION_MAJOR<<16)|(VERSION_MINOR<<8)|(VERSION_PATCH))
-#define VERSION_STRING_NOSP "v1.16.0-56-g4fcebeabe"
-#define VERSION_STRING      " v1.16.0-56-g4fcebeabe"
+#define VERSION_STRING_NOSP "v1.16.0-58-g9a2d3d1f4"
+#define VERSION_STRING      " v1.16.0-58-g9a2d3d1f4"
 #endif  // VPX_VERSION_H_
diff --git a/third_party/libvpx/source/libvpx b/third_party/libvpx/source/libvpx
index 4fcebea..9a2d3d1 160000
--- a/third_party/libvpx/source/libvpx
+++ b/third_party/libvpx/source/libvpx
@@ -1 +1 @@
-Subproject commit 4fcebeabe58e79255d291acb4cead4ed7953149e
+Subproject commit 9a2d3d1f46afbdfa9b9820a9fd3aacb084e65e2f
diff --git a/third_party/perfetto b/third_party/perfetto
index 21c7778..05b8b9cf 160000
--- a/third_party/perfetto
+++ b/third_party/perfetto
@@ -1 +1 @@
-Subproject commit 21c77783aa90ba9b46e79949c797ca6bc516c124
+Subproject commit 05b8b9cf93e16e960e71f38bf2e41944b4611f22
diff --git a/third_party/skia b/third_party/skia
index ddff74e..c16d0e9 160000
--- a/third_party/skia
+++ b/third_party/skia
@@ -1 +1 @@
-Subproject commit ddff74ea6faeb0590bdab8ff99ec9c57a0dd5281
+Subproject commit c16d0e9f30b1a1613401c0db3c93a3c2aa37c8ba
diff --git a/third_party/spirv-tools/src b/third_party/spirv-tools/src
index 4972c69..c28f593 160000
--- a/third_party/spirv-tools/src
+++ b/third_party/spirv-tools/src
@@ -1 +1 @@
-Subproject commit 4972c69eb50255b314fc0925ca757c4417e6b6c0
+Subproject commit c28f5937bce369dde1d645299a8c9873da43dc72
diff --git a/third_party/vulkan-deps b/third_party/vulkan-deps
index 158abcd5..4267dde 160000
--- a/third_party/vulkan-deps
+++ b/third_party/vulkan-deps
@@ -1 +1 @@
-Subproject commit 158abcd52e365129a0b81b455947e88a646e5691
+Subproject commit 4267dde11f3f6bef6b3aad0c5c3ba69723224ef6
diff --git a/third_party/vulkan-validation-layers/src b/third_party/vulkan-validation-layers/src
index f020266..c363aa3 160000
--- a/third_party/vulkan-validation-layers/src
+++ b/third_party/vulkan-validation-layers/src
@@ -1 +1 @@
-Subproject commit f020266adee4bb87e8fde219f6fb31f8f141213e
+Subproject commit c363aa381dedb3164e7cf5a22c4744b179cd16fe
diff --git a/tools/clang/blink_gc_plugin/RecordInfo.cpp b/tools/clang/blink_gc_plugin/RecordInfo.cpp
index e47ae8f..d996b7d 100644
--- a/tools/clang/blink_gc_plugin/RecordInfo.cpp
+++ b/tools/clang/blink_gc_plugin/RecordInfo.cpp
@@ -162,8 +162,7 @@
       if (!type)
         base = GetDependentTemplatedDecl(*it.getType());
       else {
-        base = cast_or_null<CXXRecordDecl>(
-            type->getOriginalDecl()->getDefinition());
+        base = cast_or_null<CXXRecordDecl>(type->getDecl()->getDefinition());
         if (base)
           queue.push_back(base);
       }
diff --git a/tools/clang/plugins/CheckIPCVisitor.cpp b/tools/clang/plugins/CheckIPCVisitor.cpp
index e4583151c..2184fc0d 100644
--- a/tools/clang/plugins/CheckIPCVisitor.cpp
+++ b/tools/clang/plugins/CheckIPCVisitor.cpp
@@ -230,8 +230,8 @@
     }
 
     if (auto* record = dyn_cast<RecordType>(type)) {
-      if (auto* spec = dyn_cast<ClassTemplateSpecializationDecl>(
-              record->getOriginalDecl())) {
+      if (auto* spec =
+              dyn_cast<ClassTemplateSpecializationDecl>(record->getDecl())) {
         const TemplateArgumentList& args = spec->getTemplateArgs();
         for (unsigned i = 0; i != args.size(); ++i) {
           if (!CheckTemplateArgument(args[i], details)) {
diff --git a/tools/clang/plugins/FindBadConstructsConsumer.cpp b/tools/clang/plugins/FindBadConstructsConsumer.cpp
index 54e3b08..57187ce 100644
--- a/tools/clang/plugins/FindBadConstructsConsumer.cpp
+++ b/tools/clang/plugins/FindBadConstructsConsumer.cpp
@@ -1015,8 +1015,8 @@
     return false;
   }
 
-  CXXRecordDecl* record = dyn_cast<CXXRecordDecl>(
-      base->getType()->getAs<RecordType>()->getOriginalDecl());
+  CXXRecordDecl* record =
+      dyn_cast<CXXRecordDecl>(base->getType()->getAs<RecordType>()->getDecl());
   SourceLocation unused;
   return None != CheckRecordForRefcountIssue(record, unused);
 }
@@ -1132,7 +1132,7 @@
     // The record with the problem will always be the last record
     // in the path, since it is the record that stopped the search.
     const CXXRecordDecl* problem_record = dyn_cast<CXXRecordDecl>(
-        it->back().Base->getType()->getAs<RecordType>()->getOriginalDecl());
+        it->back().Base->getType()->getAs<RecordType>()->getDecl());
 
     issue = CheckRecordForRefcountIssue(problem_record, loc);
 
@@ -1399,7 +1399,7 @@
   }
 
   value_expr = value_expr->IgnoreParens();
-  if (auto* lit_expr = clang::dyn_cast<clang::StringLiteral>(value_expr)) {
+  if (clang::isa<clang::StringLiteral>(value_expr)) {
     ReportIfSpellingLocNotIgnored(loc, diag_span_from_string_literal_);
     ReportIfSpellingLocNotIgnored(loc, diag_note_span_from_string_literal1_);
   }
diff --git a/tools/clang/plugins/SuppressibleDiagnosticBuilder.h b/tools/clang/plugins/SuppressibleDiagnosticBuilder.h
index bd3ffa4..ad11b36 100644
--- a/tools/clang/plugins/SuppressibleDiagnosticBuilder.h
+++ b/tools/clang/plugins/SuppressibleDiagnosticBuilder.h
@@ -21,7 +21,6 @@
                                 unsigned diagnostic_id,
                                 bool suppressed)
       : DiagnosticBuilder(diagnostics->Report(loc, diagnostic_id)),
-        diagnostics_(diagnostics),
         suppressed_(suppressed) {}
 
   ~SuppressibleDiagnosticBuilder() {
@@ -33,7 +32,6 @@
   }
 
  private:
-  clang::DiagnosticsEngine* const diagnostics_;
   const bool suppressed_;
 };
 
diff --git a/tools/clang/raw_ptr_plugin/TypePredicateUtil.h b/tools/clang/raw_ptr_plugin/TypePredicateUtil.h
index 816ad77..7c79f58 100644
--- a/tools/clang/raw_ptr_plugin/TypePredicateUtil.h
+++ b/tools/clang/raw_ptr_plugin/TypePredicateUtil.h
@@ -163,7 +163,7 @@
 
     // Clean-up: this lambda is called automatically at the scope exit.
     const auto clean_up =
-        llvm::make_scope_exit([this, &visited, &raw_type, &root, &match] {
+        llvm::scope_exit([this, &visited, &raw_type, &root, &match] {
           if (root) {
             delete visited;
           }
diff --git a/tools/json_schema_compiler/extensions_webidl_conversion.md b/tools/json_schema_compiler/extensions_webidl_conversion.md
index 97fe693c..bf17547 100644
--- a/tools/json_schema_compiler/extensions_webidl_conversion.md
+++ b/tools/json_schema_compiler/extensions_webidl_conversion.md
@@ -149,17 +149,23 @@
 ```
 Descriptive comments above the whole enum should be moved along with them.
 
-### Referencing Types from other Namespaces
-Some schemas currently reference Types which are defined in other schema files, which are indicated by the Type name being prefixed with the "namespace" of the API it is defined in, followed by a period and then the the name of the Dictionary or Enum it is referencing e.g. `extensionTypes.FrameType` or `tabs.Tab`. Since WebIDL does not allow Type names to contain periods these need to be updated during conversion.
-If a schema uses a Type like this, we instead create a local `Typedef object` at the top level of the file that uses a name combining the capitalized namespace and Type name and gives it an `ExternalExtensionType=]` extended attribute with the original string used for the type.
-e.g.
+### Typedefs for Referencing External Types or Local Aliasing
+WebIDL does not allow Type names to contain periods, so any references to Types defined in other schema files (e.g. `extensionTypes.FrameType` or `tabs.Tab`) must be updated. We handle this by creating a local `Typedef object` at the top level of the file with a name combining the capitalized namespace and Type name, and giving it an `ExternalExtensionType=]` extended attribute with the original string.
+
+Typedefs can also be used to create local aliases for types when you want to apply specific extended attributes to them, such as `[instanceOf=...]`, which is useful when they need to be used in places where extended attributes are not normally allowed by the IDL parser (e.g. on the Type of a `Promise<Type>`).
+
+If a Typedef has an `ExternalExtensionType` extended attribute it is treated as an external type reference; otherwise it is treated as a local alias and the underlying type of the Typedef is used, along with any extended attributes on it.
+
+**Example of External Type:**
 ```
+// Old .idl file
 dictionary contentScripts {
   extensionTypes.RunAt? run_at;
 }
 ```
 Would become:
 ```
+// New .webidl file
 [ExternalExtensionType="extensionTypes.RunAt"]
 typedef object ExtensionTypesRunAt;
 
@@ -168,6 +174,20 @@
 }
 ```
 
+**Example of Local Alias with instanceOf:**
+```
+// Old .idl file
+callback BlobCallback = void([instanceOf=Blob] object blob);
+```
+Would become:
+```
+// New .webidl file
+[instanceOf=Blob]
+typedef object Blob;
+
+[requiredCallback] static Promise<Blob> getBlob();
+```
+
 ### Functions and Callbacks to Promises
 All functions that used a trailing callback must be converted to return a `Promise`.
 * Return Type: The function's return type changes from `static void` to `static Promise<T>`.
diff --git a/tools/json_schema_compiler/test/web_idl/typedef_alias.idl b/tools/json_schema_compiler/test/web_idl/typedef_alias.idl
new file mode 100644
index 0000000..167429f
--- /dev/null
+++ b/tools/json_schema_compiler/test/web_idl/typedef_alias.idl
@@ -0,0 +1,17 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[instanceOf=File]
+typedef object FileAlias;
+
+typedef long LongAlias;
+
+interface TypedefAlias {
+  static undefined takesFileAlias(FileAlias file);
+  static undefined takesLongAlias(LongAlias count);
+};
+
+partial interface Browser {
+  static attribute TypedefAlias typedefAlias;
+};
diff --git a/tools/json_schema_compiler/test/web_idl/typedef_missing_extended_attribute.idl b/tools/json_schema_compiler/test/web_idl/typedef_missing_extended_attribute.idl
deleted file mode 100644
index d9a4f19..0000000
--- a/tools/json_schema_compiler/test/web_idl/typedef_missing_extended_attribute.idl
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2026 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-typedef object BasicsExampleType;
-
-// Schema which is invalid due to the typedef above missing the
-// ExternalExtensionType extended attribute.
-interface InvalidSharedTypes {
-  static undefined sharedTypeFunction(BasicsExampleType sharedTypeParam);
-};
-
-partial interface Browser {
-  static attribute InvalidSharedTypes invalidSharedTypes;
-};
diff --git a/tools/json_schema_compiler/web_idl_schema.py b/tools/json_schema_compiler/web_idl_schema.py
index 6247b1f9..0a0689b 100755
--- a/tools/json_schema_compiler/web_idl_schema.py
+++ b/tools/json_schema_compiler/web_idl_schema.py
@@ -427,25 +427,24 @@
         elif referenced_type.GetClass() == 'Callback':
           properties = Operation(referenced_type).process()
         elif referenced_type.GetClass() == 'Typedef':
-          # For now, we only use Typedefs to allow for referencing types which
-          # are defined in another schema file. These act almost like a forward
-          # declaration and we extract the "namespaced" type name to use from
-          # the extended attribute on the Typedef.
-          # TODO(crbug.com/486928682): Eventually it would be good to follow the
-          # way Blink does this, by having shared types use globally unique
-          # names and be defined in their own files. Then all the relevant type
-          # files for an API schema could also be passed to the IDL parser and
-          # our `referenced_type` code above could search through those.
-          shared_type_name = GetExtendedAttributeValue(referenced_type,
-                                                       'ExternalExtensionType')
-          if shared_type_name is None:
-            raise SchemaCompilerError(
-                'Typedefs can only be used for declaring shared Types'
-                ' referencing Types defined in other API namespaces, but one'
-                ' was found which was missing the required'
-                ' "ExternalExtensionType=" extended attribute.',
-                referenced_type)
-          properties['$ref'] = shared_type_name
+          # Typedefs can be used for declaring shared Types referencing Types
+          # defined in other API namespaces, or for defining local aliases
+          # with specific extended attributes like [instanceOf].
+          if shared_type_name := GetExtendedAttributeValue(
+              referenced_type, 'ExternalExtensionType'):
+            # TODO(crbug.com/486928682): Eventually it would be good to follow
+            # the way Blink does this, by having shared types use globally
+            # unique names and be defined in their own files. Then all the
+            # relevant type files for an API schema could also be passed to the
+            # IDL parser and our `referenced_type` code above could search
+            # through those.
+            properties['$ref'] = shared_type_name
+          else:
+            # If it's not an external type, we process the underlying type of
+            # the typedef and apply any extended attributes from the typedef
+            # itself.
+            typedef_type_node = referenced_type.GetOneOf('Type')
+            properties.update(Type(typedef_type_node).Process())
 
         else:
           raise SchemaCompilerError(
diff --git a/tools/json_schema_compiler/web_idl_schema_test.py b/tools/json_schema_compiler/web_idl_schema_test.py
index ae72034..3334574 100755
--- a/tools/json_schema_compiler/web_idl_schema_test.py
+++ b/tools/json_schema_compiler/web_idl_schema_test.py
@@ -1344,19 +1344,29 @@
     self.assertEqual('LocalType', schema['types'][0]['id'])
 
   # Tests that a Typedef in a schema without the ExternalExtensionType extended
-  # attribute throws an error.
-  def testTypedefMissingExternalExtensionTypeExtendedAttribute(self):
-    expected_error_regex = (
-        r'.* Error processing node Typedef\(BasicsExampleType\): Typedefs can'
-        r' only be used for declaring shared Types referencing Types defined in'
-        r' other API namespaces, but one was found which was missing the'
-        r' required "ExternalExtensionType=" extended attribute.')
-    self.assertRaisesRegex(
-        SchemaCompilerError,
-        expected_error_regex,
-        web_idl_schema.Load,
-        'test/web_idl/typedef_missing_extended_attribute.idl',
-    )
+  # attribute is processed as a local alias of the underlying type.
+  def testTypedefAliases(self):
+    idl = web_idl_schema.Load('test/web_idl/typedef_alias.idl')
+    self.assertEqual(1, len(idl))
+    schema = idl[0]
+
+    # takesFileAlias(FileAlias file)
+    # FileAlias is a typedef of object with [instanceOf=File]
+    file_alias_params = getFunctionParameters(schema, 'takesFileAlias')
+    self.assertEqual(
+        {
+            'name': 'file',
+            'type': 'object',
+            'additionalProperties': {
+                'type': 'any'
+            },
+            'isInstanceOf': 'File'
+        }, file_alias_params[0])
+
+    # takesLongAlias(LongAlias count)
+    # LongAlias is a typedef of long
+    long_alias_params = getFunctionParameters(schema, 'takesLongAlias')
+    self.assertEqual({'name': 'count', 'type': 'integer'}, long_alias_params[0])
 
   # Tests Manifest keys defined on a partial 'ExtensionManifest' dictionary are
   # extracted and put into the manifest keys details and not into the Types.
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index e3b165a..d26fe679 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -2762,6 +2762,7 @@
   <int value="38" label="RESULT_CODE_NORMAL_EXIT_AUTO_DE_ELEVATED"/>
   <int value="39"
       label="RESULT_CODE_TERMINATED_BY_OTHER_PROCESS_ON_COMMIT_FAILURE"/>
+  <int value="40" label="RESULT_CODE_INVALID_ISOLATED_BROWSER_PROCESS"/>
   <int value="131" label="SIGQUIT"/>
   <int value="132" label="SIGILL"/>
   <int value="133" label="SIGTRAP"/>
@@ -15638,6 +15639,7 @@
   <int value="403288255" label="enable-wheel-scroll-latching"/>
   <int value="403554154" label="SafetyCheckWeakPasswords:enabled"/>
   <int value="404176987" label="TabSwitcherColorBlendAnimate:enabled"/>
+  <int value="404546086" label="FullscreenVideoPictureInPicture:enabled"/>
   <int value="405068544" label="GlobalMediaControlsUpdatedUI:enabled"/>
   <int value="405329388"
       label="FramebustingNeedsSameOriginOrUserGesture:enabled"/>
@@ -16080,6 +16082,7 @@
   <int value="565002365" label="QuickOfficeForceFileDownload:disabled"/>
   <int value="565254510" label="LensRegionSearchStaticPage:disabled"/>
   <int value="565406673" label="EnableVirtualKeyboardMdUi:enabled"/>
+  <int value="565677129" label="AISummarizationPerformancePreference:enabled"/>
   <int value="566031886" label="DIPS:disabled"/>
   <int value="566805495" label="FriendlierErrorDialog:enabled"/>
   <int value="567368307" label="enable-experimental-canvas-features"/>
@@ -18687,6 +18690,7 @@
   <int value="1444634753" label="OsFeedbackJelly:disabled"/>
   <int value="1444719196"
       label="CellularBypassESimInstallationConnectivityCheck:enabled"/>
+  <int value="1445391138" label="FullscreenVideoPictureInPicture:disabled"/>
   <int value="1445634746"
       label="AutofillThirdPartyModeContentProvider:enabled"/>
   <int value="1445768096" label="ForceEnableWebGpuInterop:disabled"/>
@@ -20742,6 +20746,8 @@
   <int value="2132595171" label="OmniboxSearchEngineLogo:enabled"/>
   <int value="2133594095" label="CryptAuthV2DeviceActivityStatus:disabled"/>
   <int value="2133647636" label="ClipboardFilenames:disabled"/>
+  <int value="2133657265"
+      label="AISummarizationPerformancePreference:disabled"/>
   <int value="2134480727" label="MediaSessionAccelerators:disabled"/>
   <int value="2135059376" label="WiFiDirect:enabled"/>
   <int value="2135408204" label="OverscrollHistoryNavigation:disabled"/>
diff --git a/tools/metrics/histograms/metadata/autofill/histograms.xml b/tools/metrics/histograms/metadata/autofill/histograms.xml
index 5c7fb99c..78450b2 100644
--- a/tools/metrics/histograms/metadata/autofill/histograms.xml
+++ b/tools/metrics/histograms/metadata/autofill/histograms.xml
@@ -56,6 +56,8 @@
 <!-- LINT.IfChange(Autofill.Ai.EntityRecordType) -->
 
 <variants name="Autofill.Ai.EntityRecordType">
+  <variant name=".AccessibilityAnnotator"
+      summary="Emitted for Accessibility Annotator entities only."/>
   <variant name=".Local" summary="Emitted for local entities only."/>
   <variant name=".ServerWallet"
       summary="Emitted for Wallet server entities only."/>
diff --git a/tools/metrics/histograms/metadata/browser/histograms.xml b/tools/metrics/histograms/metadata/browser/histograms.xml
index bc035da1..8d8db398 100644
--- a/tools/metrics/histograms/metadata/browser/histograms.xml
+++ b/tools/metrics/histograms/metadata/browser/histograms.xml
@@ -2102,6 +2102,16 @@
   </summary>
 </histogram>
 
+<histogram name="InfoBar.Prioritization.Starved" enum="InfoBarIdentifier"
+    expires_after="2026-06-30">
+  <owner>koretadaniel@chromium.org</owner>
+  <owner>top-chrome-desktop-ui@google.com</owner>
+  <summary>
+    Records the identifier of pending infobars that were discarded (starved)
+    when the InfobarManager was reset or the tab was closed.
+  </summary>
+</histogram>
+
 <histogram name="InfoBar.Prioritization.StarvedCount" units="InfoBars"
     expires_after="2026-08-30">
   <owner>koretadaniel@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/extensions/histograms.xml b/tools/metrics/histograms/metadata/extensions/histograms.xml
index 42a4d1b..31562df 100644
--- a/tools/metrics/histograms/metadata/extensions/histograms.xml
+++ b/tools/metrics/histograms/metadata/extensions/histograms.xml
@@ -1590,17 +1590,6 @@
   </summary>
 </histogram>
 
-<histogram name="Extensions.DidInitializeServiceWorkerContextOnWorkerThread2"
-    units="ms" expires_after="2025-06-01">
-  <owner>rdevlin.cronin@chromium.org</owner>
-  <owner>extensions-core@chromium.org</owner>
-  <summary>
-    Records the time taken to install Extension JavaScript bindings per service
-    worker context. This is only emitted for the primary extension service
-    worker, as all other service workers do not receive extension bindings.
-  </summary>
-</histogram>
-
 <histogram name="Extensions.Disabled2" units="units" expires_after="never">
 <!-- expires-never: Used for monitoring user extension usage. -->
 
diff --git a/tools/metrics/histograms/metadata/mobile/enums.xml b/tools/metrics/histograms/metadata/mobile/enums.xml
index f1c66a73..9933563 100644
--- a/tools/metrics/histograms/metadata/mobile/enums.xml
+++ b/tools/metrics/histograms/metadata/mobile/enums.xml
@@ -581,6 +581,9 @@
   <int value="39" label="Report warned notification as spam button."/>
   <int value="40" label="Report unwarned notification as spam button."/>
   <int value="41" label="Download removed from history"/>
+  <int value="42" label="Agent paused"/>
+  <int value="43" label="Agent resumed"/>
+  <int value="44" label="Agent canceled"/>
 </enum>
 
 <enum name="SystemNotificationType">
@@ -627,6 +630,7 @@
   <int value="41" label="Tracing"/>
   <int value="42" label="Serial"/>
   <int value="43" label="Safety Check Revoked Notification Permissions"/>
+  <int value="44" label="Actor"/>
 </enum>
 
 </enums>
diff --git a/tools/metrics/histograms/metadata/mobile/histograms.xml b/tools/metrics/histograms/metadata/mobile/histograms.xml
index da6b561a..139c5f0 100644
--- a/tools/metrics/histograms/metadata/mobile/histograms.xml
+++ b/tools/metrics/histograms/metadata/mobile/histograms.xml
@@ -66,6 +66,7 @@
 </variants>
 
 <variants name="SystemNotificationAgeType">
+  <variant name=".Actor" summary="Actor"/>
   <variant name=".ClickToCall" summary="Click To Call"/>
   <variant name=".PriceDropChromeManaged"
       summary="Chrome Managed Price Drop Alerts"/>
@@ -731,6 +732,7 @@
 <!-- LINT.IfChange(NotificationChannelId) -->
 
   <token key="NotificationChannelId">
+    <variant name="Actor"/>
     <variant name="Announcement"/>
     <variant name="Bluetooth"/>
     <variant name="Browser"/>
diff --git a/tools/perf/core/bot_platforms.py b/tools/perf/core/bot_platforms.py
index a258d36c..237b339 100644
--- a/tools/perf/core/bot_platforms.py
+++ b/tools/perf/core/bot_platforms.py
@@ -1809,7 +1809,7 @@
   return int(v)
 
 # TODO(cbruni): use this to generate all perf configs.
-_USE_CSV_SCHEDULE_FILES: Final[bool] = False
+_USE_CSV_SCHEDULE_FILES: Final[bool] = True
 
 ALL_PLATFORMS: set[_PerfPlatform] = set()
 if _USE_CSV_SCHEDULE_FILES:
diff --git a/tools/win/setenv.py b/tools/win/setenv.py
index 14132c4..94e40b37 100644
--- a/tools/win/setenv.py
+++ b/tools/win/setenv.py
@@ -23,9 +23,9 @@
 else:
     vs_version = vs_toolchain.GetVisualStudioVersion()
     vs_path = vs_toolchain.DetectVisualStudioPath()
-    if vs_version in ['2022']:
+    if vs_version in ['2022', '2026']:
         script_path = os.path.join(vs_path,
                                    r'VC\Auxiliary\Build\vcvarsall.bat')
         print('"%s" amd64' % script_path)
     else:
-        raise Exception('Unknown VS version %s' % vs_version)
+        raise Exception('Unsupported VS version %s' % vs_version)
diff --git a/ui/accessibility/accessibility_features.cc b/ui/accessibility/accessibility_features.cc
index cfbe647..6569175 100644
--- a/ui/accessibility/accessibility_features.cc
+++ b/ui/accessibility/accessibility_features.cc
@@ -60,7 +60,7 @@
       ::features::kAccessibilityHandleOccludingViews);
 }
 
-BASE_FEATURE(kAccessibilityTextChangeTypes, base::FEATURE_DISABLED_BY_DEFAULT);
+BASE_FEATURE(kAccessibilityTextChangeTypes, base::FEATURE_ENABLED_BY_DEFAULT);
 bool IsAccessibilityTextChangeTypesEnabled() {
   return base::FeatureList::IsEnabled(
       ::features::kAccessibilityTextChangeTypes);
diff --git a/ui/accessibility/android/BUILD.gn b/ui/accessibility/android/BUILD.gn
index 90a4472..99f511f5 100644
--- a/ui/accessibility/android/BUILD.gn
+++ b/ui/accessibility/android/BUILD.gn
@@ -9,7 +9,10 @@
 android_aidl("accessibility_test_service_aidl") {
   testonly = true
   import_include = [ "javatest/src" ]
-  sources = [ "javatest/src/org/chromium/ui/accessibility/testservice/IAccessibilityTestHelperService.aidl" ]
+  sources = [
+    "javatest/src/org/chromium/ui/accessibility/testservice/EventQueryParams.aidl",
+    "javatest/src/org/chromium/ui/accessibility/testservice/IAccessibilityTestHelperService.aidl",
+  ]
 }
 
 android_apk("accessibility_test_support_apk") {
diff --git a/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/AccessibilityTestHelperService.java b/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/AccessibilityTestHelperService.java
index 3b288825f..0c6aaf3 100644
--- a/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/AccessibilityTestHelperService.java
+++ b/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/AccessibilityTestHelperService.java
@@ -16,18 +16,22 @@
     private final IAccessibilityTestHelperService.Stub mBinder =
             new IAccessibilityTestHelperService.Stub() {
                 @Override
-                public boolean waitForEvent(
-                        int eventType, String className, String text, long timeoutMs) {
+                public boolean waitForEvent(EventQueryParams params) {
                     Log.i(
                             TAG,
                             "waitForEvent called with type: "
-                                    + eventType
+                                    + params.eventType
                                     + ", class: "
-                                    + className
+                                    + params.className
                                     + ", text: "
-                                    + text);
-                    return AccessibilityTestService.tryWaitForEvent(
-                            eventType, className, text, timeoutMs);
+                                    + params.text);
+                    return AccessibilityTestService.tryWaitForEvent(params);
+                }
+
+                @Override
+                public boolean performActionOnNode(String className, String text, int action) {
+                    Log.i(TAG, "performActionOnNode called in HelperService");
+                    return AccessibilityTestService.tryPerformActionOnNode(className, text, action);
                 }
             };
 
diff --git a/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/AccessibilityTestService.java b/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/AccessibilityTestService.java
index 354dd2b..ca41561 100644
--- a/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/AccessibilityTestService.java
+++ b/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/AccessibilityTestService.java
@@ -40,14 +40,13 @@
         return sInstance;
     }
 
-    public static boolean tryWaitForEvent(
-            int eventType, String className, String text, long timeoutMs) {
+    public static boolean tryWaitForEvent(EventQueryParams params) {
         CompletableFuture<Boolean> eventFuture = new CompletableFuture<>();
         AccessibilityServiceListener listener =
                 new AccessibilityServiceListener() {
                     @Override
                     public void onAccessibilityEvent(AccessibilityEvent event) {
-                        if (eventMatches(event, eventType, className, text)) {
+                        if (eventMatches(event, params)) {
                             Log.i(TAG, "  Event MATCHED.");
                             eventFuture.complete(true);
                         }
@@ -58,7 +57,7 @@
             // Clear any previous listener, as waitForEvent claims exclusive rights.
             clearListenerLocked();
 
-            if (searchAndConsumeEventCacheLocked(eventType, className, text)) {
+            if (searchAndConsumeEventCacheLocked(params)) {
                 Log.i(TAG, "Found event in cache.");
                 return true;
             }
@@ -70,7 +69,7 @@
 
         // Did not find the event in the cache, so wait on the listener to return.
         try {
-            return eventFuture.get(timeoutMs, TimeUnit.MILLISECONDS);
+            return eventFuture.get(params.timeoutMs, TimeUnit.MILLISECONDS);
         } catch (TimeoutException e) {
             Log.w(TAG, "Timed out waiting for event");
             return false;
@@ -99,14 +98,13 @@
     }
 
     @GuardedBy("sLock")
-    public static boolean searchAndConsumeEventCacheLocked(
-            int eventType, String className, String text) {
+    public static boolean searchAndConsumeEventCacheLocked(EventQueryParams params) {
         ListIterator<AccessibilityEvent> iterator = sEventCache.listIterator();
         int foundIndex = -1;
         while (iterator.hasNext()) {
             int index = iterator.nextIndex();
             AccessibilityEvent event = iterator.next();
-            if (eventMatches(event, eventType, className, text)) {
+            if (eventMatches(event, params)) {
                 foundIndex = index;
                 break;
             }
@@ -124,17 +122,70 @@
         sEventCache.clear();
     }
 
-    static boolean eventMatches(
-            AccessibilityEvent event, int eventType, String className, String text) {
-        if (event.getEventType() != eventType) return false;
+    public static boolean tryPerformActionOnNode(String className, String text, int action) {
+        synchronized (sLock) {
+            AccessibilityTestService instance = sInstance;
+            if (instance == null) {
+                Log.e(TAG, "AccessibilityTestService instance is null");
+                return false;
+            }
+
+            AccessibilityNodeInfo root = instance.getRootInActiveWindow();
+            if (root == null) {
+                Log.e(TAG, "Root node is null");
+                return false;
+            }
+
+            AccessibilityNodeInfo targetNode = findNodeRecursive(root, className, text);
+
+            if (targetNode != null) {
+                Log.i(TAG, "Found node: " + targetNode.toString());
+                return targetNode.performAction(action);
+            }
+
+            Log.e(TAG, "Node not found");
+            return false;
+        }
+    }
+
+    private static AccessibilityNodeInfo findNodeRecursive(
+            AccessibilityNodeInfo node, String className, String text) {
+        if (node == null) return null;
+
+        CharSequence nodeClassName = node.getClassName();
+        CharSequence nodeText = node.getText();
+        Log.i(TAG, "  findNodeRecursive: " + nodeClassName + " - " + nodeText);
+
+        boolean classNameMatches =
+                TextUtils.isEmpty(className) || TextUtils.equals(nodeClassName, className);
+        boolean textMatches = TextUtils.isEmpty(text) || TextUtils.equals(nodeText, text);
+
+        if (classNameMatches && textMatches) {
+            return node;
+        }
+
+        for (int i = 0; i < node.getChildCount(); i++) {
+            AccessibilityNodeInfo child = node.getChild(i);
+            AccessibilityNodeInfo result = findNodeRecursive(child, className, text);
+            if (result != null) {
+                return result;
+            }
+        }
+        return null;
+    }
+
+    static boolean eventMatches(AccessibilityEvent event, EventQueryParams params) {
+        if (event.getEventType() != params.eventType) return false;
 
         AccessibilityNodeInfo source = event.getSource();
         CharSequence sourceClassName = source != null ? source.getClassName() : "";
         CharSequence sourceText = source != null ? source.getText() : "";
 
         boolean classNameMatches =
-                TextUtils.isEmpty(className) || TextUtils.equals(sourceClassName, className);
-        boolean textMatches = TextUtils.isEmpty(text) || TextUtils.equals(sourceText, text);
+                TextUtils.isEmpty(params.className)
+                        || TextUtils.equals(sourceClassName, params.className);
+        boolean textMatches =
+                TextUtils.isEmpty(params.text) || TextUtils.equals(sourceText, params.text);
 
         return classNameMatches && textMatches;
     }
diff --git a/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/EventQueryParams.aidl b/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/EventQueryParams.aidl
new file mode 100644
index 0000000..d306109
--- /dev/null
+++ b/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/EventQueryParams.aidl
@@ -0,0 +1,16 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.ui.accessibility.testservice;
+
+parcelable EventQueryParams {
+    /** The type of event to wait for (e.g., AccessibilityEvent.TYPE_VIEW_FOCUSED). */
+    int eventType;
+    /** The expected class name of the event source. Null or empty string matches any class name. */
+    String className;
+    /** The expected text of the event source. Null or empty string matches any text. */
+    String text;
+    /** The maximum time to wait in milliseconds. */
+    long timeoutMs;
+}
diff --git a/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/IAccessibilityTestHelperService.aidl b/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/IAccessibilityTestHelperService.aidl
index 0422495..be39e4cb 100644
--- a/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/IAccessibilityTestHelperService.aidl
+++ b/ui/accessibility/android/javatest/src/org/chromium/ui/accessibility/testservice/IAccessibilityTestHelperService.aidl
@@ -4,17 +4,24 @@
 
 package org.chromium.ui.accessibility.testservice;
 
+import org.chromium.ui.accessibility.testservice.EventQueryParams;
+
 interface IAccessibilityTestHelperService {
     /**
-     * Waits for an accessibility event of the given type on a node
-     * with the given class name. Returns true if the event is received
-     * within the timeout, false otherwise.
+     * Waits for an accessibility event matching the given query parameters.
+     * Returns true if the event is received within the timeout, false otherwise.
      *
-     * @param eventType The type of event to wait for (e.g.,
-     *     AccessibilityEvent.TYPE_VIEW_FOCUSED).
-     * @param className The expected class name of the event source.  Null or empty string matches any class name.
-     * @param text The expected text of the event source. Null or empty string matches any text.
-     * @param timeoutMs The maximum time to wait in milliseconds.
+     * @param params The event query parameters.
      */
-    boolean waitForEvent(int eventType, String className, String text, long timeoutMs);
+    boolean waitForEvent(in EventQueryParams params);
+
+    /**
+     * Finds a node matching the criteria and performs the given action on it.
+     *
+     * @param className The class name to match.
+     * @param text The text to match.
+     * @param action The action to perform (e.g., AccessibilityNodeInfo.ACTION_HOVER_ENTER).
+     * @return true if the action was performed successfully.
+     */
+    boolean performActionOnNode(String className, String text, int action);
 }
diff --git a/ui/base/clipboard/clipboard.h b/ui/base/clipboard/clipboard.h
index eb28a28b..69a51f9 100644
--- a/ui/base/clipboard/clipboard.h
+++ b/ui/base/clipboard/clipboard.h
@@ -72,6 +72,8 @@
       base::OnceCallback<void(std::map<std::string, std::string>)>;
   using ReadAvailableStandardAndCustomFormatNamesCallback =
       base::OnceCallback<void(std::vector<std::u16string>)>;
+  using GetSourceCallback =
+      base::OnceCallback<void(std::optional<DataTransferEndpoint>)>;
 
   // This enum is used to specify different privacy types of the clipboard
   // data. If a password is copied to the clipboard, based on platform support,
@@ -151,8 +153,8 @@
   virtual void OnPreShutdown() = 0;
 
   // Gets the source of the current clipboard buffer contents.
-  virtual std::optional<DataTransferEndpoint> GetSource(
-      ClipboardBuffer buffer) const = 0;
+  virtual void GetSource(ClipboardBuffer buffer,
+                         GetSourceCallback callback) const = 0;
 
   // Returns a token which uniquely identifies clipboard state.
   // ClipboardSequenceNumberTokens are used since there may be multiple
diff --git a/ui/base/clipboard/clipboard_android.cc b/ui/base/clipboard/clipboard_android.cc
index 22c65dc..bc422b9 100644
--- a/ui/base/clipboard/clipboard_android.cc
+++ b/ui/base/clipboard/clipboard_android.cc
@@ -135,7 +135,7 @@
   const std::vector<ui::FileInfo>& GetFilenames();
   void SetFilenames(std::vector<ui::FileInfo> filenames);
   void CommitToAndroidClipboard(GURL data_source);
-  std::optional<DataTransferEndpoint> GetSource();
+  void GetSource(Clipboard::GetSourceCallback callback);
   void Clear();
   std::vector<std::u16string> GetCustomTypes();
   void MarkPasswordData();
@@ -387,15 +387,16 @@
   UpdateLastModifiedTime(base::Time::Now());
 }
 
-std::optional<DataTransferEndpoint> ClipboardMap::GetSource() {
+void ClipboardMap::GetSource(Clipboard::GetSourceCallback callback) {
   base::AutoLock lock(lock_);
 
   auto iter = map_.find(ClipboardFormatType::InternalSourceUrlType());
   if (iter == map_.end()) {
-    return {};
+    std::move(callback).Run(std::nullopt);
+    return;
   }
 
-  return DataTransferEndpoint(GURL(iter->second));
+  std::move(callback).Run(DataTransferEndpoint(GURL(iter->second)));
 }
 
 void ClipboardMap::Clear() {
@@ -557,12 +558,12 @@
 
 void ClipboardAndroid::OnPreShutdown() {}
 
-std::optional<DataTransferEndpoint> ClipboardAndroid::GetSource(
-    ClipboardBuffer buffer) const {
+void ClipboardAndroid::GetSource(ClipboardBuffer buffer,
+                                 GetSourceCallback callback) const {
   DCHECK(CalledOnValidThread());
   DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
 
-  return GetClipboardMap().GetSource();
+  GetClipboardMap().GetSource(std::move(callback));
 }
 
 const ClipboardSequenceNumberToken& ClipboardAndroid::GetSequenceNumber(
diff --git a/ui/base/clipboard/clipboard_android.h b/ui/base/clipboard/clipboard_android.h
index 5d7394a2..8f2fd28 100644
--- a/ui/base/clipboard/clipboard_android.h
+++ b/ui/base/clipboard/clipboard_android.h
@@ -60,8 +60,8 @@
 
   // Clipboard overrides:
   void OnPreShutdown() override;
-  std::optional<DataTransferEndpoint> GetSource(
-      ClipboardBuffer buffer) const override;
+  void GetSource(ClipboardBuffer buffer,
+                 GetSourceCallback callback) const override;
   const ClipboardSequenceNumberToken& GetSequenceNumber(
       ClipboardBuffer buffer) const override;
   std::vector<std::u16string> GetStandardFormats(
diff --git a/ui/base/clipboard/clipboard_ios.h b/ui/base/clipboard/clipboard_ios.h
index 0203cdad..4b38c18 100644
--- a/ui/base/clipboard/clipboard_ios.h
+++ b/ui/base/clipboard/clipboard_ios.h
@@ -27,8 +27,8 @@
 
   // Clipboard overrides:
   void OnPreShutdown() override;
-  std::optional<DataTransferEndpoint> GetSource(
-      ClipboardBuffer buffer) const override;
+  void GetSource(ClipboardBuffer buffer,
+                 GetSourceCallback callback) const override;
   const ClipboardSequenceNumberToken& GetSequenceNumber(
       ClipboardBuffer buffer) const override;
   std::vector<std::u16string> GetStandardFormats(
diff --git a/ui/base/clipboard/clipboard_ios.mm b/ui/base/clipboard/clipboard_ios.mm
index 2b736d0..b510ca1 100644
--- a/ui/base/clipboard/clipboard_ios.mm
+++ b/ui/base/clipboard/clipboard_ios.mm
@@ -63,11 +63,11 @@
 void ClipboardIOS::OnPreShutdown() {}
 
 // DataTransferEndpoint is not used on this platform.
-std::optional<DataTransferEndpoint> ClipboardIOS::GetSource(
-    ClipboardBuffer buffer) const {
+void ClipboardIOS::GetSource(ClipboardBuffer buffer,
+                             GetSourceCallback callback) const {
   DCHECK(CalledOnValidThread());
   DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
-  return std::nullopt;
+  std::move(callback).Run(std::nullopt);
 }
 
 const ClipboardSequenceNumberToken& ClipboardIOS::GetSequenceNumber(
diff --git a/ui/base/clipboard/clipboard_mac.h b/ui/base/clipboard/clipboard_mac.h
index 0a96b5a..510d4d0 100644
--- a/ui/base/clipboard/clipboard_mac.h
+++ b/ui/base/clipboard/clipboard_mac.h
@@ -44,8 +44,8 @@
 
   // Clipboard overrides:
   void OnPreShutdown() override;
-  std::optional<DataTransferEndpoint> GetSource(
-      ClipboardBuffer buffer) const override;
+  void GetSource(ClipboardBuffer buffer,
+                 GetSourceCallback callback) const override;
   const ClipboardSequenceNumberToken& GetSequenceNumber(
       ClipboardBuffer buffer) const override;
   std::vector<std::u16string> GetStandardFormats(
@@ -113,9 +113,9 @@
   void ReadPngInternal(ClipboardBuffer buffer,
                        NSPasteboard* pasteboard,
                        ReadPngCallback callback) const;
-  std::optional<DataTransferEndpoint> GetSourceInternal(
-      ClipboardBuffer buffer,
-      NSPasteboard* pasteboard) const;
+  void GetSourceInternal(ClipboardBuffer buffer,
+                         NSPasteboard* pasteboard,
+                         GetSourceCallback callback) const;
   void ClearInternal(ClipboardBuffer buffer, NSPasteboard* pasteboard);
   void WritePortableAndPlatformRepresentationsInternal(
       ClipboardBuffer buffer,
diff --git a/ui/base/clipboard/clipboard_mac.mm b/ui/base/clipboard/clipboard_mac.mm
index 81736fe..03b5992 100644
--- a/ui/base/clipboard/clipboard_mac.mm
+++ b/ui/base/clipboard/clipboard_mac.mm
@@ -159,29 +159,31 @@
 
 void ClipboardMac::OnPreShutdown() {}
 
-std::optional<DataTransferEndpoint> ClipboardMac::GetSource(
-    ClipboardBuffer buffer) const {
-  return GetSourceInternal(buffer, GetPasteboard());
+void ClipboardMac::GetSource(ClipboardBuffer buffer,
+                             GetSourceCallback callback) const {
+  GetSourceInternal(buffer, GetPasteboard(), std::move(callback));
 }
 
-std::optional<DataTransferEndpoint> ClipboardMac::GetSourceInternal(
-    ClipboardBuffer buffer,
-    NSPasteboard* pasteboard) const {
+void ClipboardMac::GetSourceInternal(ClipboardBuffer buffer,
+                                     NSPasteboard* pasteboard,
+                                     GetSourceCallback callback) const {
   DCHECK(CalledOnValidThread());
   DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
 
   NSString* source_url = [pasteboard stringForType:kUTTypeChromiumSourceUrl];
 
   if (!source_url) {
-    return std::nullopt;
+    std::move(callback).Run(std::nullopt);
+    return;
   }
 
   GURL gurl(base::SysNSStringToUTF8(source_url));
   if (!gurl.is_valid()) {
-    return std::nullopt;
+    std::move(callback).Run(std::nullopt);
+    return;
   }
 
-  return DataTransferEndpoint(std::move(gurl));
+  std::move(callback).Run(DataTransferEndpoint(std::move(gurl)));
 }
 
 const ClipboardSequenceNumberToken& ClipboardMac::GetSequenceNumber(
diff --git a/ui/base/clipboard/clipboard_mac_unittest.mm b/ui/base/clipboard/clipboard_mac_unittest.mm
index d3eabec..77839313 100644
--- a/ui/base/clipboard/clipboard_mac_unittest.mm
+++ b/ui/base/clipboard/clipboard_mac_unittest.mm
@@ -110,8 +110,10 @@
   std::optional<DataTransferEndpoint> GetSource(
       const ClipboardMac* clipboard_mac,
       NSPasteboard* pasteboard) {
-    return clipboard_mac->GetSourceInternal(ClipboardBuffer::kCopyPaste,
-                                            pasteboard);
+    base::test::TestFuture<std::optional<DataTransferEndpoint>> future;
+    clipboard_mac->GetSourceInternal(ClipboardBuffer::kCopyPaste, pasteboard,
+                                     future.GetCallback());
+    return future.Get();
   }
 
   void Clear(ClipboardMac* clipboard_mac, NSPasteboard* pasteboard) {
diff --git a/ui/base/clipboard/clipboard_non_backed.cc b/ui/base/clipboard/clipboard_non_backed.cc
index 2ca67ed5..8a60c23 100644
--- a/ui/base/clipboard/clipboard_non_backed.cc
+++ b/ui/base/clipboard/clipboard_non_backed.cc
@@ -498,10 +498,10 @@
 
 void ClipboardNonBacked::OnPreShutdown() {}
 
-std::optional<DataTransferEndpoint> ClipboardNonBacked::GetSource(
-    ClipboardBuffer buffer) const {
+void ClipboardNonBacked::GetSource(ClipboardBuffer buffer,
+                                   GetSourceCallback callback) const {
   const ClipboardData* data = GetInternalClipboard(buffer).GetData();
-  return data ? data->source() : std::nullopt;
+  std::move(callback).Run(data ? data->source() : std::nullopt);
 }
 
 const ClipboardSequenceNumberToken& ClipboardNonBacked::GetSequenceNumber(
diff --git a/ui/base/clipboard/clipboard_non_backed.h b/ui/base/clipboard/clipboard_non_backed.h
index bf3c372..6a6f385 100644
--- a/ui/base/clipboard/clipboard_non_backed.h
+++ b/ui/base/clipboard/clipboard_non_backed.h
@@ -54,8 +54,8 @@
       ClipboardBuffer buffer = ClipboardBuffer::kCopyPaste);
 
   // Clipboard overrides:
-  std::optional<DataTransferEndpoint> GetSource(
-      ClipboardBuffer buffer) const override;
+  void GetSource(ClipboardBuffer buffer,
+                 GetSourceCallback callback) const override;
   const ClipboardSequenceNumberToken& GetSequenceNumber(
       ClipboardBuffer buffer) const override;
 
diff --git a/ui/base/clipboard/clipboard_ozone.cc b/ui/base/clipboard/clipboard_ozone.cc
index 61f9f38..ddd4d40 100644
--- a/ui/base/clipboard/clipboard_ozone.cc
+++ b/ui/base/clipboard/clipboard_ozone.cc
@@ -473,9 +473,9 @@
   async_clipboard_ozone_->OnPreShutdown();
 }
 
-std::optional<DataTransferEndpoint> ClipboardOzone::GetSource(
-    ClipboardBuffer buffer) const {
-  return async_clipboard_ozone_->ReadSourceAndWait(buffer);
+void ClipboardOzone::GetSource(ClipboardBuffer buffer,
+                               GetSourceCallback callback) const {
+  async_clipboard_ozone_->ReadSourceAsync(buffer, std::move(callback));
 }
 
 const ClipboardSequenceNumberToken& ClipboardOzone::GetSequenceNumber(
@@ -489,8 +489,10 @@
     const DataTransferEndpoint* data_dst) const {
   DCHECK(CalledOnValidThread());
 
-  if (!IsReadAllowed(GetSource(buffer), data_dst, base::span<uint8_t>()))
+  if (!IsReadAllowed(async_clipboard_ozone_->ReadSourceAndWait(buffer),
+                     data_dst, base::span<uint8_t>())) {
     return false;
+  }
 
   auto available_types = async_clipboard_ozone_->RequestMimeTypes(buffer);
   return std::ranges::contains(available_types, format.GetName());
diff --git a/ui/base/clipboard/clipboard_ozone.h b/ui/base/clipboard/clipboard_ozone.h
index 99fa915..385bbebe 100644
--- a/ui/base/clipboard/clipboard_ozone.h
+++ b/ui/base/clipboard/clipboard_ozone.h
@@ -34,8 +34,8 @@
 
   // Clipboard overrides:
   void OnPreShutdown() override;
-  std::optional<DataTransferEndpoint> GetSource(
-      ClipboardBuffer buffer) const override;
+  void GetSource(ClipboardBuffer buffer,
+                 GetSourceCallback callback) const override;
   const ClipboardSequenceNumberToken& GetSequenceNumber(
       ClipboardBuffer buffer) const override;
   std::vector<std::u16string> GetStandardFormats(
diff --git a/ui/base/clipboard/clipboard_util_win.cc b/ui/base/clipboard/clipboard_util_win.cc
index 43ed31b..4c62bdd 100644
--- a/ui/base/clipboard/clipboard_util_win.cc
+++ b/ui/base/clipboard/clipboard_util_win.cc
@@ -718,13 +718,19 @@
     return filenames;
   }
 
-  const int kMaxFilenameLen = 4096;
   const unsigned num_files = DragQueryFileW(hdrop, 0xffffffff, 0, 0);
   for (unsigned int i = 0; i < num_files; ++i) {
-    wchar_t filename[kMaxFilenameLen];
-    if (DragQueryFileW(hdrop, i, filename, kMaxFilenameLen)) {
-      filenames.push_back(filename);
+    const UINT required_len = DragQueryFileW(hdrop, i, nullptr, 0);
+    if (!required_len) {
+      continue;
     }
+    const UINT buffer_size = required_len + 1;
+    std::wstring filename;
+    if (!DragQueryFileW(hdrop, i, base::WriteInto(&filename, buffer_size),
+                        buffer_size)) {
+      continue;
+    }
+    filenames.push_back(std::move(filename));
   }
   return filenames;
 }
diff --git a/ui/base/clipboard/clipboard_util_win_unittest.cc b/ui/base/clipboard/clipboard_util_win_unittest.cc
index 9124746..dc350bb 100644
--- a/ui/base/clipboard/clipboard_util_win_unittest.cc
+++ b/ui/base/clipboard/clipboard_util_win_unittest.cc
@@ -8,6 +8,8 @@
 #include <wrl/client.h>
 #include <wrl/implements.h>
 
+#include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
 #include "base/win/scoped_hglobal.h"
 #include "base/win/shlwapi.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -162,5 +164,44 @@
   EXPECT_FALSE(clipboard_util::HasRealFiles(data_object.Get()));
   EXPECT_TRUE(clipboard_util::HasVirtualFilenames(data_object.Get()));
 }
+
+// Unit test for GetFilenames(HDROP) with a HDROP structure.
+TEST_F(ClipboardUtilWinTest, GetFilenamesFromHDROP) {
+  const std::wstring file1 = L"C:\\foo\\bar.txt";
+  // Long path
+  std::wstring file2 = L"D:\\long_path\\";
+  file2.append(5000, L'a');
+  file2 += L"\\file.txt";
+
+  // Double-null-terminated list
+  std::wstring files_block = file1 + L'\0' + file2 + L'\0' + L'\0';
+  const SIZE_T dropfiles_size = sizeof(DROPFILES);
+  const SIZE_T total_size =
+      dropfiles_size + files_block.size() * sizeof(wchar_t);
+  HGLOBAL hglobal = ::GlobalAlloc(GHND, total_size);
+  ASSERT_NE(hglobal, nullptr);
+  base::ScopedClosureRunner free_hglobal(
+      base::BindOnce([](HGLOBAL h) { ::GlobalFree(h); }, hglobal));
+
+  base::win::ScopedHGlobal<DROPFILES*> locked_mem(hglobal);
+  DROPFILES* dropfiles = locked_mem.data();
+  ASSERT_NE(dropfiles, nullptr);
+
+  dropfiles->pFiles = sizeof(DROPFILES);
+  dropfiles->fWide = TRUE;
+
+  // SAFETY: `total_size` explicitly includes space for `DROPFILES` plus
+  // `files_block.size()` wchar_t elements immediately following it.
+  auto data_span = UNSAFE_BUFFERS(base::span<wchar_t>(
+      reinterpret_cast<wchar_t*>(dropfiles + 1), files_block.size()));
+  data_span.copy_from(base::span<const wchar_t>(files_block));
+
+  std::vector<std::wstring> filenames =
+      clipboard_util::GetFilenames(static_cast<HDROP>(hglobal));
+
+  ASSERT_EQ(filenames.size(), 2u);
+  EXPECT_EQ(filenames[0], file1);
+  EXPECT_EQ(filenames[1], file2);
+}
 }  // namespace
-}  // namespace ui
\ No newline at end of file
+}  // namespace ui
diff --git a/ui/base/clipboard/clipboard_win.cc b/ui/base/clipboard/clipboard_win.cc
index eecec37..4ccd19c 100644
--- a/ui/base/clipboard/clipboard_win.cc
+++ b/ui/base/clipboard/clipboard_win.cc
@@ -286,19 +286,21 @@
 
 void ClipboardWin::OnPreShutdown() {}
 
-std::optional<DataTransferEndpoint> ClipboardWin::GetSource(
-    ClipboardBuffer buffer) const {
+void ClipboardWin::GetSource(ClipboardBuffer buffer,
+                             GetSourceCallback callback) const {
   DCHECK_EQ(buffer, ClipboardBuffer::kCopyPaste);
 
   ScopedClipboard clipboard;
   if (!clipboard.Acquire(GetClipboardWindow())) {
-    return std::nullopt;
+    std::move(callback).Run(std::nullopt);
+    return;
   }
 
   HANDLE data = GetClipboardDataWithLimit(
       ClipboardFormatType::InternalSourceUrlType().ToFormatEtc().cfFormat);
   if (!data) {
-    return std::nullopt;
+    std::move(callback).Run(std::nullopt);
+    return;
   }
 
   std::string source_string;
@@ -309,10 +311,11 @@
 
   GURL source_url(source_string);
   if (!source_url.is_valid()) {
-    return std::nullopt;
+    std::move(callback).Run(std::nullopt);
+    return;
   }
 
-  return DataTransferEndpoint(std::move(source_url));
+  std::move(callback).Run(DataTransferEndpoint(std::move(source_url)));
 }
 
 const ClipboardSequenceNumberToken& ClipboardWin::GetSequenceNumber(
diff --git a/ui/base/clipboard/clipboard_win.h b/ui/base/clipboard/clipboard_win.h
index f267f6a..55e051f3 100644
--- a/ui/base/clipboard/clipboard_win.h
+++ b/ui/base/clipboard/clipboard_win.h
@@ -47,8 +47,8 @@
 
   // Clipboard overrides:
   void OnPreShutdown() override;
-  std::optional<DataTransferEndpoint> GetSource(
-      ClipboardBuffer buffer) const override;
+  void GetSource(ClipboardBuffer buffer,
+                 GetSourceCallback callback) const override;
   const ClipboardSequenceNumberToken& GetSequenceNumber(
       ClipboardBuffer buffer) const override;
   std::vector<std::u16string> GetStandardFormats(
diff --git a/ui/base/clipboard/test/test_clipboard.cc b/ui/base/clipboard/test/test_clipboard.cc
index cffc2b2c..f4241b1 100644
--- a/ui/base/clipboard/test/test_clipboard.cc
+++ b/ui/base/clipboard/test/test_clipboard.cc
@@ -63,9 +63,9 @@
 
 void TestClipboard::OnPreShutdown() {}
 
-std::optional<DataTransferEndpoint> TestClipboard::GetSource(
-    ClipboardBuffer buffer) const {
-  return GetStore(buffer).GetDataSource();
+void TestClipboard::GetSource(ClipboardBuffer buffer,
+                              GetSourceCallback callback) const {
+  std::move(callback).Run(GetStore(buffer).GetDataSource());
 }
 
 const ClipboardSequenceNumberToken& TestClipboard::GetSequenceNumber(
diff --git a/ui/base/clipboard/test/test_clipboard.h b/ui/base/clipboard/test/test_clipboard.h
index 5cc3411..4c2c8dd6 100644
--- a/ui/base/clipboard/test/test_clipboard.h
+++ b/ui/base/clipboard/test/test_clipboard.h
@@ -41,8 +41,8 @@
 
   // Clipboard overrides.
   void OnPreShutdown() override;
-  std::optional<DataTransferEndpoint> GetSource(
-      ClipboardBuffer buffer) const override;
+  void GetSource(ClipboardBuffer buffer,
+                 GetSourceCallback callback) const override;
   const ClipboardSequenceNumberToken& GetSequenceNumber(
       ClipboardBuffer buffer) const override;
   std::vector<std::u16string> GetStandardFormats(
diff --git a/ui/ozone/platform/headless/headless_screen.cc b/ui/ozone/platform/headless/headless_screen.cc
index 8f4cea2..7aea2f3c 100644
--- a/ui/ozone/platform/headless/headless_screen.cc
+++ b/ui/ozone/platform/headless/headless_screen.cc
@@ -172,8 +172,7 @@
 }
 
 void HeadlessScreen::SetPrimaryDisplay(int64_t display_id) {
-  // TODO(crbug.com/397350115): Implement.
-  NOTIMPLEMENTED();
+  headless::SetPrimaryDisplay(display_list_, display_id);
 }
 
 const std::vector<Display>& HeadlessScreen::GetAllDisplays() const {
diff --git a/ui/views/bubble/bubble_dialog_delegate_view.cc b/ui/views/bubble/bubble_dialog_delegate_view.cc
index 211ddca..0cca2b3 100644
--- a/ui/views/bubble/bubble_dialog_delegate_view.cc
+++ b/ui/views/bubble/bubble_dialog_delegate_view.cc
@@ -960,7 +960,7 @@
 
 template <typename Value>
 void BubbleDialogDelegate::BubbleUmaLogger::LogMetric(
-    void (*uma_func)(std::string_view, Value),
+    void (*uma_func)(const std::string&, Value),
     std::string_view histogram_name,
     Value value) const {
   if (!base::FeatureList::IsEnabled(::features::kBubbleMetricsApi)) {
@@ -989,7 +989,7 @@
 
 // Instantiate template function to be able to use in views_unittests.
 template VIEWS_EXPORT void BubbleDialogDelegate::BubbleUmaLogger::LogMetric<
-    base::TimeDelta>(void (*uma_func)(std::string_view, base::TimeDelta),
+    base::TimeDelta>(void (*uma_func)(const std::string&, base::TimeDelta),
                      std::string_view histogram_name,
                      base::TimeDelta value) const;
 
diff --git a/ui/views/bubble/bubble_dialog_delegate_view.h b/ui/views/bubble/bubble_dialog_delegate_view.h
index 0cdb3a2..e805d99 100644
--- a/ui/views/bubble/bubble_dialog_delegate_view.h
+++ b/ui/views/bubble/bubble_dialog_delegate_view.h
@@ -569,7 +569,7 @@
     // - "Bubble.{bubble_name}.{histogram_name}" for a specific bubble
     //   subclass, if `bubble_name` is set.
     template <typename Value>
-    void LogMetric(void (*uma_func)(std::string_view, Value),
+    void LogMetric(void (*uma_func)(const std::string&, Value),
                    std::string_view histogram_name,
                    Value value) const;
 
diff --git a/ui/views/metadata/view_factory.h b/ui/views/metadata/view_factory.h
index 54e20498..0f41fc15c 100644
--- a/ui/views/metadata/view_factory.h
+++ b/ui/views/metadata/view_factory.h
@@ -27,7 +27,6 @@
 #include "ui/views/border.h"
 #include "ui/views/metadata/view_factory_internal.h"
 #include "ui/views/view.h"
-#include "ui/views/view_utils.h"
 #include "ui/views/views_export.h"
 
 class SkPath;
diff --git a/ui/views/window/native_frame_view.h b/ui/views/window/native_frame_view.h
index 8c26eaa1..e133a13 100644
--- a/ui/views/window/native_frame_view.h
+++ b/ui/views/window/native_frame_view.h
@@ -35,9 +35,9 @@
   gfx::Size GetMinimumSize() const override;
   gfx::Size GetMaximumSize() const override;
 
- private:
+ protected:
   // Our containing frame.
-  raw_ptr<Widget> widget_;
+  const raw_ptr<Widget> widget_;
 };
 
 BEGIN_VIEW_BUILDER(VIEWS_EXPORT, NativeFrameView, FrameView)
diff --git a/ui/webui/resources/cr_components/composebox/composebox.css b/ui/webui/resources/cr_components/composebox/composebox.css
index ef40639..26d9866 100644
--- a/ui/webui/resources/cr_components/composebox/composebox.css
+++ b/ui/webui/resources/cr_components/composebox/composebox.css
@@ -114,7 +114,7 @@
 }
 
 :host([searchbox-next-enabled]) #input::placeholder {
-  font-size: var(--cr-composebox-input-placeholder-font-size, 18px);
+  font-size: var(--cr-composebox-input-placeholder-font-size, 16px);
   font-weight: 400;
 }
 
diff --git a/ui/webui/resources/cr_components/composebox/composebox.ts b/ui/webui/resources/cr_components/composebox/composebox.ts
index bb2ddb5e8..a7a1d19 100644
--- a/ui/webui/resources/cr_components/composebox/composebox.ts
+++ b/ui/webui/resources/cr_components/composebox/composebox.ts
@@ -259,9 +259,11 @@
       },
       isFollowupQuery: {type: Boolean},
       shouldShowGhostFiles: {type: Boolean},
+      enableFileHint: {type: Boolean},
     };
   }
 
+  accessor enableFileHint: boolean = false;
   accessor isFollowupQuery: boolean = false;
   accessor inputPlaceholderOverride: string = '';
   accessor suggestionActivityEnabled: boolean = true;
@@ -535,7 +537,9 @@
            this.inputState_.allowedInputTypes.length > 0);
     }
 
-    if (changedPrivateProperties.has('inputPlaceholderOverride')) {
+    if (changedPrivateProperties.has('inputPlaceholderOverride') ||
+        changedPrivateProperties.has('files_') ||
+        changedPrivateProperties.has('enableFileHint')) {
       this.updateInputPlaceholder_();
     }
   }
@@ -688,6 +692,10 @@
     return carousel.getThumbnailElementByUuid(this.automaticActiveTab_.uuid);
   }
 
+  hasFiles(): boolean {
+    return this.files_.size > 0;
+  }
+
   protected async initializeState_(
       text: string = '', files: ContextualUpload[] = [],
       mode: ComposeboxToolMode = ComposeboxToolMode.kUnspecified,
@@ -1307,6 +1315,27 @@
       return;
     }
 
+    const shouldUseFileHint = this.enableFileHint && this.hasFiles() &&
+        this.activeToolMode_ === ComposeboxToolMode.kUnspecified;
+    if (shouldUseFileHint) {
+      if (this.files_.size > 1) {
+        this.inputPlaceholder_ = this.i18n('composeboxHintTextAskAboutThese');
+        return;
+      }
+      const file = this.files_.values().next().value!;
+      if (file.type === 'tab') {
+        this.inputPlaceholder_ = this.i18n('composeboxHintTextAskAboutThisTab');
+        return;
+      } else if (file.type.includes('image')) {
+        this.inputPlaceholder_ =
+            this.i18n('composeboxHintTextAskAboutThisImage');
+        return;
+      } else if (file.type === 'pdf' || file.type === 'application/pdf') {
+        this.inputPlaceholder_ = this.i18n('composeboxHintTextAskAboutThisDoc');
+        return;
+      }
+    }
+
     if (this.inputState_) {
       if (this.activeToolMode_ !== ComposeboxToolMode.kUnspecified) {
         const config = this.inputState_.toolConfigs.find(
diff --git a/ui/webui/resources/cr_components/searchbox/searchbox.css b/ui/webui/resources/cr_components/searchbox/searchbox.css
index a1b3245..9e425fd8 100644
--- a/ui/webui/resources/cr_components/searchbox/searchbox.css
+++ b/ui/webui/resources/cr_components/searchbox/searchbox.css
@@ -333,7 +333,6 @@
 }
 
 :host([ntp-realbox-next-enabled]) :is(input, textarea)::placeholder {
-  font-size: 18px;
   color: var(--cr-composebox-input-placeholder-color, var(--color-composebox-type-ahead));
   font-weight: 400;
 }
diff --git a/v8 b/v8
index 8fd9b68..78c603b 160000
--- a/v8
+++ b/v8
@@ -1 +1 @@
-Subproject commit 8fd9b68b176e8b623c40d907940be52146424b3f
+Subproject commit 78c603b9859d310dd919660859a29d5afe88b165