diff --git a/.github/prompts/create_copilot_instructions.prompt.md b/.github/prompts/create_copilot_instructions.prompt.md
index 43190d1..6a41677 100644
--- a/.github/prompts/create_copilot_instructions.prompt.md
+++ b/.github/prompts/create_copilot_instructions.prompt.md
@@ -27,7 +27,15 @@
 
 Then, introduce yourself, your goals and start by asking the user for the
 following, in the future you will be able to offer more personalized
-instructions:
+instructions. Ask the user to answer these questions, you should provide them
+in an ordered list to the user. After sharing the list, you can suggest the
+quick answer: `yes, debug_x64, no, no`, and invite the user to ask any
+questions.
+
+### If the user does have a `copilot-instructions.md` file
+If the user does have a `copilot-instructions.md` file, you will
+- offer to update it with the latest instructions if it seems out of date
+- offer to update or add `##Developer Prompt Variables`
 
 ### If the user does have a `embedder.instructions.md` file
 - ask if they want to use
@@ -38,6 +46,10 @@
   [chromium.instructions](../instructions/chromium.instructions.md)
 
 ### For both cases
+- recommend that they share recommended developer prompt variables for use by
+  other prompts such as `/autoninja` and `/gtest`.
+  - You will need to ask for `${out_dir}` this is usually something like
+    `debug_x64` or `release_x64` but it can be anything.
 - ask if they want to use
   [haystack.instructions](../instructions/haystack.instructions.md)
     - briefly explain it; if they choose to use it, mention it requires an
@@ -45,21 +57,15 @@
       after creating their instruction file
 - ask if they want user personalization
 
-### If the user does have a `copilot-instructions.md` file
-If the user does have a `copilot-instructions.md` file, you will
-- recap if its using the latest version of the instructions per above
-- if the file it out of date compared to the other prompts, offer to update it
-  with the latest instructions
-- offer to make any other changes they would like, such as personalization
-
 ## Output Format
 
 You will produce [`.github/copilot-instructions.md`](../copilot-instructions.md)
 with multiple sections, the sections must be ordered as follows if they are to
 be included:
   1. Default chromium or embedder instructions
-  2. Haystack
-  3. User personalization
+  2. Developer Prompt Variables
+  3. Haystack
+  4. User personalization
 
 **Do not** include filepath syntax in the output, such as:
 `// filepath: ...\.github\instructions\haystack.instructions.md`
@@ -70,6 +76,13 @@
 - [`chromium.instructions`](../instructions/chromium.instructions.md)
 - [`embedder.instructions`](../instructions/embedder.instructions.md)
 
+### Developer Prompt Variables
+The developer prompt variables should be a version of the following code snippet
+```markdown
+## Developer Prompt Variables
+`${out_dir}` = `out_dir`
+```
+
 ### Chromium Haystack
 
 If the user requests Chromium Haystack, you will need to help them set it up.
diff --git a/.github/resources/gtest_discovery.md b/.github/resources/gtest_discovery.md
index b903840..0713b75b 100644
--- a/.github/resources/gtest_discovery.md
+++ b/.github/resources/gtest_discovery.md
@@ -1,15 +1,13 @@
 ## GTest Discovery
-1. If the user provided a ${file} you can make the following assumptions:
-  - If the `${file}` is in the `chrome` folder and ends in `_unittest.cc`,
-    the ${test_name} `unit_tests`.
-  - If the `${file}` is in the `chrome` folder and ends in `_browsertest.cc`,
-    the ${test_name} `browser_tests`.
+1. If the user provided a `${file}` you can find a matching `${test_name}`
+   with the following command: `gn refs out/{out_dir} ${file}`. If the response
+   is `//chrome/test:browser_tests`, `browser_tests` is the `${test_name}`.
 
 2. If you were able to determine the `${test_name}` from the `${file}`, you can
-  Read the file to extract a `${test_filter}` that would match all tests in the
-  file.
-  - For example if the file has `MyTestSuite.MyTest` and `MyTestSuite.MyTest2`,
-    the `${test_filter}` can be `MyTestSuite.*`.
+   Read the file to extract a `${test_filter}` that would match all tests in the
+   file.
+   - For example if the file has `MyTestSuite.MyTest` and `MyTestSuite.MyTest2`,
+     the `${test_filter}` can be `MyTestSuite.*`.
 
 3. If you were able to determine the `${test_name}` and `${test_filter}`,
    `## GTest Discovery` has passed, otherwise it has failed.
diff --git a/AUTHORS b/AUTHORS
index 5509a661..36e16c6 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -590,6 +590,7 @@
 Imranur Rahman <ir.shimul@gmail.com>
 Ion Rosca <rosca@adobe.com>
 Irmak Kavasoglu <irmakkavasoglu@gmail.com>
+Isaac Khor <dev@isaackhor.com>
 Isaac Murchie <murchieisaac@gmail.com>
 Isaac Reilly <reillyi@amazon.com>
 Ivan Naydonov <samogot@gmail.com>
diff --git a/DEPS b/DEPS
index d6cdd67..c8564e1 100644
--- a/DEPS
+++ b/DEPS
@@ -299,11 +299,11 @@
   # 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': '3b2d2d0f73fcc9468a3bced6d6d17a56411a87f4',
+  'skia_revision': '18b85aced9b7b381ce184703bdeeae53a2fbe294',
   # 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': '7d1326123f37e10d3cf0a8ff6efc2d330e1f2b96',
+  'v8_revision': '5f0be56704de0f9fd5ee6bb6a7a780976a5bfcb7',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
@@ -331,7 +331,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling googletest
   # and whatever else without interference from each other.
-  'googletest_revision': '16d4f8eff6d7cefca6975d82a53f8fc995a6feb7',
+  'googletest_revision': '6aa03e6774f8cb70da277c56efb24b44ce29d8d7',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling lighttpd
   # and whatever else without interference from each other.
@@ -1508,7 +1508,7 @@
 
   'src/clank': {
     'url': Var('chrome_git') + '/clank/internal/apps.git' + '@' +
-    '890fb2eabfdafcda1830d1aaf97ba4bf0c52866e',
+    'c2910b6472c3849f25d534e1a00fe9691acf9f8d',
     'condition': 'checkout_android and checkout_src_internal',
   },
 
@@ -1667,7 +1667,7 @@
     'packages': [
       {
           'package': 'chromium/third_party/androidx',
-          'version': 'j7iNI3myTyI0EMBVQIEhOLRfEWiicoOLLXtRaela9s0C',
+          'version': '35sx6LkLBBzbBXpfWIfXVI8tvIsqtu2T6tWdqcT-g94C',
       },
     ],
     'condition': 'checkout_android and non_git_source',
@@ -1760,7 +1760,7 @@
       'packages': [
           {
                'package': 'chromium/third_party/android_build_tools/lint',
-               'version': 'PaYB6553MH9GJfamUZLduJESRbN13Clv2N2beHR6IQAC',
+               'version': 'Rh_qNy2kyeA9GGIdsjnPMvQa1WpHOEoUBZqV9opGGgIC',
           },
       ],
       'condition': 'checkout_android and non_git_source',
@@ -1771,7 +1771,7 @@
       'packages': [
           {
                'package': 'chromium/third_party/android_build_tools/manifest_merger',
-               'version': '0L0N3_u2ypIxEEtf0k_l36g_2CykW2BNuZBXm-v5qAUC',
+               'version': 'i1CvLtWlkB9QDx0DL_52AZNLpuQc2d6MbpRsCbzgEtEC',
           },
       ],
       'condition': 'checkout_android and non_git_source',
@@ -2548,7 +2548,7 @@
     Var('pdfium_git') + '/pdfium.git' + '@' +  Var('pdfium_revision'),
 
   'src/third_party/perfetto':
-    Var('chromium_git') + '/external/github.com/google/perfetto.git' + '@' + 'd15aa8fb5bed6a9e04f7f93ebdc3573b6f7a366e',
+    Var('chromium_git') + '/external/github.com/google/perfetto.git' + '@' + 'fa368a66f714203d1232ff62ece403bd2f5e7c10',
 
   'src/base/tracing/test/data': {
     'bucket': 'perfetto',
@@ -2867,16 +2867,16 @@
       'dep_type': 'cipd',
   },
 
-  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@12211edbca712355258047ef4f0de13f1da23ac3',
-  'src/third_party/glslang/src': '{chromium_git}/external/github.com/KhronosGroup/glslang@32f71d72a2643df675684e1795e987ac26e600df',
+  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@d8d0687affb24a8069b74b50af9cd9245a4af273',
+  'src/third_party/glslang/src': '{chromium_git}/external/github.com/KhronosGroup/glslang@93231001597dad1149a5d035af30eda50b9e6b6c',
   '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@c9aad99f9276817f18f72a4696239237c83cb775',
-  'src/third_party/spirv-tools/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Tools@736e415ebaa4290d21e42e370db5e933b9dff06d',
+  'src/third_party/spirv-tools/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Tools@01021466b5e71deaac9054f56082566c782bfd51',
   'src/third_party/vulkan-headers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Headers@75ad707a587e1469fb53a901b9b68fe9f6fbc11f',
   'src/third_party/vulkan-loader/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Loader@c913466fdc5004584890f89ff91121bdb2ffd4ba',
   'src/third_party/vulkan-tools/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Tools@60b640cb931814fcc6dabe4fc61f4738c56579f6',
   'src/third_party/vulkan-utility-libraries/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Utility-Libraries@ae56bd6e65d9faa731150e931cb35f0d895223bc',
-  'src/third_party/vulkan-validation-layers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-ValidationLayers@7760c965ea96a51e7d66433dc9f97b4cac7804f7',
+  'src/third_party/vulkan-validation-layers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-ValidationLayers@a44043332c975777a7196406f08601f6099b24f0',
 
   'src/third_party/vulkan_memory_allocator':
     Var('chromium_git') + '/external/github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git' + '@' + '56300b29fbfcc693ee6609ddad3fdd5b7a449a21',
@@ -2921,7 +2921,7 @@
     Var('chromium_git') + '/webpagereplay.git' + '@' + Var('webpagereplay_revision'),
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + '0f1742e458d2b187a6bf4470352acb6d7387274e',
+    Var('webrtc_git') + '/src.git' + '@' + '59d578881fea163b8bd63aa056ed38feefd273de',
 
   # Wuffs' canonical repository is at github.com/google/wuffs, but we use
   # Skia's mirror of Wuffs, the same as in upstream Skia's DEPS file.
@@ -4393,7 +4393,7 @@
   # Dependencies from src_internal
   'src/chromeos/ash/resources/internal': {
       'url': Var('chrome_git') + '/chrome/chromeos/ash/resources/internal.git' + '@' +
-        'f6bfe79494ac7266c15b9139f25377ce1e59b22c',
+        '79861294509d9038645cd39ca828edd93abaf37b',
       'condition': 'checkout_src_internal and checkout_chromeos',
   },
 
@@ -4439,7 +4439,7 @@
 
   'src/chrome/app/theme/google_chrome': {
       'url': Var('chrome_git') + '/chrome/theme/google_chrome.git' + '@' +
-        'd005f846501cdbfcf120e05b86efaa8de3b5b60e',
+        '61ee81844485e7d5e6f0f598124a93ae62dd07db',
       'condition': 'checkout_src_internal',
   },
 
@@ -4487,7 +4487,7 @@
 
   'src/chrome/browser/platform_experience/win': {
       'url': Var('chrome_git') + '/chrome/browser/platform_experience/win.git' + '@' +
-        '1c5aab84b6952ed67196e6a56788e9ef9a6e886e',
+        'e0503816507543e3b732a5a46109e9f71d74c3e1',
       'condition': 'checkout_src_internal',
   },
 
@@ -4641,7 +4641,7 @@
 
   'src/components/optimization_guide/internal': {
       'url': Var('chrome_git') + '/chrome/components/optimization_guide.git' + '@' +
-        '46d95254ba6bdec345073553c6f04b32cfe0a62b',
+        'aa7ee4b538e9adb19542ef9b4be4674196aaf859',
       'condition': 'checkout_src_internal',
   },
 
diff --git a/ash/ambient/test/ambient_ash_test_base.cc b/ash/ambient/test/ambient_ash_test_base.cc
index e1c5a0ca..354f113a 100644
--- a/ash/ambient/test/ambient_ash_test_base.cc
+++ b/ash/ambient/test/ambient_ash_test_base.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/ambient/test/ambient_ash_test_base.h"
 
 #include <map>
diff --git a/ash/assistant/ui/main_stage/assistant_onboarding_suggestion_view.cc b/ash/assistant/ui/main_stage/assistant_onboarding_suggestion_view.cc
index 3f1749d..082f6364 100644
--- a/ash/assistant/ui/main_stage/assistant_onboarding_suggestion_view.cc
+++ b/ash/assistant/ui/main_stage/assistant_onboarding_suggestion_view.cc
@@ -2,14 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/assistant/ui/main_stage/assistant_onboarding_suggestion_view.h"
 
+#include <array>
 #include <string_view>
+#include <utility>
 
 #include "ash/assistant/ui/assistant_ui_constants.h"
 #include "ash/assistant/ui/assistant_view_delegate.h"
@@ -51,55 +48,53 @@
 
 // Helpers ---------------------------------------------------------------------
 
-struct ColorPalette {
-  SkColor flag_off;
-  SkColor dark;
-  SkColor light;
-};
-
 SkColor GetBackgroundColor(int index) {
   // Opacity values:
   // 0x19: 10%
   // 0x4c: 30%
-  constexpr ColorPalette kBackgroundColors[] = {
-      {gfx::kGoogleBlue050, SkColorSetA(gfx::kGoogleBlue300, 0x4c),
+  constexpr std::array<std::pair<SkColor, SkColor>, 6> kBackgroundColors = {{
+      // First: Dark Mode
+      // Second: Light Mode
+      {SkColorSetA(gfx::kGoogleBlue300, 0x4c),
        SkColorSetA(gfx::kGoogleBlue600, 0x19)},
-      {gfx::kGoogleRed050, SkColorSetA(gfx::kGoogleRed300, 0x4c),
+      {SkColorSetA(gfx::kGoogleRed300, 0x4c),
        SkColorSetA(gfx::kGoogleRed600, 0x19)},
-      {gfx::kGoogleYellow050, SkColorSetA(gfx::kGoogleYellow300, 0x4c),
+      {SkColorSetA(gfx::kGoogleYellow300, 0x4c),
        SkColorSetA(gfx::kGoogleYellow600, 0x19)},
-      {gfx::kGoogleGreen050, SkColorSetA(gfx::kGoogleGreen300, 0x4c),
+      {SkColorSetA(gfx::kGoogleGreen300, 0x4c),
        SkColorSetA(gfx::kGoogleGreen600, 0x19)},
-      {SkColorSetRGB(0xF6, 0xE9, 0xF8), SkColorSetARGB(0x4c, 0xf8, 0x82, 0xff),
+      {SkColorSetARGB(0x4c, 0xf8, 0x82, 0xff),
        SkColorSetARGB(0x19, 0xc6, 0x1a, 0xd9)},
-      {gfx::kGoogleBlue050, SkColorSetA(gfx::kGoogleBlue300, 0x4c),
-       SkColorSetA(gfx::kGoogleBlue600, 0x19)}};
+      {SkColorSetA(gfx::kGoogleBlue300, 0x4c),
+       SkColorSetA(gfx::kGoogleBlue600, 0x19)},
+  }};
 
   DCHECK_GE(index, 0);
-  DCHECK_LT(index, static_cast<int>(std::size(kBackgroundColors)));
+  DCHECK_LT(index, static_cast<int>(kBackgroundColors.size()));
 
   return DarkLightModeControllerImpl::Get()->IsDarkModeEnabled()
-             ? kBackgroundColors[index].dark
-             : kBackgroundColors[index].light;
+             ? kBackgroundColors[index].first
+             : kBackgroundColors[index].second;
 }
 
 SkColor GetForegroundColor(int index) {
-  constexpr ColorPalette kForegroundColors[] = {
-      {gfx::kGoogleBlue800, gfx::kGoogleBlue200, gfx::kGoogleBlue800},
-      {gfx::kGoogleRed800, gfx::kGoogleRed200, gfx::kGoogleRed800},
-      {SkColorSetRGB(0xBF, 0x50, 0x00), gfx::kGoogleYellow200,
-       SkColorSetRGB(0xBF, 0x50, 0x00)},
-      {gfx::kGoogleGreen800, gfx::kGoogleGreen200, gfx::kGoogleGreen800},
-      {SkColorSetRGB(0x8A, 0x0E, 0x9E), SkColorSetRGB(0xf8, 0x82, 0xff),
-       SkColorSetRGB(0xaa, 0x00, 0xb8)},
-      {gfx::kGoogleBlue800, gfx::kGoogleBlue200, gfx::kGoogleBlue800}};
+  constexpr std::array<std::pair<SkColor, SkColor>, 6> kForegroundColors = {{
+      // First: Dark Mode
+      // Second: Light Mode
+      {gfx::kGoogleBlue200, gfx::kGoogleBlue800},
+      {gfx::kGoogleRed200, gfx::kGoogleRed800},
+      {gfx::kGoogleYellow200, SkColorSetRGB(0xBF, 0x50, 0x00)},
+      {gfx::kGoogleGreen200, gfx::kGoogleGreen800},
+      {SkColorSetRGB(0xf8, 0x82, 0xff), SkColorSetRGB(0xaa, 0x00, 0xb8)},
+      {gfx::kGoogleBlue200, gfx::kGoogleBlue800},
+  }};
 
   DCHECK_GE(index, 0);
-  DCHECK_LT(index, static_cast<int>(std::size(kForegroundColors)));
+  DCHECK_LT(index, static_cast<int>(kForegroundColors.size()));
 
   return DarkLightModeControllerImpl::Get()->IsDarkModeEnabled()
-             ? kForegroundColors[index].dark
-             : kForegroundColors[index].light;
+             ? kForegroundColors[index].first
+             : kForegroundColors[index].second;
 }
 
 }  // namespace
diff --git a/ash/assistant/ui/main_stage/assistant_onboarding_view_unittest.cc b/ash/assistant/ui/main_stage/assistant_onboarding_view_unittest.cc
index 04c2308..b2a7c86 100644
--- a/ash/assistant/ui/main_stage/assistant_onboarding_view_unittest.cc
+++ b/ash/assistant/ui/main_stage/assistant_onboarding_view_unittest.cc
@@ -2,13 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/assistant/ui/main_stage/assistant_onboarding_view.h"
 
+#include <array>
 #include <memory>
 #include <queue>
 #include <string>
@@ -300,7 +296,8 @@
   };
 
   auto get_color = [](int index) {
-    constexpr SkColor kForegroundColors[6][3] = {
+    constexpr std::array<std::array<SkColor, 3>, 6> kForegroundColors = {{
+
         // Colors of dark/light mode is disabled, dark mode, light mode.
         {gfx::kGoogleBlue800, gfx::kGoogleBlue200, gfx::kGoogleBlue800},
         {gfx::kGoogleRed800, gfx::kGoogleRed200, gfx::kGoogleRed800},
@@ -309,7 +306,7 @@
         {gfx::kGoogleGreen800, gfx::kGoogleGreen200, gfx::kGoogleGreen800},
         {SkColorSetRGB(0x8A, 0x0E, 0x9E), SkColorSetRGB(0xf8, 0x82, 0xff),
          SkColorSetRGB(0xaa, 0x00, 0xb8)},
-        {gfx::kGoogleBlue800, gfx::kGoogleBlue200, gfx::kGoogleBlue800}};
+        {gfx::kGoogleBlue800, gfx::kGoogleBlue200, gfx::kGoogleBlue800}}};
     const int color_index =
         DarkLightModeControllerImpl::Get()->IsDarkModeEnabled() ? 1 : 2;
     return kForegroundColors[index][color_index];
diff --git a/ash/capture_mode/camera_video_frame_renderer.cc b/ash/capture_mode/camera_video_frame_renderer.cc
index b67199bb..a7d4917 100644
--- a/ash/capture_mode/camera_video_frame_renderer.cc
+++ b/ash/capture_mode/camera_video_frame_renderer.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/capture_mode/camera_video_frame_renderer.h"
 
 #include <cmath>
diff --git a/ash/clipboard/test_support/clipboard_history_item_builder.cc b/ash/clipboard/test_support/clipboard_history_item_builder.cc
index 5c2614a..4df686c9 100644
--- a/ash/clipboard/test_support/clipboard_history_item_builder.cc
+++ b/ash/clipboard/test_support/clipboard_history_item_builder.cc
@@ -2,12 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/clipboard/test_support/clipboard_history_item_builder.h"
+
 #include <vector>
 
 #include "ash/clipboard/clipboard_history_item.h"
@@ -182,7 +178,8 @@
 
 ClipboardHistoryItemBuilder& ClipboardHistoryItemBuilder::SetPng(
     const scoped_refptr<base::RefCountedMemory>& png) {
-  std::vector<uint8_t> data(png->data(), png->data() + png->size());
+  std::vector<uint8_t> data;
+  data.assign(png->begin(), png->end());
   return SetPng(std::move(data));
 }
 
diff --git a/ash/color_enhancement/color_enhancement_controller.cc b/ash/color_enhancement/color_enhancement_controller.cc
index 0801840..6d7552f 100644
--- a/ash/color_enhancement/color_enhancement_controller.cc
+++ b/ash/color_enhancement/color_enhancement_controller.cc
@@ -2,13 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/color_enhancement/color_enhancement_controller.h"
 
+#include <array>
 #include <memory>
 
 #include "ash/shell.h"
@@ -32,37 +28,38 @@
 //
 // The first index is ColorVisionCorrectionType enum, so this must be kept in
 // that order.
-const float kSimulationParams[3][9][3] = {
+constexpr std::array<std::array<float, 3>, 27> kSimulationParams = {{
+
     // ColorVisionCorrectionType::kProtanomaly:
-    {{0.4720, -1.2946, 0.9857},
-     {-0.6128, 1.6326, 0.0187},
-     {0.1407, -0.3380, -0.0044},
-     {-0.1420, 0.2488, 0.0044},
-     {0.1872, -0.3908, 0.9942},
-     {-0.0451, 0.1420, 0.0013},
-     {0.0222, -0.0253, -0.0004},
-     {-0.0290, -0.0201, 0.0006},
-     {0.0068, 0.0454, 0.9990}},
+    {{0.4720, -1.2946, 0.9857}},
+    {{-0.6128, 1.6326, 0.0187}},
+    {{0.1407, -0.3380, -0.0044}},
+    {{-0.1420, 0.2488, 0.0044}},
+    {{0.1872, -0.3908, 0.9942}},
+    {{-0.0451, 0.1420, 0.0013}},
+    {{0.0222, -0.0253, -0.0004}},
+    {{-0.0290, -0.0201, 0.0006}},
+    {{0.0068, 0.0454, 0.9990}},
     // ColorVisionCorrectionType::kDeuteranomaly:
-    {{0.5442, -1.1454, 0.9818},
-     {-0.7091, 1.5287, 0.0238},
-     {0.1650, -0.3833, -0.0055},
-     {-0.1664, 0.4368, 0.0056},
-     {0.2178, -0.5327, 0.9927},
-     {-0.0514, 0.0958, 0.0017},
-     {0.0180, -0.0288, -0.0006},
-     {-0.0232, -0.0649, 0.0007},
-     {0.0052, 0.0360, 0.9998}},
+    {{0.5442, -1.1454, 0.9818}},
+    {{-0.7091, 1.5287, 0.0238}},
+    {{0.1650, -0.3833, -0.0055}},
+    {{-0.1664, 0.4368, 0.0056}},
+    {{0.2178, -0.5327, 0.9927}},
+    {{-0.0514, 0.0958, 0.0017}},
+    {{0.0180, -0.0288, -0.0006}},
+    {{-0.0232, -0.0649, 0.0007}},
+    {{0.0052, 0.0360, 0.9998}},
     // ColorVisionCorrectionType::kTritanomaly:
-    {{0.4275, -0.0181, 0.9307},
-     {-0.2454, 0.0013, 0.0827},
-     {-0.1821, 0.0168, -0.0134},
-     {-0.1280, 0.0047, 0.0202},
-     {0.0233, -0.0398, 0.9728},
-     {0.1048, 0.0352, 0.0070},
-     {-0.0156, 0.0061, 0.0071},
-     {0.3841, 0.2947, 0.0151},
-     {-0.3685, -0.3008, 0.9778}}};
+    {{0.4275, -0.0181, 0.9307}},
+    {{-0.2454, 0.0013, 0.0827}},
+    {{-0.1821, 0.0168, -0.0134}},
+    {{-0.1280, 0.0047, 0.0202}},
+    {{0.0233, -0.0398, 0.9728}},
+    {{0.1048, 0.0352, 0.0070}},
+    {{-0.0156, 0.0061, 0.0071}},
+    {{0.3841, 0.2947, 0.0151}},
+    {{-0.3685, -0.3008, 0.9778}}}};
 
 // Returns a 3x3 matrix for simulating the given type of CVD with the given
 // severity.
@@ -74,11 +71,13 @@
   gfx::Matrix3F result = gfx::Matrix3F::Zeros();
   for (int i = 0; i < 3; i++) {
     for (int j = 0; j < 3; j++) {
+      int type_start_row = static_cast<int>(type) * 9;
       int param_row = i * 3 + j;
-      result.set(i, j,
-                 kSimulationParams[type][param_row][0] * severity_squared +
-                     kSimulationParams[type][param_row][1] * severity +
-                     kSimulationParams[type][param_row][2]);
+      result.set(
+          i, j,
+          kSimulationParams[type_start_row + param_row][0] * severity_squared +
+              kSimulationParams[type_start_row + param_row][1] * severity +
+              kSimulationParams[type_start_row + param_row][2]);
     }
   }
   return result;
diff --git a/ash/constants/ash_switches.cc b/ash/constants/ash_switches.cc
index f97ecb1..66165f3 100644
--- a/ash/constants/ash_switches.cc
+++ b/ash/constants/ash_switches.cc
@@ -868,6 +868,9 @@
 // If set, the overview button will be visible.
 const char kOverviewButtonForTests[] = "overview-button-for-tests";
 
+// If set, the overrrides the overscan settings on all displays.
+const char kOverscanInsetsOverride[] = "overscan-insets-override";
+
 // Controls how often the HiddenNetworkHandler class checks for wrongly hidden
 // networks. The interval should be provided in seconds, should follow the
 // format "--hidden-network-migration-interval=#", and should be >= 1.
diff --git a/ash/constants/ash_switches.h b/ash/constants/ash_switches.h
index d52e084..1e0d570 100644
--- a/ash/constants/ash_switches.h
+++ b/ash/constants/ash_switches.h
@@ -288,6 +288,7 @@
 COMPONENT_EXPORT(ASH_CONSTANTS)
 extern const char kOobeTriggerSyncTimeoutForTests[];
 COMPONENT_EXPORT(ASH_CONSTANTS) extern const char kOverviewButtonForTests[];
+COMPONENT_EXPORT(ASH_CONSTANTS) extern const char kOverscanInsetsOverride[];
 COMPONENT_EXPORT(ASH_CONSTANTS)
 extern const char kHiddenNetworkMigrationInterval[];
 COMPONENT_EXPORT(ASH_CONSTANTS)
diff --git a/ash/display/cros_display_config.cc b/ash/display/cros_display_config.cc
index f56b25e8b..dd0356a 100644
--- a/ash/display/cros_display_config.cc
+++ b/ash/display/cros_display_config.cc
@@ -889,8 +889,7 @@
     case crosapi::mojom::DisplayConfigOperation::kStart: {
       DVLOG(1) << "OverscanCalibrationStart: " << display_id;
       gfx::Insets insets =
-          Shell::Get()->window_tree_host_manager()->GetOverscanInsets(
-              display.id());
+          Shell::Get()->display_manager()->GetOverscanInsets(display.id());
       if (calibrator) {
         DVLOG(1) << "Replacing existing calibrator for id: " << display_id;
       }
diff --git a/ash/display/display_color_manager_unittest.cc b/ash/display/display_color_manager_unittest.cc
index 2d7d35b..6c4a6eb 100644
--- a/ash/display/display_color_manager_unittest.cc
+++ b/ash/display/display_color_manager_unittest.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/display/display_color_manager.h"
 
 #include <memory>
diff --git a/ash/display/display_prefs.cc b/ash/display/display_prefs.cc
index cd15308..d7aa841 100644
--- a/ash/display/display_prefs.cc
+++ b/ash/display/display_prefs.cc
@@ -28,7 +28,6 @@
 #include "components/prefs/scoped_user_pref_update.h"
 #include "third_party/cros_system_api/dbus/service_constants.h"
 #include "ui/display/display_features.h"
-#include "ui/display/display_switches.h"
 #include "ui/display/manager/display_layout_store.h"
 #include "ui/display/manager/display_manager.h"
 #include "ui/display/manager/json_converter.h"
@@ -218,7 +217,7 @@
 
     if (base::Contains(it.first, ",")) {
       std::vector<std::string> ids_str = base::SplitString(
-          it.first, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+          it.first, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
       std::vector<int64_t> ids;
       for (std::string id_str : ids_str) {
         int64_t id;
@@ -275,7 +274,25 @@
     }
 
     gfx::Insets insets;
-    if (ValueToInsets(*dict_value, &insets)) {
+    if (base::CommandLine::ForCurrentProcess()->HasSwitch(
+            switches::kOverscanInsetsOverride)) {
+      std::string value =
+          base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
+              switches::kOverscanInsetsOverride);
+      auto values = base::SplitString(value, ",",
+                                      base::WhitespaceHandling::TRIM_WHITESPACE,
+                                      base::SplitResult::SPLIT_WANT_ALL);
+      int top, left, bottom, right;
+      if (values.size() == 4 && base::StringToInt(values[0], &top) &&
+          base::StringToInt(values[1], &left) &&
+          base::StringToInt(values[2], &bottom) &&
+          base::StringToInt(values[3], &right)) {
+        insets = gfx::Insets::TLBR(top, left, bottom, right);
+        insets_to_set = &insets;
+      } else {
+        LOG(ERROR) << "Failed to parse overscan insets:" << value;
+      }
+    } else if (ValueToInsets(*dict_value, &insets) && !insets.IsEmpty()) {
       insets_to_set = &insets;
     }
 
diff --git a/ash/display/display_prefs_unittest.cc b/ash/display/display_prefs_unittest.cc
index 12156a0..13de930 100644
--- a/ash/display/display_prefs_unittest.cc
+++ b/ash/display/display_prefs_unittest.cc
@@ -402,8 +402,7 @@
   window_tree_host_manager->SetPrimaryDisplayId(dummy_id);
   EXPECT_NE(dummy_id, display::Screen::GetScreen()->GetPrimaryDisplay().id());
 
-  window_tree_host_manager->SetOverscanInsets(
-      id1, gfx::Insets::TLBR(10, 11, 12, 13));
+  display_manager()->SetOverscanInsets(id1, gfx::Insets::TLBR(10, 11, 12, 13));
   display_manager()->SetDisplayRotation(id1, display::Display::ROTATE_90,
                                         display::Display::RotationSource::USER);
 
@@ -826,8 +825,8 @@
   display_manager()->UpdateZoomFactor(id1, 1.f / scale);
   window_tree_host_manager->SetPrimaryDisplayId(id2);
   int64_t new_primary = display::Screen::GetScreen()->GetPrimaryDisplay().id();
-  window_tree_host_manager->SetOverscanInsets(
-      new_primary, gfx::Insets::TLBR(10, 11, 12, 13));
+  display_manager()->SetOverscanInsets(new_primary,
+                                       gfx::Insets::TLBR(10, 11, 12, 13));
   display_manager()->SetDisplayRotation(new_primary,
                                         display::Display::ROTATE_90,
                                         display::Display::RotationSource::USER);
@@ -899,8 +898,8 @@
   window_tree_host_manager->SetPrimaryDisplayId(id2);
   const int64_t new_primary =
       display::Screen::GetScreen()->GetPrimaryDisplay().id();
-  window_tree_host_manager->SetOverscanInsets(
-      new_primary, gfx::Insets::TLBR(10, 11, 12, 13));
+  display_manager()->SetOverscanInsets(new_primary,
+                                       gfx::Insets::TLBR(10, 11, 12, 13));
   display_manager()->SetDisplayRotation(new_primary,
                                         display::Display::ROTATE_90,
                                         display::Display::RotationSource::USER);
diff --git a/ash/display/overscan_calibrator.cc b/ash/display/overscan_calibrator.cc
index a079c540..9c470f7 100644
--- a/ash/display/overscan_calibrator.cc
+++ b/ash/display/overscan_calibrator.cc
@@ -122,8 +122,8 @@
       committed_(false) {
   // Undo the overscan calibration temporarily so that the user can see
   // dark boundary and current overscan region.
-  Shell::Get()->window_tree_host_manager()->SetOverscanInsets(display_.id(),
-                                                              gfx::Insets());
+  Shell::Get()->display_manager()->SetOverscanInsets(display_.id(),
+                                                     gfx::Insets());
   UpdateUILayer();
 }
 
@@ -131,13 +131,13 @@
   // Overscan calibration has finished without commit, so the display has to
   // be the original offset.
   if (!committed_) {
-    Shell::Get()->window_tree_host_manager()->SetOverscanInsets(
-        display_.id(), initial_insets_);
+    Shell::Get()->display_manager()->SetOverscanInsets(display_.id(),
+                                                       initial_insets_);
   }
 }
 
 void OverscanCalibrator::Commit() {
-  Shell::Get()->window_tree_host_manager()->SetOverscanInsets(
+  Shell::Get()->display_manager()->SetOverscanInsets(
       display_.id(), ConvertToHost(display_, insets_));
   committed_ = true;
 }
diff --git a/ash/display/window_tree_host_manager.cc b/ash/display/window_tree_host_manager.cc
index 051560f..59f27d1 100644
--- a/ash/display/window_tree_host_manager.cc
+++ b/ash/display/window_tree_host_manager.cc
@@ -451,16 +451,6 @@
   return windows;
 }
 
-gfx::Insets WindowTreeHostManager::GetOverscanInsets(int64_t display_id) const {
-  return GetDisplayManager()->GetOverscanInsets(display_id);
-}
-
-void WindowTreeHostManager::SetOverscanInsets(
-    int64_t display_id,
-    const gfx::Insets& insets_in_dip) {
-  GetDisplayManager()->SetOverscanInsets(display_id, insets_in_dip);
-}
-
 std::vector<RootWindowController*>
 WindowTreeHostManager::GetAllRootWindowControllers() {
   std::vector<RootWindowController*> controllers;
diff --git a/ash/display/window_tree_host_manager.h b/ash/display/window_tree_host_manager.h
index ff97532..1edab84 100644
--- a/ash/display/window_tree_host_manager.h
+++ b/ash/display/window_tree_host_manager.h
@@ -103,11 +103,6 @@
   // mode, this return a RootWindowController for the primary root window only.
   std::vector<RootWindowController*> GetAllRootWindowControllers();
 
-  // Gets/Sets/Clears the overscan insets for the specified |display_id|. See
-  // display_manager.h for the details.
-  gfx::Insets GetOverscanInsets(int64_t display_id) const;
-  void SetOverscanInsets(int64_t display_id, const gfx::Insets& insets_in_dip);
-
   // Checks if the mouse pointer is on one of displays, and moves to
   // the center of the nearest display if it's outside of all displays.
   void UpdateMouseLocationAfterDisplayChange();
diff --git a/ash/display/window_tree_host_manager_unittest.cc b/ash/display/window_tree_host_manager_unittest.cc
index 4b45a4b..242553c5 100644
--- a/ash/display/window_tree_host_manager_unittest.cc
+++ b/ash/display/window_tree_host_manager_unittest.cc
@@ -1512,8 +1512,6 @@
 }
 
 TEST_F(WindowTreeHostManagerTest, OverscanInsets) {
-  WindowTreeHostManager* window_tree_host_manager =
-      Shell::Get()->window_tree_host_manager();
   TestEventHandler event_handler;
   Shell::Get()->AddPreTargetHandler(&event_handler);
 
@@ -1521,8 +1519,8 @@
   display::Display display1 = display::Screen::GetScreen()->GetPrimaryDisplay();
   aura::Window::Windows root_windows = Shell::GetAllRootWindows();
 
-  window_tree_host_manager->SetOverscanInsets(
-      display1.id(), gfx::Insets::TLBR(10, 15, 20, 25));
+  display_manager()->SetOverscanInsets(display1.id(),
+                                       gfx::Insets::TLBR(10, 15, 20, 25));
   display::test::DisplayManagerTestApi display_manager_test(display_manager());
   EXPECT_EQ(gfx::Rect(0, 0, 80, 170), root_windows[0]->bounds());
   EXPECT_EQ(gfx::Size(150, 200), root_windows[1]->bounds().size());
@@ -1533,7 +1531,7 @@
   generator.MoveMouseToInHost(20, 25);
   EXPECT_EQ(gfx::Point(5, 15), event_handler.GetLocationAndReset());
 
-  window_tree_host_manager->SetOverscanInsets(display1.id(), gfx::Insets());
+  display_manager()->SetOverscanInsets(display1.id(), gfx::Insets());
   EXPECT_EQ(gfx::Rect(0, 0, 120, 200), root_windows[0]->bounds());
   EXPECT_EQ(gfx::Rect(120, 0, 150, 200),
             display_manager_test.GetSecondaryDisplay().bounds());
diff --git a/ash/events/keyboard_driven_event_rewriter_unittest.cc b/ash/events/keyboard_driven_event_rewriter_unittest.cc
index 1300699..30bad5d 100644
--- a/ash/events/keyboard_driven_event_rewriter_unittest.cc
+++ b/ash/events/keyboard_driven_event_rewriter_unittest.cc
@@ -2,15 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/events/keyboard_driven_event_rewriter.h"
 
 #include <stddef.h>
 
+#include <array>
 #include <string>
 
 #include "base/compiler_specific.h"
@@ -87,39 +83,41 @@
 };
 
 TEST_F(KeyboardDrivenEventRewriterTest, PassThrough) {
-  struct {
+  struct TestData {
     ui::KeyboardCode ui_keycode;
     int ui_flags;
-  } kTests[] = {
-    { ui::VKEY_A, ui::EF_NONE },
-    { ui::VKEY_A, ui::EF_CONTROL_DOWN },
-    { ui::VKEY_A, ui::EF_ALT_DOWN },
-    { ui::VKEY_A, ui::EF_SHIFT_DOWN },
-    { ui::VKEY_A, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN },
-    { ui::VKEY_A, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN | ui::EF_SHIFT_DOWN },
-
-    { ui::VKEY_LEFT, ui::EF_NONE },
-    { ui::VKEY_LEFT, ui::EF_CONTROL_DOWN },
-    { ui::VKEY_LEFT, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN },
-
-    { ui::VKEY_RIGHT, ui::EF_NONE },
-    { ui::VKEY_RIGHT, ui::EF_CONTROL_DOWN },
-    { ui::VKEY_RIGHT, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN },
-
-    { ui::VKEY_UP, ui::EF_NONE },
-    { ui::VKEY_UP, ui::EF_CONTROL_DOWN },
-    { ui::VKEY_UP, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN },
-
-    { ui::VKEY_DOWN, ui::EF_NONE },
-    { ui::VKEY_DOWN, ui::EF_CONTROL_DOWN },
-    { ui::VKEY_DOWN, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN },
-
-    { ui::VKEY_RETURN, ui::EF_NONE },
-    { ui::VKEY_RETURN, ui::EF_CONTROL_DOWN },
-    { ui::VKEY_RETURN, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN },
   };
 
-  for (size_t i = 0; i < std::size(kTests); ++i) {
+  constexpr std::array<TestData, 21> kTests = {{
+      {ui::VKEY_A, ui::EF_NONE},
+      {ui::VKEY_A, ui::EF_CONTROL_DOWN},
+      {ui::VKEY_A, ui::EF_ALT_DOWN},
+      {ui::VKEY_A, ui::EF_SHIFT_DOWN},
+      {ui::VKEY_A, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN},
+      {ui::VKEY_A, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN | ui::EF_SHIFT_DOWN},
+
+      {ui::VKEY_LEFT, ui::EF_NONE},
+      {ui::VKEY_LEFT, ui::EF_CONTROL_DOWN},
+      {ui::VKEY_LEFT, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN},
+
+      {ui::VKEY_RIGHT, ui::EF_NONE},
+      {ui::VKEY_RIGHT, ui::EF_CONTROL_DOWN},
+      {ui::VKEY_RIGHT, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN},
+
+      {ui::VKEY_UP, ui::EF_NONE},
+      {ui::VKEY_UP, ui::EF_CONTROL_DOWN},
+      {ui::VKEY_UP, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN},
+
+      {ui::VKEY_DOWN, ui::EF_NONE},
+      {ui::VKEY_DOWN, ui::EF_CONTROL_DOWN},
+      {ui::VKEY_DOWN, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN},
+
+      {ui::VKEY_RETURN, ui::EF_NONE},
+      {ui::VKEY_RETURN, ui::EF_CONTROL_DOWN},
+      {ui::VKEY_RETURN, ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN},
+  }};
+
+  for (size_t i = 0; i < kTests.size(); ++i) {
     EXPECT_EQ(
         base::StringPrintf("PassThrough ui_flags=%d", kTests[i].ui_flags),
         GetRewrittenEventAsString(kTests[i].ui_keycode, kTests[i].ui_flags,
@@ -131,19 +129,21 @@
 TEST_F(KeyboardDrivenEventRewriterTest, Rewrite) {
   const int kModifierMask = ui::EF_SHIFT_DOWN;
 
-  struct {
+  struct TestCase {
     ui::KeyboardCode ui_keycode;
     int ui_flags;
-  } kTests[] = {
-    { ui::VKEY_LEFT, kModifierMask },
-    { ui::VKEY_RIGHT, kModifierMask },
-    { ui::VKEY_UP, kModifierMask },
-    { ui::VKEY_DOWN, kModifierMask },
-    { ui::VKEY_RETURN, kModifierMask },
-    { ui::VKEY_F6, kModifierMask },
   };
 
-  for (size_t i = 0; i < std::size(kTests); ++i) {
+  constexpr std::array<TestCase, 6> kTests{{
+      {ui::VKEY_LEFT, kModifierMask},
+      {ui::VKEY_RIGHT, kModifierMask},
+      {ui::VKEY_UP, kModifierMask},
+      {ui::VKEY_DOWN, kModifierMask},
+      {ui::VKEY_RETURN, kModifierMask},
+      {ui::VKEY_F6, kModifierMask},
+  }};
+
+  for (size_t i = 0; i < kTests.size(); ++i) {
     EXPECT_EQ(
         "Rewritten ui_flags=0",
         GetRewrittenEventAsString(kTests[i].ui_keycode, kTests[i].ui_flags,
diff --git a/ash/examples/test_app_window.cc b/ash/examples/test_app_window.cc
index 8c1e2dab..236ef8f 100644
--- a/ash/examples/test_app_window.cc
+++ b/ash/examples/test_app_window.cc
@@ -12,6 +12,7 @@
 #include "base/strings/string_number_conversions.h"
 #include "chromeos/ui/base/app_types.h"
 #include "chromeos/ui/base/window_properties.h"
+#include "ui/aura/client/aura_constants.h"
 #include "ui/base/models/combobox_model.h"
 #include "ui/color/color_variant.h"
 #include "ui/gfx/geometry/size.h"
@@ -121,6 +122,17 @@
           views::Checkbox* cb = static_cast<views::Checkbox*>(event.target());
           cb->GetWidget()->widget_delegate()->SetCanMaximize(cb->GetChecked());
         }));
+    add_checkbox(this, u"Always On Top", u"always on top",
+                 /*initial_state=*/false,
+                 base::BindRepeating([](const ui::Event& event) {
+                   views::Checkbox* cb =
+                       static_cast<views::Checkbox*>(event.target());
+                   auto* window = cb->GetWidget()->GetNativeWindow();
+                   window->SetProperty(aura::client::kZOrderingKey,
+                                       cb->GetChecked()
+                                           ? ui::ZOrderLevel::kFloatingWindow
+                                           : ui::ZOrderLevel::kNormal);
+                 }));
 
     AddChildView(std::make_unique<views::LabelButton>(
         base::BindRepeating(&TestView::UpdateMinimumSize,
diff --git a/base/process/process.h b/base/process/process.h
index eacea062..aa41efa 100644
--- a/base/process/process.h
+++ b/base/process/process.h
@@ -304,7 +304,9 @@
 #endif  // BUILDFLAG(IS_MAC)
 
 #if BUILDFLAG(IS_IOS) && BUILDFLAG(USE_BLINK)
-  using TerminateCallback = bool (*)(ProcessHandle handle);
+  using TerminateCallback = bool (*)(ProcessHandle handle,
+                                     int exit_code,
+                                     bool wait);
   using WaitForExitCallback = bool (*)(ProcessHandle handle,
                                        int* exit_code,
                                        base::TimeDelta timeout);
diff --git a/base/process/process_ios.cc b/base/process/process_ios.cc
index edf2e492..f3d9c3a 100644
--- a/base/process/process_ios.cc
+++ b/base/process/process_ios.cc
@@ -49,7 +49,7 @@
   }
 #endif
   CHECK(g_terminate_callback);
-  return (*g_terminate_callback)(process_);
+  return (*g_terminate_callback)(process_, exit_code, wait);
 }
 
 bool Process::WaitForExitWithTimeout(TimeDelta timeout, int* exit_code) const {
diff --git a/build/fuchsia/linux_internal.sdk.sha1 b/build/fuchsia/linux_internal.sdk.sha1
index 1fe719b..c3ea0c8 100644
--- a/build/fuchsia/linux_internal.sdk.sha1
+++ b/build/fuchsia/linux_internal.sdk.sha1
@@ -1 +1 @@
-28.20250515.102.1
+28.20250522.103.1
diff --git a/cc/input/input_handler.cc b/cc/input/input_handler.cc
index 79c340d..f2aa254 100644
--- a/cc/input/input_handler.cc
+++ b/cc/input/input_handler.cc
@@ -1019,7 +1019,7 @@
   // CC side always uses fractional scroll deltas.
   bool use_fractional_offsets = true;
   std::unique_ptr<SnapSelectionStrategy> strategy =
-      SnapSelectionStrategy::CreateForEndAndDirection(
+      SnapSelectionStrategy::CreateForDisplacement(
           current_offset, snap_displacement, use_fractional_offsets);
 
   double snapport_height_adjustment =
diff --git a/cc/input/scroll_snap_data.cc b/cc/input/scroll_snap_data.cc
index ef9e165..dc38a90 100644
--- a/cc/input/scroll_snap_data.cc
+++ b/cc/input/scroll_snap_data.cc
@@ -530,7 +530,11 @@
   float base_position =
       horiz ? strategy.base_position().x() : strategy.base_position().y();
 
+  // True if we have found a "preferred" candidate.
+  bool preferred_candidate = false;
   float smallest_distance = horiz ? proximity_range_.x() : proximity_range_.y();
+  float proximity_distance =
+      horiz ? proximity_range_.x() : proximity_range_.y();
 
   auto evaluate = [&](const SnapSearchResult& candidate,
                       const SnapAreaData& area) {
@@ -541,20 +545,38 @@
       return;
     }
     float distance = std::abs(candidate.snap_offset() - base_position);
-    if (distance > smallest_distance) {
+    if (distance > proximity_distance) {
+      return;
+    }
+
+    bool is_preferred_candidate =
+        strategy.IsPreferredSnapPosition(axis, candidate.snap_offset());
+    // If we have a preferred candidate, skip those which are not preferred.
+    if (preferred_candidate && !is_preferred_candidate) {
+      return;
+    }
+    // If this snap area is further away from the best candidate, and
+    // we either already have a preferred candidate or this candidate is not
+    // preferred, then skip it.
+    if (distance > smallest_distance &&
+        (preferred_candidate || !is_preferred_candidate)) {
       return;
     }
     // Aligned snap areas that have focus should be given preference when
     // selecting snap targets.
-    if (distance < smallest_distance || candidate.has_focus_within()) {
+    if (distance < smallest_distance ||
+        (is_preferred_candidate &&
+         (!preferred_candidate || candidate.has_focus_within()))) {
       smallest_distance = distance;
       closest = candidate;
+      preferred_candidate = is_preferred_candidate;
     } else if (closest && !closest->has_focus_within()) {
       if (closest->element_id() == targeted_area_id_) {
         return;
       }
       if (candidate.element_id() == targeted_area_id_) {
         closest = candidate;
+        preferred_candidate = is_preferred_candidate;
         return;
       }
       const auto candidate_rect = candidate.rect();
@@ -568,11 +590,13 @@
           closest_rect != candidate_rect) {
         smallest_distance = distance;
         closest = candidate;
+        preferred_candidate = is_preferred_candidate;
       } else if ((scroll_snap_type_.axis == SnapAxis::kBoth) &&
                  (area.scroll_snap_align.alignment_block !=
                   SnapAlignment::kNone) &&
                  (area.scroll_snap_align.alignment_inline !=
-                  SnapAlignment::kNone)) {
+                  SnapAlignment::kNone) &&
+                 is_preferred_candidate == preferred_candidate) {
         // This candidate is equally aligned with the current closest. Since it
         // can be snapped to in both axes, designate it a potential alternative
         // if we don't already have a potential alternative or it is a better
diff --git a/cc/input/scroll_snap_data_unittest.cc b/cc/input/scroll_snap_data_unittest.cc
index 8821ef2..f37c833f3 100644
--- a/cc/input/scroll_snap_data_unittest.cc
+++ b/cc/input/scroll_snap_data_unittest.cc
@@ -25,7 +25,7 @@
       float expected_covered_end = std::numeric_limits<float>::max()) {
     float invalid = std::numeric_limits<float>::max();
     std::unique_ptr<SnapSelectionStrategy> strategy =
-        SnapSelectionStrategy::CreateForEndAndDirection(
+        SnapSelectionStrategy::CreateForDisplacement(
             gfx::PointF(cur_pos, 0), gfx::Vector2dF(delta, 0),
             false /* use_fractional_deltas */);
 
@@ -50,7 +50,7 @@
       float expected_covered_end = std::numeric_limits<float>::max()) {
     float invalid = std::numeric_limits<float>::max();
     std::unique_ptr<SnapSelectionStrategy> strategy =
-        SnapSelectionStrategy::CreateForEndAndDirection(
+        SnapSelectionStrategy::CreateForDisplacement(
             gfx::PointF(0, cur_pos), gfx::Vector2dF(0, delta),
             false /* use_fractional_deltas */);
 
@@ -334,7 +334,7 @@
             result.target_element_ids);
 
   std::unique_ptr<SnapSelectionStrategy> end_direction_strategy =
-      SnapSelectionStrategy::CreateForEndAndDirection(
+      SnapSelectionStrategy::CreateForDisplacement(
           gfx::PointF(600, 0), gfx::Vector2dF(15, 15),
           false /* use_fractional_deltas */);
   result = container.FindSnapPosition(*end_direction_strategy);
@@ -381,7 +381,7 @@
             result.target_element_ids);
 
   std::unique_ptr<SnapSelectionStrategy> end_direction_strategy =
-      SnapSelectionStrategy::CreateForEndAndDirection(
+      SnapSelectionStrategy::CreateForDisplacement(
           gfx::PointF(650, 10), gfx::Vector2d(15, 15),
           false /* use_fractional_deltas */);
   result = container.FindSnapPosition(*end_direction_strategy);
@@ -423,7 +423,7 @@
   container.AddSnapAreaData(closer_to_target);
 
   std::unique_ptr<SnapSelectionStrategy> end_direction_strategy =
-      SnapSelectionStrategy::CreateForEndAndDirection(
+      SnapSelectionStrategy::CreateForDisplacement(
           gfx::PointF(0, 0), gfx::Vector2d(600, 0),
           false /* use_fractional_deltas */);
 
@@ -453,7 +453,7 @@
   container.AddSnapAreaData(covering_area);
 
   std::unique_ptr<SnapSelectionStrategy> strategy =
-      SnapSelectionStrategy::CreateForEndAndDirection(
+      SnapSelectionStrategy::CreateForDisplacement(
           gfx::PointF(0, 0), gfx::Vector2d(300, 0),
           false /* use_fractional_deltas */);
 
@@ -479,7 +479,7 @@
   container.AddSnapAreaData(stop_area);
 
   std::unique_ptr<SnapSelectionStrategy> strategy =
-      SnapSelectionStrategy::CreateForEndAndDirection(
+      SnapSelectionStrategy::CreateForDisplacement(
           gfx::PointF(150, 0), gfx::Vector2d(200, 0),
           false /* use_fractional_deltas */);
 
@@ -773,7 +773,7 @@
   container.AddSnapAreaData(area);
 
   std::unique_ptr<SnapSelectionStrategy> end_direction_strategy =
-      SnapSelectionStrategy::CreateForEndAndDirection(
+      SnapSelectionStrategy::CreateForDisplacement(
           gfx::PointF(0, 100), gfx::Vector2dF(0, 300),
           false /* use_fractional_deltas */);
 
@@ -785,7 +785,7 @@
   EXPECT_EQ(50, result.covered_range_y->start());
   EXPECT_EQ(850, result.covered_range_y->end());
 
-  end_direction_strategy = SnapSelectionStrategy::CreateForEndAndDirection(
+  end_direction_strategy = SnapSelectionStrategy::CreateForDisplacement(
       gfx::PointF(0, 100), gfx::Vector2dF(0, -100),
       false /* use_fractional_deltas */);
   result = container.FindSnapPosition(*end_direction_strategy);
diff --git a/cc/input/scroll_utils.cc b/cc/input/scroll_utils.cc
index 8dd1a6c..3526c0c6 100644
--- a/cc/input/scroll_utils.cc
+++ b/cc/input/scroll_utils.cc
@@ -37,6 +37,17 @@
 }
 
 // static
+int ScrollUtils::CalculateMinPageSnap(int length) {
+  const int min_page_step = length * kMinFractionToStepWhenSnapPaging;
+  return std::max(min_page_step, 1);
+}
+
+// static
+int ScrollUtils::CalculateMaxPageSnap(int length) {
+  return std::max(length, 1);
+}
+
+// static
 int ScrollUtils::CalculatePageStep(int length) {
   const int min_page_step = length * kMinFractionToStepWhenPaging;
   const int page_step =
diff --git a/cc/input/scroll_utils.h b/cc/input/scroll_utils.h
index 947156e..e2b0b56 100644
--- a/cc/input/scroll_utils.h
+++ b/cc/input/scroll_utils.h
@@ -18,6 +18,7 @@
 namespace cc {
 
 static constexpr int kPixelsPerLineStep = 40;
+static constexpr float kMinFractionToStepWhenSnapPaging = 0.4f;
 static constexpr float kMinFractionToStepWhenPaging = 0.875f;
 #if BUILDFLAG(IS_MAC)
 static constexpr int kMaxOverlapBetweenPages = 40;
@@ -38,6 +39,8 @@
       const gfx::SizeF& scroller_size,
       const gfx::SizeF& viewport_size);
 
+  static int CalculateMinPageSnap(int length);
+  static int CalculateMaxPageSnap(int length);
   static int CalculatePageStep(int length);
 };
 
diff --git a/cc/input/snap_selection_strategy.cc b/cc/input/snap_selection_strategy.cc
index c62e5c5..025d525 100644
--- a/cc/input/snap_selection_strategy.cc
+++ b/cc/input/snap_selection_strategy.cc
@@ -5,6 +5,9 @@
 #include "cc/input/snap_selection_strategy.h"
 
 #include <cmath>
+#include <limits>
+
+#include "ui/gfx/geometry/vector2d_f.h"
 
 namespace cc {
 
@@ -21,16 +24,36 @@
                                           gfx::Vector2dF step,
                                           bool use_fractional_offsets,
                                           SnapStopAlwaysFilter filter) {
-  return std::make_unique<DirectionStrategy>(current_position, step, filter,
-                                             use_fractional_offsets);
+  return std::make_unique<DirectionStrategy>(
+      current_position, step, DirectionStrategy::StepPreference::kDirection,
+      gfx::Vector2dF(), gfx::Vector2dF(), filter, use_fractional_offsets);
 }
 
 std::unique_ptr<SnapSelectionStrategy>
-SnapSelectionStrategy::CreateForEndAndDirection(gfx::PointF current_position,
-                                                gfx::Vector2dF displacement,
-                                                bool use_fractional_offsets) {
-  return std::make_unique<EndAndDirectionStrategy>(
-      current_position, displacement, use_fractional_offsets);
+SnapSelectionStrategy::CreateForDisplacement(gfx::PointF current_position,
+                                             gfx::Vector2dF displacement,
+                                             bool use_fractional_offsets,
+                                             SnapStopAlwaysFilter filter) {
+  return std::make_unique<DirectionStrategy>(
+      current_position, displacement,
+      DirectionStrategy::StepPreference::kDistance, gfx::Vector2dF(),
+      gfx::Vector2dF(std::numeric_limits<float>::max(),
+                     std::numeric_limits<float>::max()),
+      filter, use_fractional_offsets);
+}
+
+std::unique_ptr<SnapSelectionStrategy>
+SnapSelectionStrategy::CreateForPreferredDisplacement(
+    gfx::PointF current_position,
+    gfx::Vector2dF displacement,
+    gfx::Vector2dF min_displacement,
+    gfx::Vector2dF max_displacement,
+    bool use_fractional_offsets,
+    SnapStopAlwaysFilter filter) {
+  return std::make_unique<DirectionStrategy>(
+      current_position, displacement,
+      DirectionStrategy::StepPreference::kDistance, min_displacement,
+      max_displacement, filter, use_fractional_offsets);
 }
 
 std::unique_ptr<SnapSelectionStrategy>
@@ -79,6 +102,11 @@
   return current_position_;
 }
 
+bool EndPositionStrategy::IsPreferredSnapPosition(SearchAxis axis,
+                                                  float position) const {
+  return true;
+}
+
 // |position| is unused in this method.
 bool EndPositionStrategy::IsValidSnapPosition(SearchAxis axis,
                                               float position) const {
@@ -117,7 +145,22 @@
 }
 
 gfx::PointF DirectionStrategy::base_position() const {
-  return current_position_;
+  return preferred_step_ == StepPreference::kDirection
+             ? current_position_
+             : current_position_ + step_;
+}
+
+bool DirectionStrategy::IsPreferredSnapPosition(SearchAxis axis,
+                                                float position) const {
+  if (axis == SearchAxis::kX) {
+    float delta = position - current_position_.x();
+    return std::abs(delta) >= std::abs(preferred_min_displacement_.x()) &&
+           std::abs(delta) <= std::abs(preferred_max_displacement_.x());
+  } else {
+    float delta = position - current_position_.y();
+    return std::abs(delta) >= std::abs(preferred_min_displacement_.y()) &&
+           std::abs(delta) <= std::abs(preferred_max_displacement_.y());
+  }
 }
 
 bool DirectionStrategy::IsValidSnapPosition(SearchAxis axis,
@@ -148,6 +191,10 @@
           area.must_snap);
 }
 
+bool DirectionStrategy::ShouldRespectSnapStop() const {
+  return true;
+}
+
 const std::optional<SnapSearchResult>& DirectionStrategy::PickBestResult(
     const std::optional<SnapSearchResult>& closest,
     const std::optional<SnapSearchResult>& covering) const {
@@ -160,15 +207,25 @@
   if (!covering.has_value())
     return closest;
 
-  // "Right" or "Down" arrow.
-  if ((step_.x() > 0 || step_.y() > 0) &&
-      closest.value().snap_offset() < covering.value().snap_offset()) {
-    return closest;
+  // If covering and closest represent the same snap area, covering best
+  // preserves the intended scroll position.
+  if (covering->element_id() == closest->element_id()) {
+    return covering;
   }
-  // "Left" or "Up" arrow.
-  if ((step_.x() < 0 || step_.y() < 0) &&
-      closest.value().snap_offset() > covering.value().snap_offset()) {
-    return closest;
+
+  // If we only intend to scroll in the given direction, prefer the closer
+  // snap position.
+  if (preferred_step_ == StepPreference::kDirection) {
+    // Scroll right or down.
+    if ((step_.x() > 0 || step_.y() > 0) &&
+        closest.value().snap_offset() < covering.value().snap_offset()) {
+      return closest;
+    }
+    // Scroll left or up.
+    if ((step_.x() < 0 || step_.y() < 0) &&
+        closest.value().snap_offset() > covering.value().snap_offset()) {
+      return closest;
+    }
   }
 
   return covering;
@@ -182,59 +239,4 @@
   return std::make_unique<DirectionStrategy>(*this);
 }
 
-bool EndAndDirectionStrategy::ShouldSnapOnX() const {
-  return displacement_.x() != 0;
-}
-
-bool EndAndDirectionStrategy::ShouldSnapOnY() const {
-  return displacement_.y() != 0;
-}
-
-gfx::PointF EndAndDirectionStrategy::intended_position() const {
-  return current_position_ + displacement_;
-}
-
-gfx::PointF EndAndDirectionStrategy::base_position() const {
-  return current_position_ + displacement_;
-}
-
-bool EndAndDirectionStrategy::IsValidSnapPosition(SearchAxis axis,
-                                                  float position) const {
-  // If not using fractional offsets then it is possible for the currently
-  // snapped area's offset, which is fractional, to not be equal to the current
-  // scroll offset, which is not fractional. Therefore we round the offsets so
-  // that any position within 0.5 of the current position is ignored.
-  if (axis == SearchAxis::kX) {
-    float delta = position - current_position_.x();
-    if (!use_fractional_offsets_)
-      delta = std::round(delta);
-    return (displacement_.x() > 0 && delta > 0) ||  // Right
-           (displacement_.x() < 0 && delta < 0);    // Left
-  } else {
-    float delta = position - current_position_.y();
-    if (!use_fractional_offsets_)
-      delta = std::round(delta);
-    return (displacement_.y() > 0 && delta > 0) ||  // Down
-           (displacement_.y() < 0 && delta < 0);    // Up
-  }
-}
-
-bool EndAndDirectionStrategy::ShouldRespectSnapStop() const {
-  return true;
-}
-
-const std::optional<SnapSearchResult>& EndAndDirectionStrategy::PickBestResult(
-    const std::optional<SnapSearchResult>& closest,
-    const std::optional<SnapSearchResult>& covering) const {
-  return covering.has_value() ? covering : closest;
-}
-
-bool EndAndDirectionStrategy::UsingFractionalOffsets() const {
-  return use_fractional_offsets_;
-}
-
-std::unique_ptr<SnapSelectionStrategy> EndAndDirectionStrategy::Clone() const {
-  return std::make_unique<EndAndDirectionStrategy>(*this);
-}
-
 }  // namespace cc
diff --git a/cc/input/snap_selection_strategy.h b/cc/input/snap_selection_strategy.h
index 5e7462d..5e5afeb 100644
--- a/cc/input/snap_selection_strategy.h
+++ b/cc/input/snap_selection_strategy.h
@@ -22,11 +22,15 @@
  public:
   SnapSelectionStrategy() = default;
   virtual ~SnapSelectionStrategy() = default;
+  // Strategy for scrolling to a particular position in a non-directional
+  // manner.
   static std::unique_ptr<SnapSelectionStrategy> CreateForEndPosition(
       const gfx::PointF& current_position,
       bool scrolled_x,
       bool scrolled_y);
 
+  // Strategy for scrolling in a direction by some small amount,
+  // giving preference to stop at snap areas.
   // |use_fractional_offsets| should be true when the current position is
   // provided in fractional pixels.
   static std::unique_ptr<SnapSelectionStrategy> CreateForDirection(
@@ -34,10 +38,30 @@
       gfx::Vector2dF step,
       bool use_fractional_offsets,
       SnapStopAlwaysFilter filter = SnapStopAlwaysFilter::kIgnore);
-  static std::unique_ptr<SnapSelectionStrategy> CreateForEndAndDirection(
+
+  // Strategy for scrolling by some large offset in a particular direction.
+  // Unlike CreateForDirection, prefers scrolling by the given displacement
+  // over snapping to nearby points.
+  // |use_fractional_offsets| should be true when the current position is
+  // provided in fractional pixels.
+  static std::unique_ptr<SnapSelectionStrategy> CreateForDisplacement(
       gfx::PointF current_position,
       gfx::Vector2dF displacement,
-      bool use_fractional_offsets);
+      bool use_fractional_offsets,
+      SnapStopAlwaysFilter filter = SnapStopAlwaysFilter::kIgnore);
+
+  // Similar to the previous strategy, this prefers scrolling by the given
+  // displacement. However, it additionally prefers snap points that scroll
+  // within the given range.
+  // |use_fractional_offsets| should be true when the current position is
+  // provided in fractional pixels.
+  static std::unique_ptr<SnapSelectionStrategy> CreateForPreferredDisplacement(
+      gfx::PointF current_position,
+      gfx::Vector2dF displacement,
+      gfx::Vector2dF min_displacement,
+      gfx::Vector2dF max_displacement,
+      bool use_fractional_offsets,
+      SnapStopAlwaysFilter filter = SnapStopAlwaysFilter::kIgnore);
 
   // Creates a selection strategy that attempts to snap to previously snapped
   // targets if possible, but defaults to finding the closest snap point if
@@ -61,6 +85,10 @@
   // Returns the current scroll position of the snap container.
   const gfx::PointF& current_position() const { return current_position_; }
 
+  // Returns true if the given snap offset matches the strategy's preference.
+  virtual bool IsPreferredSnapPosition(SearchAxis axis,
+                                       float position) const = 0;
+
   // Returns true if the selection strategy considers the given snap offset
   // valid for the current axis.
   virtual bool IsValidSnapPosition(SearchAxis axis, float position) const = 0;
@@ -124,6 +152,7 @@
   gfx::PointF intended_position() const override;
   gfx::PointF base_position() const override;
 
+  bool IsPreferredSnapPosition(SearchAxis axis, float position) const override;
   bool IsValidSnapPosition(SearchAxis axis, float position) const override;
   bool HasIntendedDirection() const override;
   bool ShouldPrioritizeSnapTargets() const override;
@@ -143,21 +172,41 @@
 // Examples for intended direction scrolls include
 // - pressing an arrow key on the keyboard
 // - a swiping gesture interpreted as a fixed (rather than inertial) scroll
-// For this type of scrolls, we want to
-// * Minimize the distance between the snap position and the starting position,
+// - a “fling” gesture, interpreted with momentum
+// - programmatically scrolling via APIs such as scrollBy()
+// - paging operations such as the PgUp/PgDn keys (or equivalent operations on
+//   the scrollbar)
+// For this type of scroll, we want to
+// * Minimize the distance between the snap position and
+//   the starting position if we only prefer the direction
 //   so that we stop at the first snap position in that direction.
+// * When the step distance is preferred, prefer skipping snap positions
+//   to scroll closer to the step distance.
 // * Return the default intended position (using the default step) if that makes
 //   a snap area covers the snapport.
 class DirectionStrategy : public SnapSelectionStrategy {
  public:
+  enum class StepPreference {
+    // Prefer only the direction, but otherwise choose a closer snap position.
+    kDirection,
+    // Prefer snap areas close to the specified step distance.
+    kDistance
+  };
+
   // |use_fractional_offsets| should be true when the current position is
   // provided in fractional pixels.
   DirectionStrategy(const gfx::PointF& current_position,
                     const gfx::Vector2dF& step,
+                    StepPreference preferred_step,
+                    const gfx::Vector2dF preferred_min_displacement,
+                    const gfx::Vector2dF preferred_max_displacement,
                     SnapStopAlwaysFilter filter,
                     bool use_fractional_offsets)
       : SnapSelectionStrategy(current_position),
         step_(step),
+        preferred_step_(preferred_step),
+        preferred_min_displacement_(preferred_min_displacement),
+        preferred_max_displacement_(preferred_max_displacement),
         snap_stop_always_filter_(filter),
         use_fractional_offsets_(use_fractional_offsets) {}
   DirectionStrategy(const DirectionStrategy& other) = default;
@@ -169,54 +218,11 @@
   gfx::PointF intended_position() const override;
   gfx::PointF base_position() const override;
 
+  bool IsPreferredSnapPosition(SearchAxis axis, float position) const override;
   bool IsValidSnapPosition(SearchAxis axis, float position) const override;
   bool IsValidSnapArea(SearchAxis axis,
                        const SnapAreaData& area) const override;
 
-  const std::optional<SnapSearchResult>& PickBestResult(
-      const std::optional<SnapSearchResult>& closest,
-      const std::optional<SnapSearchResult>& covering) const override;
-
-  bool UsingFractionalOffsets() const override;
-
-  std::unique_ptr<SnapSelectionStrategy> Clone() const override;
-
- private:
-  // The default step for this DirectionStrategy.
-  const gfx::Vector2dF step_;
-  SnapStopAlwaysFilter snap_stop_always_filter_;
-  bool use_fractional_offsets_;
-};
-
-// Examples for intended direction and end position scrolls include
-// - a “fling” gesture, interpreted with momentum
-// - programmatically scrolling via APIs such as scrollBy()
-// - paging operations such as the PgUp/PgDn keys (or equivalent operations on
-//   the scrollbar)
-// For this type of scrolls, we want to
-// * Minimize the distance between the snap position and the end position.
-// * Return the end position if that makes a snap area covers the snapport.
-class EndAndDirectionStrategy : public SnapSelectionStrategy {
- public:
-  // |use_fractional_offsets| should be true when the current position is
-  // provided in fractional pixels.
-  EndAndDirectionStrategy(const gfx::PointF& current_position,
-                          const gfx::Vector2dF& displacement,
-                          bool use_fractional_offsets)
-      : SnapSelectionStrategy(current_position),
-        displacement_(displacement),
-        use_fractional_offsets_(use_fractional_offsets) {}
-  EndAndDirectionStrategy(const EndAndDirectionStrategy& other) = default;
-  ~EndAndDirectionStrategy() override = default;
-
-  bool ShouldSnapOnX() const override;
-  bool ShouldSnapOnY() const override;
-
-  gfx::PointF intended_position() const override;
-  gfx::PointF base_position() const override;
-
-  bool IsValidSnapPosition(SearchAxis axis, float position) const override;
-
   bool ShouldRespectSnapStop() const override;
 
   const std::optional<SnapSearchResult>& PickBestResult(
@@ -228,7 +234,18 @@
   std::unique_ptr<SnapSelectionStrategy> Clone() const override;
 
  private:
-  const gfx::Vector2dF displacement_;
+  // The default step for this DirectionStrategy.
+  const gfx::Vector2dF step_;
+  // How strictly to prefer the step's magnitude.
+  const StepPreference preferred_step_;
+
+  // Some operations, e.g. scrolling by a page, prefer snap areas that
+  // scroll no more than a certain amount and at least a certain amount.
+  // 0 represents unrestricted displacement.
+  const gfx::Vector2dF preferred_min_displacement_;
+  const gfx::Vector2dF preferred_max_displacement_;
+
+  SnapStopAlwaysFilter snap_stop_always_filter_;
   bool use_fractional_offsets_;
 };
 
diff --git a/cc/layers/picture_layer_impl.cc b/cc/layers/picture_layer_impl.cc
index 3d56810..a2d09d2 100644
--- a/cc/layers/picture_layer_impl.cc
+++ b/cc/layers/picture_layer_impl.cc
@@ -1076,9 +1076,18 @@
   if (raster_source_) {
     const ScrollTree& scroll_tree =
         layer_tree_impl()->property_trees()->scroll_tree();
+    const TransformTree& transform_tree =
+        layer_tree_impl()->property_trees()->transform_tree();
     for (auto [element_id, _] :
          raster_source_->GetDisplayItemList()->raster_inducing_scrolls()) {
-      map[element_id] = scroll_tree.current_scroll_offset(element_id);
+      // The transform node has the realized scroll offset and snap amount,
+      // and should be used for rendering.
+      const auto* scroll_node = scroll_tree.FindNodeFromElementId(element_id);
+      CHECK(scroll_node);
+      const auto* transform = transform_tree.Node(scroll_node->transform_id);
+      CHECK(transform);
+      map[element_id] =
+          gfx::PointAtOffsetFromOrigin(-transform->to_parent.To2dTranslation());
     }
   }
   return map;
diff --git a/cc/layers/picture_layer_impl_unittest.cc b/cc/layers/picture_layer_impl_unittest.cc
index 2722c16..be679766 100644
--- a/cc/layers/picture_layer_impl_unittest.cc
+++ b/cc/layers/picture_layer_impl_unittest.cc
@@ -2301,13 +2301,14 @@
   RecordingSource recording;
   Region invalidation;
   recording.Update(layer_bounds, 1, client, invalidation);
-  SetupPendingTreeWithFixedTileSize(
-      FakeRasterSource::CreateFromRecordingSource(recording), tile_size,
-      Region());
+  SetupPendingTreeWithFixedTileSize(nullptr, tile_size, Region());
+  pending_layer()->SetBounds(layer_bounds);
   CreateScrollNodeForNonCompositedScroller(
       host_impl()->pending_tree()->property_trees(),
       pending_layer()->scroll_tree_index(), scroll_element_id,
       scroll_contents_bounds, scroll_container_bounds, scroll_container_origin);
+  pending_layer()->SetRasterSource(
+      FakeRasterSource::CreateFromRecordingSource(recording), invalidation);
   ScrollTree& pending_scroll_tree =
       host_impl()->pending_tree()->property_trees()->scroll_tree_mutable();
   pending_scroll_tree.SetScrollingContentsCullRect(scroll_element_id,
@@ -6702,14 +6703,36 @@
   EXPECT_EQ(gfx::Rect(100, 300, 200, 200), info2.visual_rect);
   EXPECT_FALSE(info2.has_discardable_images);
 
+  gfx::Size layer_bounds(500, 500);
+  SetupPendingTree();
+  pending_layer()->SetBounds(layer_bounds);
+  auto* property_trees = host_impl()->pending_tree()->property_trees();
+  auto& scroll_tree = property_trees->scroll_tree_mutable();
+  int scroll_node_id1 =
+      CreateScrollNodeForNonCompositedScroller(
+          property_trees, pending_layer()->scroll_tree_index(),
+          scroll_element_id1, gfx::Size(200, 200), gfx::Size(300, 200),
+          gfx::Point())
+          .id;
+  ASSERT_TRUE(scroll_tree.CanRealizeScrollsOnPendingTree(
+      *scroll_tree.Node(scroll_node_id1)));
+  int scroll_node_id2 =
+      CreateScrollNodeForNonCompositedScroller(
+          property_trees, scroll_node_id1, scroll_element_id2,
+          gfx::Size(200, 200), gfx::Size(1000, 1000), gfx::Point(100, 300))
+          .id;
+  ASSERT_TRUE(scroll_tree.CanRealizeScrollsOnPendingTree(
+      *scroll_tree.Node(scroll_node_id2)));
+
   FakeContentLayerClient client;
   client.set_display_item_list(display_list);
-  gfx::Size layer_bounds(500, 500);
   RecordingSource recording;
   Region invalidation;
   recording.Update(layer_bounds, 1, client, invalidation);
   auto raster = FakeRasterSource::CreateFromRecordingSource(recording);
-  SetupTreesWithInvalidation(raster, raster, invalidation);
+  pending_layer()->SetRasterSource(raster, Region());
+  ActivateTree();
+  SetupPendingTree(raster, layer_bounds, invalidation);
   pending_layer()->set_invalidation(Region());
 
   auto pending_image_map = pending_layer()->discardable_image_map();
@@ -6720,12 +6743,12 @@
               ElementsAre(gfx::Rect(-1, -1, 202, 202)));
 
   // Simulate a raster-inducing scroll.
-  host_impl()
-      ->pending_tree()
-      ->property_trees()
-      ->scroll_tree_mutable()
-      .GetOrCreateSyncedScrollOffsetForTesting(scroll_element_id1)
-      ->SetCurrent(gfx::PointF(0, 100));
+  scroll_tree.GetOrCreateSyncedScrollOffsetForTesting(scroll_element_id1)
+      ->SetCurrent(gfx::PointF(0, 100.25f));
+  host_impl()->pending_tree()->DidUpdateScrollOffset(scroll_element_id1, false);
+  property_trees->transform_tree_mutable().UpdateTransforms(
+      scroll_tree.Node(scroll_node_id1)->transform_id);
+
   // Invalidating scroll_element_id1 will invalidate scroll visual rect.
   pending_layer()->InvalidateRasterInducingScrolls({scroll_element_id1});
   EXPECT_EQ(info1.visual_rect, pending_layer()->invalidation().bounds());
diff --git a/cc/mojom/BUILD.gn b/cc/mojom/BUILD.gn
index a4235bb2..cd3fbc16 100644
--- a/cc/mojom/BUILD.gn
+++ b/cc/mojom/BUILD.gn
@@ -170,8 +170,8 @@
           copyable_pass_by_value = true
         },
       ]
-      traits_headers = [ "//cc/mojom/synced_scroll_offset_type_traits.h" ]
-      traits_sources = [ "//cc/mojom/synced_scroll_offset_type_traits.cc" ]
+      traits_headers = [ "//cc/mojom/synced_scroll_offset_mojom_traits.h" ]
+      traits_sources = [ "//cc/mojom/synced_scroll_offset_mojom_traits.cc" ]
       traits_public_deps = [ "//ui/gfx/geometry/mojom" ]
     },
     {
diff --git a/cc/mojom/synced_scroll_offset_type_traits.cc b/cc/mojom/synced_scroll_offset_mojom_traits.cc
similarity index 92%
rename from cc/mojom/synced_scroll_offset_type_traits.cc
rename to cc/mojom/synced_scroll_offset_mojom_traits.cc
index dd43bc4..f232572 100644
--- a/cc/mojom/synced_scroll_offset_type_traits.cc
+++ b/cc/mojom/synced_scroll_offset_mojom_traits.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "cc/mojom/synced_scroll_offset_type_traits.h"
+#include "cc/mojom/synced_scroll_offset_mojom_traits.h"
 
 #include <utility>
 
diff --git a/cc/mojom/synced_scroll_offset_type_traits.h b/cc/mojom/synced_scroll_offset_mojom_traits.h
similarity index 84%
rename from cc/mojom/synced_scroll_offset_type_traits.h
rename to cc/mojom/synced_scroll_offset_mojom_traits.h
index 7ffba6f8..6be7cb6 100644
--- a/cc/mojom/synced_scroll_offset_type_traits.h
+++ b/cc/mojom/synced_scroll_offset_mojom_traits.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef CC_MOJOM_SYNCED_SCROLL_OFFSET_TYPE_TRAITS_H_
-#define CC_MOJOM_SYNCED_SCROLL_OFFSET_TYPE_TRAITS_H_
+#ifndef CC_MOJOM_SYNCED_SCROLL_OFFSET_MOJOM_TRAITS_H_
+#define CC_MOJOM_SYNCED_SCROLL_OFFSET_MOJOM_TRAITS_H_
 
 #include "base/memory/scoped_refptr.h"
 #include "cc/mojom/synced_scroll_offset.mojom-shared.h"
@@ -33,4 +33,4 @@
 
 }  // namespace mojo
 
-#endif  // CC_MOJOM_SYNCED_SCROLL_OFFSET_TYPE_TRAITS_H_
+#endif  // CC_MOJOM_SYNCED_SCROLL_OFFSET_MOJOM_TRAITS_H_
diff --git a/chrome/android/java/res/xml/bookmark_widget_info.xml b/chrome/android/java/res/xml/bookmark_widget_info.xml
index 1773346..e05c5ec 100644
--- a/chrome/android/java/res/xml/bookmark_widget_info.xml
+++ b/chrome/android/java/res/xml/bookmark_widget_info.xml
@@ -9,7 +9,7 @@
 <appwidget-provider
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:minWidth="180dp"
-    android:minHeight="180dp"
+    android:minHeight="150dp"
     android:minResizeWidth="@dimen/bookmark_widget_min_width"
     android:minResizeHeight="@dimen/bookmark_widget_min_height"
     android:previewImage="@drawable/bookmark_widget_preview"
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/BrowsingDataCounterBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/BrowsingDataCounterBridge.java
index 7780e41..13f49fd 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/BrowsingDataCounterBridge.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/BrowsingDataCounterBridge.java
@@ -8,11 +8,13 @@
 import org.jni_zero.JniType;
 import org.jni_zero.NativeMethods;
 
+import org.chromium.build.annotations.NullMarked;
 import org.chromium.chrome.browser.profiles.Profile;
 
 /**
  * Communicates between BrowsingDataCounter (C++ backend) and ClearBrowsingDataFragment (Java UI).
  */
+@NullMarked
 public class BrowsingDataCounterBridge {
     /** Can receive a callback from a BrowsingDataCounter. */
     public interface BrowsingDataCounterCallback {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/ClearBrowsingDataCheckBoxPreference.java b/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/ClearBrowsingDataCheckBoxPreference.java
index 4072fa9c..5c32d34 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/ClearBrowsingDataCheckBoxPreference.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/ClearBrowsingDataCheckBoxPreference.java
@@ -16,18 +16,23 @@
 
 import androidx.preference.PreferenceViewHolder;
 
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
 import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
 import org.chromium.components.browser_ui.settings.ChromeBaseCheckBoxPreference;
 import org.chromium.ui.text.ChromeClickableSpan;
 import org.chromium.ui.text.SpanApplier;
 
+import java.util.Objects;
+
 /**
  * A preference representing one browsing data type in ClearBrowsingDataFragment. This class allows
  * clickable links inside the checkbox summary.
  */
+@NullMarked
 public class ClearBrowsingDataCheckBoxPreference extends ChromeBaseCheckBoxPreference {
-    private View mView;
-    private Runnable mLinkClickDelegate;
+    private @Nullable View mView;
+    private @Nullable Runnable mLinkClickDelegate;
     private boolean mHasClickableSpans;
 
     /** Constructor for inflating from XML. */
@@ -94,9 +99,9 @@
     }
 
     @Override
-    public void setSummary(CharSequence summary) {
+    public void setSummary(@Nullable CharSequence summary) {
         // If there is no link in the summary invoke the default behavior.
-        String summaryString = summary.toString();
+        String summaryString = Objects.requireNonNullElse(summary, "").toString();
         if (!summaryString.contains("<link>") || !summaryString.contains("</link>")) {
             super.setSummary(summary);
             return;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/UrlFilter.java b/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/UrlFilter.java
index 659625be..86439f1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/UrlFilter.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/UrlFilter.java
@@ -4,7 +4,10 @@
 
 package org.chromium.chrome.browser.browsing_data;
 
+import org.chromium.build.annotations.NullMarked;
+
 /** A URL->boolean filter used in browsing data deletions. */
+@NullMarked
 public interface UrlFilter {
     /**
      * @param url The url to be matched.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/AuthTabColorProvider.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/AuthTabColorProvider.java
index 622883e..0de27aad 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/AuthTabColorProvider.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/AuthTabColorProvider.java
@@ -12,29 +12,29 @@
 import android.graphics.Color;
 
 import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.browser.auth.AuthTabColorSchemeParams;
 import androidx.browser.auth.AuthTabIntent;
 import androidx.browser.customtabs.CustomTabsIntent;
 
 import org.chromium.base.Log;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
 import org.chromium.chrome.browser.browserservices.intents.ColorProvider;
 import org.chromium.chrome.browser.theme.SurfaceColorUpdateUtils;
 import org.chromium.ui.util.ColorUtils;
 
 /** {@link ColorProvider} implementation used for Auth Tab. */
+@NullMarked
 public class AuthTabColorProvider implements ColorProvider {
     private static final String TAG = "AuthTabColorProvider";
 
     private final @ColorInt int mToolbarColor;
     private final @ColorInt int mBottomBarColor;
     private final boolean mHasCustomToolbarColor;
-    @Nullable private final Integer mNavigationBarColor;
-    @Nullable private final Integer mNavigationBarDividerColor;
+    private final @Nullable Integer mNavigationBarColor;
+    private final @Nullable Integer mNavigationBarDividerColor;
 
-    private static @NonNull AuthTabColorSchemeParams getColorSchemeParams(
-            Intent intent, int colorScheme) {
+    private static AuthTabColorSchemeParams getColorSchemeParams(Intent intent, int colorScheme) {
         if (colorScheme == COLOR_SCHEME_SYSTEM) {
             assert false
                     : "Color scheme passed to IntentDataProvider should not be "
@@ -51,12 +51,10 @@
     }
 
     public AuthTabColorProvider(
-            @NonNull Intent intent,
-            @NonNull Context context,
-            @CustomTabsIntent.ColorScheme int colorScheme) {
+            Intent intent, Context context, @CustomTabsIntent.ColorScheme int colorScheme) {
         AuthTabColorSchemeParams params = getColorSchemeParams(intent, colorScheme);
         mHasCustomToolbarColor = params.getToolbarColor() != null;
-        mToolbarColor = retrieveToolbarColor(params, context, mHasCustomToolbarColor);
+        mToolbarColor = retrieveToolbarColor(params, context);
         mBottomBarColor = mToolbarColor;
         mNavigationBarColor =
                 params.getNavigationBarColor() == null
@@ -65,9 +63,8 @@
         mNavigationBarDividerColor = params.getNavigationBarDividerColor();
     }
 
-    private static int retrieveToolbarColor(
-            AuthTabColorSchemeParams params, Context context, boolean hasCustomToolbarColor) {
-        if (hasCustomToolbarColor) {
+    private static int retrieveToolbarColor(AuthTabColorSchemeParams params, Context context) {
+        if (params.getToolbarColor() != null) {
             return ColorUtils.getOpaqueColor(params.getToolbarColor());
         }
         return SurfaceColorUpdateUtils.getDefaultThemeColor(context, /* isIncognito= */ false);
@@ -84,12 +81,12 @@
     }
 
     @Override
-    public Integer getNavigationBarColor() {
+    public @Nullable Integer getNavigationBarColor() {
         return mNavigationBarColor;
     }
 
     @Override
-    public Integer getNavigationBarDividerColor() {
+    public @Nullable Integer getNavigationBarDividerColor() {
         return mNavigationBarDividerColor;
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabAuthUrlHeuristics.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabAuthUrlHeuristics.java
index cfbf8de..8fee327f 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabAuthUrlHeuristics.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabAuthUrlHeuristics.java
@@ -12,6 +12,8 @@
 import org.jni_zero.NativeMethods;
 
 import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
 import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.components.embedder_support.util.UrlConstants;
@@ -23,6 +25,7 @@
  * Helper class to record histogram to determine whether the Custom Tab was launched with what looks
  * like an OAuth URL.
  */
+@NullMarked
 public class CustomTabAuthUrlHeuristics {
     // This should be kept in sync with the definition |CustomTabsAuthScheme| in
     // tools/metrics/histograms/metadata/custom_tabs/enums.xml.
@@ -112,7 +115,7 @@
                 CustomTabAuthUrlHeuristics.AuthScheme.COUNT);
     }
 
-    static @CustomTabAuthUrlHeuristics.AuthScheme int getAuthSchemeEnum(String scheme) {
+    static @CustomTabAuthUrlHeuristics.AuthScheme int getAuthSchemeEnum(@Nullable String scheme) {
         if (UrlConstants.HTTP_SCHEME.equalsIgnoreCase(scheme)) {
             return CustomTabAuthUrlHeuristics.AuthScheme.HTTP;
         } else if (UrlConstants.HTTPS_SCHEME.equalsIgnoreCase(scheme)) {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabBottomBarView.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabBottomBarView.java
index 74e65dd..3c039e795 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabBottomBarView.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabBottomBarView.java
@@ -9,13 +9,14 @@
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 
-import androidx.annotation.Nullable;
-
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
 import org.chromium.components.browser_ui.widget.BoundedLinearLayout;
 import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener;
 import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.SwipeHandler;
 
 /** A custom container view for the Custom Tab bottom bar that supports swipe gesture handling. */
+@NullMarked
 public class CustomTabBottomBarView extends BoundedLinearLayout {
     private final Context mContext;
     private @Nullable BottomBarSwipeGestureListener mSwipeGestureListener;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabFileUtils.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabFileUtils.java
index 867070b..10861a7 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabFileUtils.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabFileUtils.java
@@ -4,6 +4,8 @@
 
 package org.chromium.chrome.browser.customtabs;
 
+import org.chromium.build.annotations.NullMarked;
+
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -11,6 +13,7 @@
 import java.util.List;
 
 /** File utilities for Custom Tabs. */
+@NullMarked
 public class CustomTabFileUtils {
     /** Threshold where old state files should be deleted (30 days). */
     protected static final long STATE_EXPIRY_THRESHOLD = 30L * 24 * 60 * 60 * 1000;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabNightModeStateController.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabNightModeStateController.java
index aeaa6106..e791cec 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabNightModeStateController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabNightModeStateController.java
@@ -6,13 +6,15 @@
 
 import android.content.Intent;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatDelegate;
 import androidx.browser.customtabs.CustomTabsIntent;
 
 import org.chromium.base.IntentUtils;
 import org.chromium.base.ObserverList;
+import org.chromium.build.annotations.MonotonicNonNull;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.build.annotations.RequiresNonNull;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
 import org.chromium.chrome.browser.lifecycle.DestroyObserver;
 import org.chromium.chrome.browser.night_mode.NightModeStateProvider;
@@ -21,6 +23,7 @@
 import org.chromium.chrome.browser.night_mode.SystemNightModeMonitor;
 
 /** Maintains and provides the night mode state for {@link CustomTabActivity}. */
+@NullMarked
 public class CustomTabNightModeStateController implements DestroyObserver, NightModeStateProvider {
     private final ObserverList<Observer> mObservers = new ObserverList<>();
     private final PowerSavingModeMonitor mPowerSavingModeMonitor;
@@ -34,7 +37,7 @@
      */
     private int mRequestedColorScheme;
 
-    private AppCompatDelegate mAppCompatDelegate;
+    private @MonotonicNonNull AppCompatDelegate mAppCompatDelegate;
 
     @Nullable // Null initially, so that the first update is always applied (see updateNightMode()).
     private Boolean mIsInNightMode;
@@ -90,12 +93,12 @@
     }
 
     @Override
-    public void addObserver(@NonNull Observer observer) {
+    public void addObserver(Observer observer) {
         mObservers.addObserver(observer);
     }
 
     @Override
-    public void removeObserver(@NonNull Observer observer) {
+    public void removeObserver(Observer observer) {
         mObservers.removeObserver(observer);
     }
 
@@ -106,6 +109,7 @@
         return false;
     }
 
+    @RequiresNonNull({"mAppCompatDelegate"})
     private void updateNightMode() {
         boolean shouldBeInNightMode = shouldBeInNightMode();
         if (mIsInNightMode != null && mIsInNightMode == shouldBeInNightMode) return;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTaskDescriptionIconGenerator.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTaskDescriptionIconGenerator.java
index f0ef8f94..6e11a6b 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTaskDescriptionIconGenerator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTaskDescriptionIconGenerator.java
@@ -7,10 +7,13 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
 import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
 import org.chromium.url.GURL;
 
 /** Generates icons suitable for Custom Tabs in the recent tasks list. */
+@NullMarked
 public class CustomTabTaskDescriptionIconGenerator {
     private static final int APP_ICON_MIN_SIZE_DP = 32;
     private static final int APP_ICON_SIZE_DP = 64;
@@ -22,13 +25,13 @@
     private final int mMinSizePx;
 
     /** The page URL for which {@link #mGeneratedIcon} was generated. */
-    private GURL mGeneratedPageUrl;
+    private @Nullable GURL mGeneratedPageUrl;
 
     /** The most recently generated icon. */
-    private Bitmap mGeneratedIcon;
+    private @Nullable Bitmap mGeneratedIcon;
 
     /** Generates the icon if there is no adequate favicon. */
-    private RoundedIconGenerator mGenerator;
+    private @Nullable RoundedIconGenerator mGenerator;
 
     public CustomTabTaskDescriptionIconGenerator(Context context) {
         mContext = context;
@@ -45,7 +48,7 @@
      * @param largestFavicon The largest favicon available at the page URL.
      * @return The icon to use in the recent tasks list.
      */
-    public Bitmap getBitmap(GURL pageUrl, Bitmap largestFavicon) {
+    public @Nullable Bitmap getBitmap(GURL pageUrl, Bitmap largestFavicon) {
         if (largestFavicon != null
                 && largestFavicon.getWidth() >= mMinSizePx
                 && largestFavicon.getHeight() >= mMinSizePx) {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTrustedCdnPublisherUrlVisibility.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTrustedCdnPublisherUrlVisibility.java
index 7b9ef3d..64d6063 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTrustedCdnPublisherUrlVisibility.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTrustedCdnPublisherUrlVisibility.java
@@ -5,6 +5,8 @@
 package org.chromium.chrome.browser.customtabs;
 
 import org.chromium.base.UnownedUserData;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
 import org.chromium.chrome.browser.lifecycle.DestroyObserver;
 import org.chromium.chrome.browser.tab.Tab;
@@ -17,9 +19,10 @@
  * Implementation of {@link TrustedCdn.PublisherUrlVisibility} to provide Tab with
  * the availability of publisher URL of trusted CDN when attached to a custom tab activity.
  */
+@NullMarked
 class CustomTabTrustedCdnPublisherUrlVisibility
         implements PublisherUrlVisibility, DestroyObserver, UnownedUserData {
-    private WindowAndroid mWindowAndroid;
+    private @Nullable WindowAndroid mWindowAndroid;
     private BooleanSupplier mIsPublisherPackageForSession;
 
     CustomTabTrustedCdnPublisherUrlVisibility(
@@ -37,6 +40,7 @@
         return mIsPublisherPackageForSession.getAsBoolean();
     }
 
+    @SuppressWarnings("NullAway")
     @Override
     public void onDestroy() {
         PublisherUrlVisibility.detach(this);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsFeatureUsage.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsFeatureUsage.java
index d2a01db4..2f228f8 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsFeatureUsage.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsFeatureUsage.java
@@ -8,12 +8,14 @@
 import androidx.annotation.VisibleForTesting;
 
 import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.build.annotations.NullMarked;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.BitSet;
 
 /** Records a histogram that tracks usage of all the CCT features of interest. */
+@NullMarked
 public class CustomTabsFeatureUsage {
     @VisibleForTesting
     public static final String CUSTOM_TABS_FEATURE_USAGE_HISTOGRAM = "CustomTabs.FeatureUsage";
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsShareBroadcastReceiver.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsShareBroadcastReceiver.java
index f3336f4..b1098e7 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsShareBroadcastReceiver.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsShareBroadcastReceiver.java
@@ -9,11 +9,13 @@
 import android.content.Intent;
 
 import org.chromium.base.metrics.RecordUserAction;
+import org.chromium.build.annotations.NullMarked;
 
 /**
  * Receives shared content broadcast from Chrome Custom Tabs and shows a share sheet to share the
  * url.
  */
+@NullMarked
 public final class CustomTabsShareBroadcastReceiver extends BroadcastReceiver {
     @Override
     public void onReceive(Context context, Intent intent) {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/IncognitoCustomTabColorProvider.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/IncognitoCustomTabColorProvider.java
index 63d1145..23e9172 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/IncognitoCustomTabColorProvider.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/IncognitoCustomTabColorProvider.java
@@ -7,12 +7,13 @@
 import android.content.Context;
 import android.graphics.Color;
 
-import androidx.annotation.Nullable;
-
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
 import org.chromium.chrome.browser.browserservices.intents.ColorProvider;
 import org.chromium.chrome.browser.theme.SurfaceColorUpdateUtils;
 
 /** ColorProvider implementation used for incognito profiles. */
+@NullMarked
 public final class IncognitoCustomTabColorProvider implements ColorProvider {
     private final int mToolbarColor;
     private final int mBottomBarColor;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/NavigationInfoCaptureTrigger.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/NavigationInfoCaptureTrigger.java
index b33ea51..0aeae8f0 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/NavigationInfoCaptureTrigger.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/NavigationInfoCaptureTrigger.java
@@ -8,6 +8,7 @@
 
 import org.chromium.base.Callback;
 import org.chromium.base.ThreadUtils;
+import org.chromium.build.annotations.NullMarked;
 import org.chromium.chrome.browser.tab.Tab;
 
 import java.util.LinkedList;
@@ -23,6 +24,7 @@
  * If a capture has not been taken after a long amount of time or when the Tab is hidden, we also
  * capture.
  */
+@NullMarked
 public class NavigationInfoCaptureTrigger {
     private static final int ONLOAD_DELAY_MS = 1000;
     private static final int ONLOAD_LONG_DELAY_MS = 15000;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/RequestThrottler.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/RequestThrottler.java
index 31c4021..19d29fe 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/RequestThrottler.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/RequestThrottler.java
@@ -14,6 +14,8 @@
 import org.chromium.base.ContextUtils;
 import org.chromium.base.task.PostTask;
 import org.chromium.base.task.TaskTraits;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
 
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -28,6 +30,7 @@
  *
  * This class is *not* thread-safe.
  */
+@NullMarked
 class RequestThrottler {
     // These are for (a).
     private static final long MIN_DELAY = 100;
@@ -47,14 +50,14 @@
     private static final String BANNED_UNTIL = "banned_until_";
 
     private static final AtomicBoolean sAccessedSharedPreferences = new AtomicBoolean();
-    private static SparseArray<RequestThrottler> sUidToThrottler;
+    private static @Nullable SparseArray<RequestThrottler> sUidToThrottler;
 
     private final SharedPreferences mSharedPreferences;
     private final int mUid;
     private float mScore;
     private long mLastPrerenderRequestMs;
     private long mBannedUntilMs;
-    private String mUrl;
+    private @Nullable String mUrl;
 
     /**
      * Updates the prediction stats and returns whether prediction is allowed.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/TwaOfflineDataProvider.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/TwaOfflineDataProvider.java
index 3c50a56c..e510aca5 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/TwaOfflineDataProvider.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/TwaOfflineDataProvider.java
@@ -5,6 +5,8 @@
 package org.chromium.chrome.browser.customtabs;
 
 import org.chromium.base.UserData;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
 import org.chromium.chrome.browser.tab.Tab;
 
 import java.util.List;
@@ -13,6 +15,7 @@
  * The lifetime of a tab is complicated and not always associated with an Activity. Offline page
  * info could be required at any time, so we store it with the tab itself.
  */
+@NullMarked
 public class TwaOfflineDataProvider implements UserData {
     private static final Class<TwaOfflineDataProvider> USER_DATA_KEY = TwaOfflineDataProvider.class;
 
@@ -20,7 +23,7 @@
     private final List<String> mAdditionalTwaOrigins;
     private final String mClientPackageName;
 
-    public static TwaOfflineDataProvider from(Tab tab) {
+    public static @Nullable TwaOfflineDataProvider from(Tab tab) {
         if (tab == null) return null;
         return tab.getUserDataHost().getUserData(USER_DATA_KEY);
     }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/OmniboxTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/OmniboxTest.java
index f07152a..5c5376f 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/OmniboxTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/OmniboxTest.java
@@ -24,11 +24,11 @@
 import org.chromium.base.test.util.CallbackHelper;
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.CriteriaHelper;
-import org.chromium.base.test.util.DisabledTest;
 import org.chromium.base.test.util.EnormousTest;
 import org.chromium.base.test.util.Feature;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.omnibox.status.StatusCoordinator;
+import org.chromium.chrome.browser.omnibox.styles.OmniboxResourceProvider;
 import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteController.OnSuggestionsReceivedListener;
 import org.chromium.chrome.browser.profiles.ProfileManager;
 import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
@@ -102,7 +102,6 @@
     @Test
     @MediumTest
     @Feature({"Omnibox"})
-    @DisabledTest(message = "crbug.com/419016470 - likely addressed, but can't trigger bot from CQ")
     public void testDefaultText() {
         mActivityTestRule.startOnNtp();
 
@@ -111,13 +110,17 @@
         // Omnibox on NTP shows the hint text.
         Assert.assertNotNull(urlBar);
         Assert.assertEquals("Location bar has text.", "", urlBar.getText().toString());
-        Assert.assertEquals(
-                "Location bar has incorrect hint.",
-                mActivityTestRule
-                        .getActivity()
-                        .getResources()
-                        .getString(R.string.omnibox_empty_hint_with_dse_name, "Google"),
-                urlBar.getHint().toString());
+
+        CriteriaHelper.pollUiThread(
+                () -> {
+                    Assert.assertEquals(
+                            "Location bar has incorrect hint.",
+                            OmniboxResourceProvider.getString(
+                                    mActivityTestRule.getActivity(),
+                                    R.string.omnibox_empty_hint_with_dse_name,
+                                    "Google"),
+                            urlBar.getHint().toString());
+                });
 
         // Type something in the omnibox.
         // Note that the TextView does not provide a way to test if the hint is showing, the API
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/adding_new_tests.md b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/adding_new_tests.md
index 61e8efbc..3ee4a441 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/adding_new_tests.md
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/adding_new_tests.md
@@ -154,7 +154,7 @@
 If you are adding an AR test and none of the existing datasets work for it, you
 can create and upload a new dataset that fits your needs. Dataset creation
 requires some internal tools, see go/arcore-chrome-collect-recordings (internal
-link) or contact either bsheedy@ or bialpio@ for instructions.
+link) or contact bsheedy@ for instructions.
 
 Once you have your playback dataset (.mp4 file), simply place it in
 `//chrome/test/data/xr/ar_playback_datasets/` and upload it using
diff --git a/chrome/android/profiles/arm.newest.txt b/chrome/android/profiles/arm.newest.txt
index 4b8bdecd..ad4b5c4 100644
--- a/chrome/android/profiles/arm.newest.txt
+++ b/chrome/android/profiles/arm.newest.txt
@@ -1 +1 @@
-chromeos-chrome-arm-138.0.7190.0_rc-r1-merged.afdo.bz2
+chromeos-chrome-arm-138.0.7194.0_rc-r1-merged.afdo.bz2
diff --git a/chrome/android/profiles/newest.txt b/chrome/android/profiles/newest.txt
index 4f13291..0285343 100644
--- a/chrome/android/profiles/newest.txt
+++ b/chrome/android/profiles/newest.txt
@@ -1 +1 @@
-chromeos-chrome-amd64-138.0.7190.0_rc-r1-merged.afdo.bz2
+chromeos-chrome-amd64-138.0.7194.0_rc-r1-merged.afdo.bz2
diff --git a/chrome/app/chromium_strings.grd b/chrome/app/chromium_strings.grd
index ef22aef9..aa807fb6 100644
--- a/chrome/app/chromium_strings.grd
+++ b/chrome/app/chromium_strings.grd
@@ -1908,6 +1908,10 @@
           desc="The label of the Lens overlay entrypoint in the omnibox">
           Image Search
         </message>
+        <message name="IDS_CONTENT_LENS_OVERLAY_HOMEWORK_ENTRYPOINT_LABEL"
+          desc="The label of the Lens overlay entrypoint in the omnibox offering homework help">
+          Homework help
+        </message>
         <message name="IDS_SIDE_PANEL_LENS_OVERLAY_TOOLBAR_TOOLTIP"
           desc="The tooltip for the Lens overlay toolbar button.">
           Search with Image Search
diff --git a/chrome/app/chromium_strings_grd/IDS_CONTENT_LENS_OVERLAY_HOMEWORK_ENTRYPOINT_LABEL.png.sha1 b/chrome/app/chromium_strings_grd/IDS_CONTENT_LENS_OVERLAY_HOMEWORK_ENTRYPOINT_LABEL.png.sha1
new file mode 100644
index 0000000..63f3cd24
--- /dev/null
+++ b/chrome/app/chromium_strings_grd/IDS_CONTENT_LENS_OVERLAY_HOMEWORK_ENTRYPOINT_LABEL.png.sha1
@@ -0,0 +1 @@
+48544fec5adba6b87e82aa44a07cd66226d2d172
\ No newline at end of file
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 2cf24c4..3fc773a 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -7757,7 +7757,7 @@
       <message name="IDS_NTP_CUSTOMIZE_CHROME_CHANGE_THEME_LABEL" desc="The label for the action button for changing Chrome theme in the New Tab Page customize chrome side panel.">
         Change theme
       </message>
-      <message name="IDS_NTP_CUSTOMIZE_FOOTER_HEADER" desc="The label for the Footer title in the customization menu on the New Tab Page.">
+      <message name="IDS_NTP_CUSTOMIZE_FOOTER_HEADER" desc="The toggle for the Footer title in the customization menu on the New Tab Page." meaning="The footer which shows at the bottom of the New Tab page.">
         Footer
       </message>
       <message name="IDS_NTP_CUSTOMIZE_CHROME_MANAGED_NEW_TAB_PAGE" desc="The string that shows on the customize chrome UI when a 3rd party manages the new tab page.">
diff --git a/chrome/app/generated_resources_grd/IDS_NTP_CUSTOMIZE_FOOTER_HEADER.png.sha1 b/chrome/app/generated_resources_grd/IDS_NTP_CUSTOMIZE_FOOTER_HEADER.png.sha1
index 0f8a76e..ac6bda1 100644
--- a/chrome/app/generated_resources_grd/IDS_NTP_CUSTOMIZE_FOOTER_HEADER.png.sha1
+++ b/chrome/app/generated_resources_grd/IDS_NTP_CUSTOMIZE_FOOTER_HEADER.png.sha1
@@ -1 +1 @@
-0a2f0293525b47823871981fb69bf77a01efd6e5
\ No newline at end of file
+f50c51861fbbc0e63ebd24734a52c1e725f8a2a7
\ No newline at end of file
diff --git a/chrome/app/google_chrome_strings.grd b/chrome/app/google_chrome_strings.grd
index a0f46d3e..5eab363 100644
--- a/chrome/app/google_chrome_strings.grd
+++ b/chrome/app/google_chrome_strings.grd
@@ -1922,6 +1922,10 @@
           desc="The label of the Lens overlay entrypoint in the omnibox">
           Google Lens
         </message>
+        <message name="IDS_CONTENT_LENS_OVERLAY_HOMEWORK_ENTRYPOINT_LABEL"
+          desc="The label of the Lens overlay entrypoint in the omnibox offering homework help">
+          Homework help
+        </message>
         <message name="IDS_SIDE_PANEL_LENS_OVERLAY_TOOLBAR_TOOLTIP"
           desc="The tooltip for the Lens overlay toolbar button.">
           Search with Google Lens
diff --git a/chrome/app/google_chrome_strings_grd/IDS_CONTENT_LENS_OVERLAY_HOMEWORK_ENTRYPOINT_LABEL.png.sha1 b/chrome/app/google_chrome_strings_grd/IDS_CONTENT_LENS_OVERLAY_HOMEWORK_ENTRYPOINT_LABEL.png.sha1
new file mode 100644
index 0000000..63f3cd24
--- /dev/null
+++ b/chrome/app/google_chrome_strings_grd/IDS_CONTENT_LENS_OVERLAY_HOMEWORK_ENTRYPOINT_LABEL.png.sha1
@@ -0,0 +1 @@
+48544fec5adba6b87e82aa44a07cd66226d2d172
\ No newline at end of file
diff --git a/chrome/app/os_settings_strings.grdp b/chrome/app/os_settings_strings.grdp
index 6a1c149..b92cef7 100644
--- a/chrome/app/os_settings_strings.grdp
+++ b/chrome/app/os_settings_strings.grdp
@@ -884,9 +884,6 @@
   <message name="IDS_SETTINGS_INPUT_METHOD_OPTIONS_JAPANESE_KEYMAP_STYLE" desc="The label for Japanese keyboard settings dropdown within the settings page to set the keymap style for the Japanese keyboard. The available dropdown values are 'Custom keymap', 'ATOK', 'MS-IME', 'Kotoeri', and 'ChromeOS'. In Japanese, this string should be 'キー設定の選択'.">
     Keymap style
   </message>
-  <message name="IDS_SETTINGS_INPUT_METHOD_OPTIONS_JAPANESE_KEYMAP_STYLE_CUSTOM" desc="The label for Japanese settings page to set the keymap style to use a Custom keymap. This is 'Custom Keymap' in Japanese. In Japanese, this string should be 'カスタム'.">
-    Custom keymap
-  </message>
   <message name="IDS_SETTINGS_INPUT_METHOD_OPTIONS_JAPANESE_KEYMAP_STYLE_ATOK" translateable="false">
     ATOK
   </message>
diff --git a/chrome/app/os_settings_strings_grdp/IDS_SETTINGS_INPUT_METHOD_OPTIONS_JAPANESE_KEYMAP_STYLE_CUSTOM.png.sha1 b/chrome/app/os_settings_strings_grdp/IDS_SETTINGS_INPUT_METHOD_OPTIONS_JAPANESE_KEYMAP_STYLE_CUSTOM.png.sha1
deleted file mode 100644
index d0f6d0e9..0000000
--- a/chrome/app/os_settings_strings_grdp/IDS_SETTINGS_INPUT_METHOD_OPTIONS_JAPANESE_KEYMAP_STYLE_CUSTOM.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-254a6961462989a79fd756fe1d5290abbb44d744
\ No newline at end of file
diff --git a/chrome/app/theme/google_chrome b/chrome/app/theme/google_chrome
index d005f846..61ee818 160000
--- a/chrome/app/theme/google_chrome
+++ b/chrome/app/theme/google_chrome
@@ -1 +1 @@
-Subproject commit d005f846501cdbfcf120e05b86efaa8de3b5b60e
+Subproject commit 61ee81844485e7d5e6f0f598124a93ae62dd07db
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index f638761..1329d876 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -4423,6 +4423,7 @@
       "//chrome/browser/ui/toolbar/cast:impl",
       "//chrome/browser/ui/user_education",
       "//chrome/browser/ui/views/download",
+      "//chrome/browser/ui/views/new_tab_footer",
       "//chrome/browser/ui/views/side_panel",
       "//chrome/browser/ui/views/toolbar",
       "//chrome/browser/ui/webui:webui_util",
@@ -4589,6 +4590,7 @@
       "//chrome/browser/ui/browser_window:impl",
       "//chrome/browser/ui/exclusive_access",
       "//chrome/browser/ui/views/toolbar",
+      "//chrome/browser/ui/views/new_tab_footer",
       "//chrome/browser/ui/webui/searchbox",
       "//chrome/browser/ui/webui/top_chrome:impl",
       "//chrome/browser/ui/content_settings:impl",
@@ -6780,7 +6782,10 @@
       "enterprise/connectors/reporting/reporting_event_router_factory.cc",
       "enterprise/connectors/reporting/reporting_event_router_factory.h",
     ]
-    deps += [ "//components/enterprise/connectors/core" ]
+    deps += [
+      "//components/enterprise/common/proto:chrome_reporting_entity",
+      "//components/enterprise/connectors/core",
+    ]
   }
 
   if (enterprise_cloud_content_analysis) {
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 6358be45..4778a56 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -1368,9 +1368,8 @@
       std::size(kOmniboxStarterPackExpansionStagingUrl), nullptr}};
 
 const FeatureEntry::FeatureParam kOmniboxSearchAggregatorProdParams[] = {
-    {"name", "Agentspace (prod)"},
+    {"name", "Agentspace"},
     {"shortcut", "agentspace"},
-    {"icon_url", "https://gstatic.com/vertexaisearch/favicon.png"},
     {"search_url",
      "https://vertexaisearch.cloud.google.com/home/cid/"
      "8884f744-aae1-4fbc-8a64-b8bf7cbf270e?q={searchTerms}"},
@@ -1378,41 +1377,18 @@
      "https://discoveryengine.googleapis.com/v1alpha/projects/862721868538/"
      "locations/global/collections/default_collection/engines/"
      "teamfood-v11_1720671063545/completionConfig:completeQuery"}};
-const FeatureEntry::FeatureParam
-    kOmniboxSearchAggregatorProdWithFallbackIconParams[] = {
-        {"name", "Agentspace (prod with fallback icon)"},
-        {"shortcut", "agentspace"},
-        {"search_url",
-         "https://vertexaisearch.cloud.google.com/home/cid/"
-         "8884f744-aae1-4fbc-8a64-b8bf7cbf270e?q={searchTerms}"},
-        {"suggest_url",
-         "https://discoveryengine.googleapis.com/v1alpha/projects/862721868538/"
-         "locations/global/collections/default_collection/engines/"
-         "teamfood-v11_1720671063545/completionConfig:completeQuery"}};
 const FeatureEntry::FeatureParam kOmniboxSearchAggregatorStagingParams[] = {
     {"name", "Agentspace (staging)"},
     {"shortcut", "agentspace"},
     {"icon_url", "https://gstatic.com/vertexaisearch/favicon.png"},
     {"search_url",
      "https://vertexaisearch.cloud.google.com/home/cid/"
-     "8884f744-aae1-4fbc-8a64-b8bf7cbf270e?e=97710846%2C97750609%2C97760709%"
-     "2C97711975&q={searchTerms}"},
+     "3abd7045-7845-4f83-b204-e39fcbca3494?q={searchTerms}&mods=widget_staging_"
+     "api_mod"},
     {"suggest_url",
      "https://staging-discoveryengine.sandbox.googleapis.com/v1alpha/projects/"
      "862721868538/locations/global/collections/default_collection/engines/"
      "teamfood-v11/completionConfig:completeQuery"}};
-const FeatureEntry::FeatureParam kOmniboxSearchAggregatorAlternateParams[] = {
-    {"name", "NeuraVibe"},
-    {"shortcut", "neura"},
-    {"icon_url", "https://gstatic.com/vertexaisearch/favicon.png"},
-    {"search_url",
-     "https://vertexaisearch.cloud.google.com/home/cid/"
-     "e04a19e6-1fc2-48ba-9d0d-53c6aabcba7b?e=97844069%2C-97770083&"
-     "q={searchTerms}"},
-    {"suggest_url",
-     "https://discoveryengine.googleapis.com/v1alpha/projects/301214329925/"
-     "locations/global/collections/default_collection/engines/"
-     "neuravibeblendedsearch_1727383849310/completionConfig:completeQuery"}};
 const FeatureEntry::FeatureParam kOmniboxSearchAggregatorDemoParams[] = {
     {"name", "Neuravibe"},
     {"shortcut", "neura"},
@@ -1427,13 +1403,8 @@
 const FeatureEntry::FeatureVariation kOmniboxSearchAggregatorVariations[] = {
     {"prod", kOmniboxSearchAggregatorProdParams,
      std::size(kOmniboxSearchAggregatorProdParams), nullptr},
-    {"prod (with fallback icon)",
-     kOmniboxSearchAggregatorProdWithFallbackIconParams,
-     std::size(kOmniboxSearchAggregatorProdWithFallbackIconParams), nullptr},
     {"staging", kOmniboxSearchAggregatorStagingParams,
      std::size(kOmniboxSearchAggregatorStagingParams), nullptr},
-    {"alternate", kOmniboxSearchAggregatorAlternateParams,
-     std::size(kOmniboxSearchAggregatorAlternateParams), nullptr},
     {"demo", kOmniboxSearchAggregatorDemoParams,
      std::size(kOmniboxSearchAggregatorDemoParams), nullptr}};
 
@@ -12563,6 +12534,16 @@
 #endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) ||
         // BUILDFLAG(IS_CHROME_OS)
 
+    {"autofill-enable-multiple-request-in-virtual-card-downstream-enrollment",
+     flag_descriptions::
+         kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollmentName,
+     flag_descriptions::
+         kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollmentDescription,
+     kOsAll,
+     FEATURE_VALUE_TYPE(
+         autofill::features::
+             kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollment)},
+
     // Add new entries above this line.
 
     // NOTE: Adding a new flag requires adding a corresponding entry to enum
diff --git a/chrome/browser/actor/actor_coordinator.cc b/chrome/browser/actor/actor_coordinator.cc
index 1917679e..125c05d 100644
--- a/chrome/browser/actor/actor_coordinator.cc
+++ b/chrome/browser/actor/actor_coordinator.cc
@@ -142,7 +142,8 @@
 }
 
 void ActorCoordinator::StartTask(const BrowserAction& action,
-                                 StartTaskCallback callback) {
+                                 StartTaskCallback callback,
+                                 std::optional<tabs::TabHandle> tab_handle) {
   CHECK(base::FeatureList::IsEnabled(features::kGlicActor));
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
@@ -150,8 +151,9 @@
   // Posts to a sequence to avoid potential races with multiple attempts to
   // start a task.
   base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
-      FROM_HERE, base::BindOnce(&ActorCoordinator::TryStartNewTask,
-                                GetWeakPtr(), action, std::move(callback)));
+      FROM_HERE,
+      base::BindOnce(&ActorCoordinator::TryStartNewTask, GetWeakPtr(), action,
+                     std::move(callback), std::move(tab_handle)));
 }
 
 void ActorCoordinator::StopTask() {
@@ -227,8 +229,10 @@
           web_contents.GetPrimaryMainFrame()->GetLastCommittedOrigin()));
 }
 
-void ActorCoordinator::TryStartNewTask(const BrowserAction& action,
-                                       StartTaskCallback callback) {
+void ActorCoordinator::TryStartNewTask(
+    const BrowserAction& action,
+    StartTaskCallback callback,
+    std::optional<tabs::TabHandle> tab_handle) {
   // Check for a failed attempt to initialize a new task, where there is a new
   // tab observer but it's navigation handle is no longer valid. The expectation
   // is that new tab observer will be notified regardless if the navigation
@@ -256,7 +260,23 @@
 
   initializing_new_task_ = true;
 
-  // Force the task to be performed in a new tab.
+  // If a tab handle was provided, try to get the tab interface
+  if (tab_handle) {
+    if (auto* tab_interface = tab_handle->Get()) {
+      // Create a new task with the existing tab
+      task_state_ = std::make_unique<Task>(*tab_interface);
+      initializing_new_task_ = false;
+      PostTaskForStartCallback(std::move(callback), task_state_->tab);
+      return;
+    }
+    // If we couldn't get the tab interface, error out
+    VLOG(1) << "Could not get tab interface for handle";
+    PostTaskForStartInitializationFailed(std::move(callback));
+    return;
+  }
+
+  // If no tab handle was provided, create a new tab
+  VLOG(1) << "No tab handle provided, creating new tab";
   CreateNewTab(std::move(callback));
 }
 
diff --git a/chrome/browser/actor/actor_coordinator.h b/chrome/browser/actor/actor_coordinator.h
index 873d90b7..18e9e1f 100644
--- a/chrome/browser/actor/actor_coordinator.h
+++ b/chrome/browser/actor/actor_coordinator.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_ACTOR_ACTOR_COORDINATOR_H_
 
 #include <memory>
+#include <optional>
 
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
@@ -14,6 +15,7 @@
 #include "base/types/id_type.h"
 #include "chrome/browser/actor/tools/tool_controller.h"
 #include "chrome/common/actor.mojom-forward.h"
+#include "components/tabs/public/tab_interface.h"
 #include "content/public/browser/web_contents_observer.h"
 
 class Profile;
@@ -60,17 +62,18 @@
   static void RegisterWithProfile(Profile* profile);
 
   // Starts a new task.
-  // Currently, requires a navigate action to start, and always creates a new
-  // tab.
-  // If starting the task succeeds, provides the newly-created tab in the
-  // callback, otherwise null.
-  // Starting the task may fail for any of:
+  // Currently, requires a navigate action to start.
+  // If starting the task succeeds, provides the tab in the callback, otherwise
+  // null. Starting the task may fail for any of:
   //   - The `action` is not navigate.
   //   - There is already a task started, or attempting to create a new tab to
   //   start a task.
-  //   - Unable to create a new tab.
+  //   - If a tab handle is provided, the tab must exist and be valid. The task
+  //   will fail if the tab cannot be found or is invalid.
+  //   - If no tab handle is provided, a new tab will be created.
   void StartTask(const optimization_guide::proto::BrowserAction& action,
-                 StartTaskCallback callback);
+                 StartTaskCallback callback,
+                 std::optional<tabs::TabHandle> tab_handle);
 
   // Stops the currently running task, if one is active. Callbacks for
   // in-progress actions are invoked.
@@ -99,7 +102,8 @@
   // Starts a new task, after validating there isn't already a task being
   // initialized or in progress.
   void TryStartNewTask(const optimization_guide::proto::BrowserAction& action,
-                       StartTaskCallback callback);
+                       StartTaskCallback callback,
+                       std::optional<tabs::TabHandle> tab_handle);
 
   // Invokes the StartTask callback when initializing a new task failed (e.g.
   // error creating a new tab). Must be called to reset from the "initializing"
diff --git a/chrome/browser/ai/ai_data_keyed_service.cc b/chrome/browser/ai/ai_data_keyed_service.cc
index 753f649f..41a20684 100644
--- a/chrome/browser/ai/ai_data_keyed_service.cc
+++ b/chrome/browser/ai/ai_data_keyed_service.cc
@@ -65,6 +65,7 @@
 #include "ui/gfx/geometry/rect.h"
 
 #if BUILDFLAG(ENABLE_GLIC)
+#include "chrome/browser/actor/actor_coordinator.h"
 #include "chrome/browser/glic/host/context/glic_page_context_fetcher.h"
 #include "chrome/browser/glic/host/context/glic_tab_data.h"
 #include "chrome/browser/glic/host/glic.mojom.h"
@@ -892,7 +893,9 @@
       dummy_navigate_action,
       base::BindOnce(&AiDataKeyedService::OnTaskCreated,
                      weak_factory_.GetWeakPtr(), std::move(callback), task_id_,
-                     tab_id_));
+                     tab_id_),
+      task.has_tab_id() ? std::make_optional(tabs::TabHandle(task.tab_id()))
+                        : std::nullopt);
 #endif  // BUILDFLAG(ENABLE_GLIC)
 }
 
diff --git a/chrome/browser/ai/ai_on_device_browsertest.cc b/chrome/browser/ai/ai_on_device_browsertest.cc
index b330827..daf0d85c 100644
--- a/chrome/browser/ai/ai_on_device_browsertest.cc
+++ b/chrome/browser/ai/ai_on_device_browsertest.cc
@@ -4,6 +4,7 @@
 
 #include <string>
 
+#include "base/strings/stringprintf.h"
 #include "base/test/scoped_feature_list.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
diff --git a/chrome/browser/apps/guest_view/web_view_browsertest.cc b/chrome/browser/apps/guest_view/web_view_browsertest.cc
index 051203e..fa5ee4c 100644
--- a/chrome/browser/apps/guest_view/web_view_browsertest.cc
+++ b/chrome/browser/apps/guest_view/web_view_browsertest.cc
@@ -23,6 +23,7 @@
 #include "base/run_loop.h"
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/bind.h"
@@ -7205,11 +7206,16 @@
 
 // Helper class to turn off strict site isolation while still using site
 // isolation paths for <webview>.  This forces <webview> to use the default
-// SiteInstance paths. The helper also defines one isolated origin at
-// isolated.com, which takes precedence over the command-line switch to disable
-// site isolation and can be used to test a combination of SiteInstances that
-// require and don't require dedicated processes.
-class WebViewWithDefaultSiteInstanceTest : public SitePerProcessWebViewTest {
+// SiteInstance or default SiteInstanceGroup paths. The helper also defines one
+// isolated origin at isolated.com, which takes precedence over the command-line
+// switch to disable site isolation and can be used to test a combination of
+// SiteInstances that require and don't require dedicated processes.
+// This test is parameterized to run in MPArch or InnerWebContents mode, and
+// DefaultSiteInstance or DefaultSiteInstanceGroup mode, totaling 4
+// configurations.
+class WebViewWithDefaultSiteInstanceTest
+    : public WebViewTestBase,
+      public testing::WithParamInterface<testing::tuple<bool, bool>> {
  public:
   WebViewWithDefaultSiteInstanceTest() = default;
   ~WebViewWithDefaultSiteInstanceTest() override = default;
@@ -7222,28 +7228,42 @@
     command_line->AppendSwitch(switches::kDisableSiteIsolation);
     command_line->AppendSwitchASCII(switches::kIsolateOrigins,
                                     "http://isolated.com");
-    SitePerProcessWebViewTest::SetUpCommandLine(command_line);
+    feature_list_.InitWithFeatureStates(
+        {{features::kGuestViewMPArch, testing::get<0>(GetParam())},
+         {features::kDefaultSiteInstanceGroups, testing::get<1>(GetParam())}});
+
+    WebViewTestBase::SetUpCommandLine(command_line);
   }
 
   content::test::FencedFrameTestHelper& fenced_frame_test_helper() {
     return fenced_frame_test_helper_;
   }
 
+  static std::string DescribeParams(
+      const testing::TestParamInfo<ParamType>& info) {
+    const auto [mparch, site_instance_group] = info.param;
+    return base::StringPrintf("%s_%s", mparch ? "MPArch" : "InnerWebContents",
+                              site_instance_group ? "DefaultSiteInstanceGroups"
+                                                  : "DefaultSiteInstances");
+  }
+
  private:
   content::test::FencedFrameTestHelper fenced_frame_test_helper_;
+  base::test::ScopedFeatureList feature_list_;
 };
 
 INSTANTIATE_TEST_SUITE_P(/* no prefix */,
                          WebViewWithDefaultSiteInstanceTest,
-                         testing::Bool(),
+                         testing::Combine(testing::Bool(), testing::Bool()),
                          WebViewWithDefaultSiteInstanceTest::DescribeParams);
 
-// Check that when strict site isolation is turned off (via a command-line flag
-// or from chrome://flags), the <webview> site isolation paths still work. In
-// particular, <webview> navigations should use a default SiteInstance which
-// should still be considered a guest SiteInstance in the guest's
-// StoragePartition.  Cross-site navigations in the guest should stay in the
-// same SiteInstance, and the guest process shouldn't be locked.
+// Check that when strict site isolation is turned off (via a command-line
+// flag or from chrome://flags), the <webview> site isolation paths still
+// work. In particular, <webview> navigations should use a default
+// SiteInstance or default SiteInstanceGroup which should still be considered
+// a guest SiteInstance in the guest's StoragePartition. Cross-site
+// navigations in the guest should stay in the same SiteInstance or
+// SiteInstanceGroup, and the guest process shouldn't be locked.
 IN_PROC_BROWSER_TEST_P(WebViewWithDefaultSiteInstanceTest, SimpleNavigations) {
   ASSERT_TRUE(StartEmbeddedTestServer());
 
@@ -7275,27 +7295,40 @@
     load_observer.Wait();
   }
 
-  // Expect that we stayed in the same (default) SiteInstance.
+  // Expect that we stayed in the same (default) SiteInstance or
+  // SiteInstanceGroup.
   main_frame = GetGuestRenderFrameHost();
   ASSERT_TRUE(main_frame);
-  if (!main_frame->ShouldChangeRenderFrameHostOnSameSiteNavigation()) {
-    // The RenderFrameHost will stay the same when we don't change
-    // RenderFrameHosts on same-SiteInstance navigations.
-    EXPECT_EQ(main_frame->GetGlobalId(), original_id);
+  if (base::FeatureList::IsEnabled(features::kDefaultSiteInstanceGroups)) {
+    EXPECT_NE(starting_instance, main_frame->GetSiteInstance());
+    EXPECT_EQ(starting_instance->GetSiteInstanceGroupId(),
+              main_frame->GetSiteInstance()->GetSiteInstanceGroupId());
+  } else {
+    EXPECT_EQ(starting_instance, main_frame->GetSiteInstance());
+    if (!main_frame->ShouldChangeRenderFrameHostOnSameSiteNavigation()) {
+      // The RenderFrameHost will stay the same when we don't change
+      // RenderFrameHosts on same-SiteInstance navigations.
+      EXPECT_EQ(main_frame->GetGlobalId(), original_id);
+    }
   }
-  EXPECT_EQ(starting_instance, main_frame->GetSiteInstance());
   EXPECT_FALSE(main_frame->GetSiteInstance()->RequiresDedicatedProcess());
   EXPECT_FALSE(main_frame->GetProcess()->IsProcessLockedToSiteForTesting());
 
   // Navigate <webview> subframe cross-site.  Check that it stays in the same
-  // SiteInstance and process.
+  // process, and SiteInstance/Group.
   const GURL frame_url =
       embedded_test_server()->GetURL("b.test", "/title1.html");
   content::RenderFrameHost* subframe = content::ChildFrameAt(main_frame, 0);
   ASSERT_TRUE(subframe);
   EXPECT_TRUE(NavigateToURLFromRenderer(subframe, frame_url));
   subframe = content::ChildFrameAt(main_frame, 0);
-  EXPECT_EQ(main_frame->GetSiteInstance(), subframe->GetSiteInstance());
+  if (base::FeatureList::IsEnabled(features::kDefaultSiteInstanceGroups)) {
+    EXPECT_NE(main_frame->GetSiteInstance(), subframe->GetSiteInstance());
+    EXPECT_EQ(main_frame->GetSiteInstance()->GetSiteInstanceGroupId(),
+              subframe->GetSiteInstance()->GetSiteInstanceGroupId());
+  } else {
+    EXPECT_EQ(main_frame->GetSiteInstance(), subframe->GetSiteInstance());
+  }
   EXPECT_EQ(main_frame->GetProcess(), subframe->GetProcess());
   EXPECT_TRUE(subframe->GetSiteInstance()->IsGuest());
   EXPECT_FALSE(subframe->GetSiteInstance()->RequiresDedicatedProcess());
@@ -7303,10 +7336,11 @@
 }
 
 // Similar to the test above, but also exercises navigations to an isolated
-// origin, which takes precedence over switches::kDisableSiteIsolation. In this
-// setup, navigations to the isolated origin should use a normal SiteInstance
-// that requires a dedicated process, while all other navigations should use
-// the default SiteInstance and an unlocked process.
+// origin, which takes precedence over switches::kDisableSiteIsolation. In
+// this setup, navigations to the isolated origin should use a normal
+// SiteInstance that requires a dedicated process, while all other navigations
+// should use the default SiteInstance or default SiteInstanceGroup and an
+// unlocked process.
 IN_PROC_BROWSER_TEST_P(WebViewWithDefaultSiteInstanceTest, IsolatedOrigin) {
   ASSERT_TRUE(StartEmbeddedTestServer());
 
diff --git a/chrome/browser/ash/printing/oauth2/http_exchange.cc b/chrome/browser/ash/printing/oauth2/http_exchange.cc
index 4868847..9942233 100644
--- a/chrome/browser/ash/printing/oauth2/http_exchange.cc
+++ b/chrome/browser/ash/printing/oauth2/http_exchange.cc
@@ -14,6 +14,7 @@
 #include "base/strings/escape.h"
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
 #include "base/values.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "services/network/public/cpp/resource_request.h"
diff --git a/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc b/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc
index 022cc878..8f1686b5e 100644
--- a/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc
+++ b/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc
@@ -559,19 +559,32 @@
 #if !BUILDFLAG(IS_ANDROID)
   if (auto* lens_search_controller =
           GetLensSearchController(GetWebContents(web_contents_getter_))) {
-    // Only allow Lens entrypoints if the Lens overlay is enabled and Lens is
-    // not currently active. Guaranteed to exist if lens_search_controller is
-    // not null.
+    // Guaranteed to exist if lens_search_controller is  not null.
     return lens_search_controller->GetTabInterface()
         ->GetBrowserWindowInterface()
         ->GetFeatures()
         .lens_overlay_entry_point_controller()
-        ->AreVisible();
+        ->IsEnabled();
   }
 #endif
   return false;
 }
 
+bool ChromeAutocompleteProviderClient::AreLensEntrypointsVisible() const {
+  #if !BUILDFLAG(IS_ANDROID)
+    if (auto* lens_search_controller =
+            GetLensSearchController(GetWebContents(web_contents_getter_))) {
+      // Guaranteed to exist if lens_search_controller is  not null.
+      return lens_search_controller->GetTabInterface()
+          ->GetBrowserWindowInterface()
+          ->GetFeatures()
+          .lens_overlay_entry_point_controller()
+          ->AreVisible();
+    }
+  #endif
+    return false;
+  }
+
 base::CallbackListSubscription
 ChromeAutocompleteProviderClient::GetLensSuggestInputsWhenReady(
     LensOverlaySuggestInputsCallback callback) const {
@@ -648,12 +661,12 @@
   if (auto* lens_search_controller =
           GetLensSearchController(GetWebContents(web_contents_getter_))) {
     if (show) {
+      lens_search_controller->OpenLensOverlay(
+          lens::LensOverlayInvocationSource::kOmniboxPageAction);
+    } else {
       // TODO(crbug.com/402497756): For prototyping, reusing the existing
       // omnibox entry point. However, for production, create a new invocation
       // source for this new entry point.
-      lens_search_controller->OpenLensOverlay(
-          lens::LensOverlayInvocationSource::kOmnibox);
-    } else {
       lens_search_controller->StartContextualization(
           lens::LensOverlayInvocationSource::kOmnibox);
     }
@@ -669,6 +682,7 @@
   if (auto* lens_search_controller =
           GetLensSearchController(GetWebContents(web_contents_getter_))) {
     lens_search_controller->IssueContextualSearchRequest(
+        lens::LensOverlayInvocationSource::kOmniboxContextualSuggestion,
         destination_url, match_type, is_zero_prefix_suggestion);
   }
 #endif  // !BUILDFLAG(IS_ANDROID)
diff --git a/chrome/browser/autocomplete/chrome_autocomplete_provider_client.h b/chrome/browser/autocomplete/chrome_autocomplete_provider_client.h
index cfb587c0..b17c620b 100644
--- a/chrome/browser/autocomplete/chrome_autocomplete_provider_client.h
+++ b/chrome/browser/autocomplete/chrome_autocomplete_provider_client.h
@@ -123,6 +123,7 @@
   bool IsHistoryEmbeddingsEnabled() const override;
   bool IsHistoryEmbeddingsSettingVisible() const override;
   bool IsLensEnabled() const override;
+  bool AreLensEntrypointsVisible() const override;
   base::CallbackListSubscription GetLensSuggestInputsWhenReady(
       LensOverlaySuggestInputsCallback callback) const override;
   base::WeakPtr<AutocompleteProviderClient> GetWeakPtr() override;
diff --git a/chrome/browser/autocomplete/chrome_autocomplete_provider_client_browsertest.cc b/chrome/browser/autocomplete/chrome_autocomplete_provider_client_browsertest.cc
index 83c79dd..791acff 100644
--- a/chrome/browser/autocomplete/chrome_autocomplete_provider_client_browsertest.cc
+++ b/chrome/browser/autocomplete/chrome_autocomplete_provider_client_browsertest.cc
@@ -123,7 +123,7 @@
       .Times(1)
       .WillOnce(testing::Invoke(
           [](lens::LensOverlayInvocationSource invocation_source) {
-            EXPECT_EQ(lens::LensOverlayInvocationSource::kOmnibox,
+            EXPECT_EQ(lens::LensOverlayInvocationSource::kOmniboxPageAction,
                       invocation_source);
           }));
 
diff --git a/chrome/browser/autofill/autofill_across_iframes_browsertest.cc b/chrome/browser/autofill/autofill_across_iframes_browsertest.cc
index 1cad3e2..91242c6 100644
--- a/chrome/browser/autofill/autofill_across_iframes_browsertest.cc
+++ b/chrome/browser/autofill/autofill_across_iframes_browsertest.cc
@@ -13,6 +13,7 @@
 #include <vector>
 
 #include "base/command_line.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index df1b55e5..a3466de 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -1980,14 +1980,15 @@
   return true;
 }
 
-std::optional<
-    content::ContentBrowserClient::SpareProcessRefusedByEmbedderReason>
-ChromeContentBrowserClient::ShouldUseSpareRenderProcessHost(
+bool ChromeContentBrowserClient::ShouldUseSpareRenderProcessHost(
     content::BrowserContext* browser_context,
-    const GURL& site_url) {
+    const GURL& site_url,
+    std::optional<SpareProcessRefusedByEmbedderReason>& refused_reason) {
+  refused_reason = std::nullopt;
   Profile* profile = Profile::FromBrowserContext(browser_context);
   if (!profile) {
-    return SpareProcessRefusedByEmbedderReason::NoProfile;
+    refused_reason = SpareProcessRefusedByEmbedderReason::NoProfile;
+    return false;
   }
 
 #if !BUILDFLAG(IS_ANDROID)
@@ -1999,17 +2000,20 @@
     // The NTP page chrome://new-tab-page and chrome://new-tab-page-third-party
     // are using WebUI and will not use instant renderer.
     // The only usecase is chrome-search:// URLs.
-    return SpareProcessRefusedByEmbedderReason::InstantRendererForNewTabPage;
+    refused_reason =
+        SpareProcessRefusedByEmbedderReason::InstantRendererForNewTabPage;
+    return false;
   }
 #endif
 
 #if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
   if (!ChromeContentBrowserClientExtensionsPart::
           ShouldUseSpareRenderProcessHost(profile, site_url)) {
-    return SpareProcessRefusedByEmbedderReason::ExtensionProcess;
+    refused_reason = SpareProcessRefusedByEmbedderReason::ExtensionProcess;
+    return false;
   }
 #endif
-  return std::nullopt;
+  return true;
 }
 
 bool ChromeContentBrowserClient::DoesSiteRequireDedicatedProcess(
diff --git a/chrome/browser/chrome_content_browser_client.h b/chrome/browser/chrome_content_browser_client.h
index 93129eb1..5e38776 100644
--- a/chrome/browser/chrome_content_browser_client.h
+++ b/chrome/browser/chrome_content_browser_client.h
@@ -208,9 +208,11 @@
       const GURL& site_instance_original_url) override;
   bool ShouldAllowProcessPerSiteForMultipleMainFrames(
       content::BrowserContext* context) override;
-  std::optional<SpareProcessRefusedByEmbedderReason>
-  ShouldUseSpareRenderProcessHost(content::BrowserContext* browser_context,
-                                  const GURL& site_url) override;
+  bool ShouldUseSpareRenderProcessHost(
+      content::BrowserContext* browser_context,
+      const GURL& site_url,
+      std::optional<SpareProcessRefusedByEmbedderReason>& refused_reason)
+      override;
   bool DoesSiteRequireDedicatedProcess(content::BrowserContext* browser_context,
                                        const GURL& effective_site_url) override;
   bool ShouldAllowCrossProcessSandboxedFrameForPrecursor(
diff --git a/chrome/browser/chrome_content_browser_client_unittest.cc b/chrome/browser/chrome_content_browser_client_unittest.cc
index f57f609..96855bb1 100644
--- a/chrome/browser/chrome_content_browser_client_unittest.cc
+++ b/chrome/browser/chrome_content_browser_client_unittest.cc
@@ -1639,27 +1639,31 @@
       content::ContentBrowserClient::SpareProcessRefusedByEmbedderReason;
   ChromeContentBrowserClient browser_client;
 
+  std::optional<SpareProcessRefusedByEmbedderReason> refused_reason;
   // Standard web URL
-  EXPECT_FALSE(browser_client.ShouldUseSpareRenderProcessHost(
-      &profile_, GURL("https://www.example.com")));
+  EXPECT_TRUE(browser_client.ShouldUseSpareRenderProcessHost(
+      &profile_, GURL("https://www.example.com"), refused_reason));
+  EXPECT_FALSE(refused_reason.has_value());
 
   // No profile
-  EXPECT_EQ(SpareProcessRefusedByEmbedderReason::NoProfile,
-            browser_client.ShouldUseSpareRenderProcessHost(
-                nullptr, GURL("https://www.example.com")));
+  EXPECT_FALSE(browser_client.ShouldUseSpareRenderProcessHost(
+      nullptr, GURL("https://www.example.com"), refused_reason));
+  EXPECT_EQ(SpareProcessRefusedByEmbedderReason::NoProfile, refused_reason);
 
 #if !BUILDFLAG(IS_ANDROID)
   // Chrome-search URL
+  EXPECT_FALSE(browser_client.ShouldUseSpareRenderProcessHost(
+      &profile_, GURL("chrome-search://test"), refused_reason));
   EXPECT_EQ(SpareProcessRefusedByEmbedderReason::InstantRendererForNewTabPage,
-            browser_client.ShouldUseSpareRenderProcessHost(
-                &profile_, GURL("chrome-search://test")));
+            refused_reason);
 #endif
 
 #if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
   // Extension URL
+  EXPECT_FALSE(browser_client.ShouldUseSpareRenderProcessHost(
+      &profile_, GURL("chrome-extension://test-extension/"), refused_reason));
   EXPECT_EQ(SpareProcessRefusedByEmbedderReason::ExtensionProcess,
-            browser_client.ShouldUseSpareRenderProcessHost(
-                &profile_, GURL("chrome-extension://test-extension/")));
+            refused_reason);
 #endif
 }
 
diff --git a/chrome/browser/enterprise/connectors/reporting/crash_reporting_context.cc b/chrome/browser/enterprise/connectors/reporting/crash_reporting_context.cc
index 21a11a7e..368fd5d 100644
--- a/chrome/browser/enterprise/connectors/reporting/crash_reporting_context.cc
+++ b/chrome/browser/enterprise/connectors/reporting/crash_reporting_context.cc
@@ -13,8 +13,12 @@
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/common/channel_info.h"
 #include "components/crash/core/app/crashpad.h"
+#include "components/enterprise/common/proto/synced_from_google3/chrome_reporting_entity.pb.h"
 #include "components/enterprise/connectors/core/connectors_prefs.h"
+#include "components/enterprise/connectors/core/realtime_reporting_client_base.h"
 #include "components/enterprise/connectors/core/reporting_service_settings.h"
+#include "components/enterprise/connectors/core/reporting_utils.h"
+#include "components/policy/core/common/cloud/realtime_reporting_job_configuration.h"
 #include "components/prefs/pref_service.h"
 #include "components/version_info/version_info.h"
 
@@ -40,6 +44,37 @@
       ->chrome_browser_cloud_management_controller();
 }
 
+base::Value::Dict GetBrowserCrashEventDeprecated(const std::string& channel,
+                                                 const std::string& version,
+                                                 const std::string& report_id,
+                                                 const std::string& platform) {
+  base::Value::Dict event;
+  event.Set(kKeyChannel, channel);
+  event.Set(kKeyVersion, version);
+  event.Set(kKeyReportId, report_id);
+  event.Set(kKeyPlatform, platform);
+
+  return event;
+}
+
+::chrome::cros::reporting::proto::Event GetBrowserCrashEvent(
+    const std::string& channel,
+    const std::string& version,
+    const std::string& report_id,
+    const std::string& platform,
+    time_t report_creation_time) {
+  ::chrome::cros::reporting::proto::Event event;
+  auto* browser_crash_event = event.mutable_browser_crash_event();
+  browser_crash_event->set_channel(channel);
+  browser_crash_event->set_version(version);
+  browser_crash_event->set_report_id(report_id);
+  browser_crash_event->set_platform(platform);
+  *event.mutable_time() =
+      ToProtoTimestamp(base::Time::FromTimeT(report_creation_time));
+
+  return event;
+}
+
 // Copy new reports (i.e. reports that have not been sent to the
 // reporting server) from `reports_to_be_copied` to `reports`
 // based on the `latest_creation_time`.
@@ -169,14 +204,19 @@
   int64_t latest_creation_time = -1;
 
   for (const auto& report : reports) {
-    base::Value::Dict event;
-    event.Set(kKeyChannel, channel);
-    event.Set(kKeyVersion, version);
-    event.Set(kKeyReportId, report.id);
-    event.Set(kKeyPlatform, platform);
-    reporting_client->ReportPastEvent(
-        kBrowserCrashEvent, settings.value(), std::move(event),
-        base::Time::FromTimeT(report.creation_time));
+    if (base::FeatureList::IsEnabled(
+            policy::kUploadRealtimeReportingEventsUsingProto)) {
+      reporting_client->ReportEvent(
+          GetBrowserCrashEvent(channel, version, report.id, platform,
+                               report.creation_time),
+          settings.value());
+    } else {
+      reporting_client->ReportPastEvent(
+          kBrowserCrashEvent, settings.value(),
+          GetBrowserCrashEventDeprecated(channel, version, report.id, platform),
+          base::Time::FromTimeT(report.creation_time));
+    }
+
     if (report.creation_time > latest_creation_time) {
       latest_creation_time = report.creation_time;
     }
diff --git a/chrome/browser/enterprise/connectors/reporting/crash_reporting_context_unittest.cc b/chrome/browser/enterprise/connectors/reporting/crash_reporting_context_unittest.cc
index 8865cfb..53f6d42 100644
--- a/chrome/browser/enterprise/connectors/reporting/crash_reporting_context_unittest.cc
+++ b/chrome/browser/enterprise/connectors/reporting/crash_reporting_context_unittest.cc
@@ -5,6 +5,8 @@
 
 #include "base/command_line.h"
 #include "base/files/scoped_temp_dir.h"
+#include "base/test/protobuf_matchers.h"
+#include "base/test/scoped_feature_list.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/enterprise/connectors/reporting/realtime_reporting_client.h"
 #include "chrome/browser/enterprise/connectors/reporting/realtime_reporting_client_factory.h"
@@ -16,6 +18,8 @@
 #include "components/enterprise/connectors/core/connectors_prefs.h"
 #include "components/enterprise/connectors/core/reporting_service_settings.h"
 #include "components/enterprise/connectors/core/reporting_test_utils.h"
+#include "components/enterprise/connectors/core/reporting_utils.h"
+#include "components/policy/core/common/cloud/realtime_reporting_job_configuration.h"
 #include "components/version_info/version_info.h"
 #include "content/public/test/browser_task_environment.h"
 #include "testing/gmock/include/gmock/gmock.h"
@@ -26,8 +30,10 @@
 #include "chrome/test/base/scoped_channel_override.h"
 #endif
 
+using base::test::EqualsProto;
 using ::testing::_;
 using ::testing::ByMove;
+using ::testing::ByRef;
 using ::testing::Eq;
 using ::testing::Return;
 
@@ -60,18 +66,31 @@
 
 }  // namespace
 
-class CrashReportingContextTest : public testing::Test {
+class CrashReportingContextTest : public testing::TestWithParam<bool> {
  public:
   CrashReportingContextTest()
       : profile_manager_(TestingBrowserProcess::GetGlobal()) {}
 
-  void SetUp() override { EXPECT_TRUE(profile_manager_.SetUp()); }
+  void SetUp() override {
+    EXPECT_TRUE(profile_manager_.SetUp());
+
+    if (use_proto_format()) {
+      feature_list_.InitAndEnableFeature(
+          policy::kUploadRealtimeReportingEventsUsingProto);
+    } else {
+      feature_list_.InitAndDisableFeature(
+          policy::kUploadRealtimeReportingEventsUsingProto);
+    }
+  }
+
+  bool use_proto_format() { return GetParam(); }
 
   content::BrowserTaskEnvironment task_environment_;
   TestingProfileManager profile_manager_;
+  base::test::ScopedFeatureList feature_list_;
 };
 
-TEST_F(CrashReportingContextTest, GetNewReportsFromDB) {
+TEST_P(CrashReportingContextTest, GetNewReportsFromDB) {
   base::ScopedTempDir database_dir;
   ASSERT_TRUE(database_dir.CreateUniqueTempDir());
   std::unique_ptr<crashpad::CrashReportDatabase> database =
@@ -86,7 +105,7 @@
   EXPECT_EQ(reports.size(), 1u);
 }
 
-TEST_F(CrashReportingContextTest, GetAndSetLatestCrashReportingTime) {
+TEST_P(CrashReportingContextTest, GetAndSetLatestCrashReportingTime) {
   time_t timestamp = base::Time::Now().ToTimeT();
 
   SetLatestCrashReportTime(g_browser_process->local_state(), timestamp);
@@ -94,7 +113,7 @@
             GetLatestCrashReportTime(g_browser_process->local_state()));
 }
 
-TEST_F(CrashReportingContextTest, UploadToReportingServer) {
+TEST_P(CrashReportingContextTest, UploadToReportingServer) {
   EXPECT_EQ(static_cast<long>(0u),
             GetLatestCrashReportTime(g_browser_process->local_state()));
 
@@ -102,6 +121,7 @@
   std::vector<crashpad::CrashReportDatabase::Report> reports;
   crashpad::CrashReportDatabase::Report report;
   report.creation_time = timestamp;
+  report.id = "123";
   reports.push_back(report);
 
   TestingProfile* profile =
@@ -121,16 +141,45 @@
       static_cast<test::MockRealtimeReportingClient*>(
           RealtimeReportingClientFactory::GetForProfile(profile));
 
-  EXPECT_CALL(*reporting_client,
-              ReportPastEvent(kBrowserCrashEvent, _, _,
-                              base::Time::FromTimeT(timestamp)))
-      .Times(1);
+  ::chrome::cros::reporting::proto::Event expected_event_proto;
+  base::Value::Dict expected_event;
+
+  if (use_proto_format()) {
+    auto* browser_crash_event =
+        expected_event_proto.mutable_browser_crash_event();
+    browser_crash_event->set_channel(
+        version_info::GetChannelString(chrome::GetChannel()));
+    browser_crash_event->set_version(version_info::GetVersionNumber());
+    browser_crash_event->set_report_id("123");
+    browser_crash_event->set_platform(version_info::GetOSType());
+    *expected_event_proto.mutable_time() =
+        ToProtoTimestamp(base::Time::FromTimeT(timestamp));
+
+    EXPECT_CALL(*reporting_client,
+                ReportEvent(EqualsProto(expected_event_proto), _))
+        .Times(1);
+  } else {
+    expected_event.Set("channel",
+                       version_info::GetChannelString(chrome::GetChannel()));
+    expected_event.Set("version", version_info::GetVersionNumber());
+    expected_event.Set("reportId", "123");
+    expected_event.Set("platform", version_info::GetOSType());
+
+    EXPECT_CALL(
+        *reporting_client,
+        ReportPastEvent(kBrowserCrashEvent, _, Eq(ByRef(expected_event)),
+                        base::Time::FromTimeT(timestamp)))
+        .Times(1);
+  }
+
   UploadToReportingServer(reporting_client->AsWeakPtrImpl(),
                           g_browser_process->local_state(), reports);
   EXPECT_EQ(timestamp,
             GetLatestCrashReportTime(g_browser_process->local_state()));
 }
 
+INSTANTIATE_TEST_SUITE_P(, CrashReportingContextTest, ::testing::Bool());
+
 #if BUILDFLAG(GOOGLE_CHROME_BRANDING) && !BUILDFLAG(IS_ANDROID)
 
 struct PollingIntervalParams {
@@ -151,9 +200,9 @@
 
 TEST_P(CrashpadPollingIntervalTest, GetCrashpadPollingInterval) {
   chrome::ScopedChannelOverride scoped_channel(GetParam().channel);
-  base::CommandLine* commandLine = base::CommandLine::ForCurrentProcess();
-  commandLine->AppendSwitchASCII(kCrashpadPollingIntervalFlag,
-                                 GetParam().cmd_flag);
+  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
+  command_line->AppendSwitchASCII(kCrashpadPollingIntervalFlag,
+                                  GetParam().cmd_flag);
   EXPECT_EQ(GetCrashpadPollingInterval(),
             base::Seconds(GetParam().expected_interval));
 }
diff --git a/chrome/browser/extensions/ai_language_model_browsertest.cc b/chrome/browser/extensions/ai_language_model_browsertest.cc
index 8af2296..2624702 100644
--- a/chrome/browser/extensions/ai_language_model_browsertest.cc
+++ b/chrome/browser/extensions/ai_language_model_browsertest.cc
@@ -11,7 +11,6 @@
 #include "base/test/scoped_feature_list.h"
 #include "base/version_info/channel.h"
 #include "chrome/browser/extensions/extension_browsertest.h"
-#include "components/embedder_support/switches.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/test/browser_test.h"
 #include "extensions/test/result_catcher.h"
@@ -19,30 +18,10 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/common/features_generated.h"
 
-// TODO(crbug.com/350642260): the prompt API for extension OT is not affecting
-// ChromeOS. We have skipped the logic for ChromeOS so the test will be skipped
-// as well.
 namespace extensions {
 
 namespace {
 
-// This is the public key of tools/origin_trials/eftest.key, used to validate
-// origin trial tokens generated by tools/origin_trials/generate_token.py.
-constexpr char kOriginTrialPublicKeyForTesting[] =
-    "dRCs+TocuKkocNKa0AtZ4awrt9XKH2SQCI6o4FY6BNA=";
-
-// The extension origin trial token (expired on 2032-11-26) was generated by
-// ```
-// tools/origin_trials/generate_token.py
-// chrome-extension://jnapclmfkaejhjkddbmiafekigmcbmma AIPromptAPIForExtension
-// --expire-days 3000
-// ```
-constexpr char kLanguageModelOriginTrialTokensField[] =
-    "\"trial_tokens\":[\"A5nDxhrF7Qe4GiLouR1mgL5XKSk4wXA0B/RV2VyQcZj2IkLALdG/"
-    "FHrucKbG1TKD8QidNfqBdC07wP8KJaF6EQYAAAB9eyJvcmlnaW4iOiAiY2hyb21lLWV4dGVuc2"
-    "lvbjovL2puYXBjbG1ma2Flamhqa2RkYm1pYWZla2lnbWNibW1hIiwgImZlYXR1cmUiOiAiQUlQ"
-    "cm9tcHRBUElGb3JFeHRlbnNpb24iLCAiZXhwaXJ5IjogMTk4NTA2MjMwM30=\"],";
-
 // The `key` field stores the public key for the extension with id
 // "jnapclmfkaejhjkddbmiafekigmcbmma".
 static constexpr char kManifestTemplate[] =
@@ -51,7 +30,6 @@
       "name": "AI language model test",
       "version": "0.1",
       "manifest_version": 3,
-      %s
       "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3H6Jc0On6l0H3DJ6bx4aOW3+srCfjSdr+3ukwIEZrL6jDy500XweIwOp9PItpM9sijwu8v1rdyoBPubm/ottp/oz42aKp+2xIxcMTa6/cA2BL2kOWxwv+WP9d01IOFbFpWmQBDQNpp2UmH67OFbie6zHhyrSJKL2o9d05iX0a9Xwv9W48JKYpldo+/2JTP/5en0jxgiN+qkOCZuLag2cS/6Az0LArqsf5D+ReJemIBCNJhVxu3P0naxfEG6B6XczzuuptrX3H2vDr1LxZasLh9bzV88+8BxarjETACebOfqy366QxXluwAjnu/NHPv53edXlXvXrZ0C69RvvlMh1qQIDAQAB",
       "description": "Extension for testing the AI language model API.",
       "background": {
@@ -72,26 +50,18 @@
     )JS";
 
 // The boolean tuple describing:
-// 1. if the chrome://flag `kAIPromptAPI` is explicitly enabled;
-// 2. if the kAIPromptAPI kill switch is triggered;
-// 3. if the chrome://flag `kAIPromptAPIForExtension` is explicitly enabled;
-// 4. if the kAIPromptAPIForExtension kill switch is triggered;
-// 5. if the extension has an origin trial token.
-using Variant = std::tuple<bool, bool, bool, bool, bool>;
+// 1. if the `kAIPromptAPI` chrome://flag is explicitly enabled;
+// 2. if the `kAIPromptAPI` kill switch is triggered;
+// 3. if the `kAIPromptAPIForExtension` kill switch is triggered;
+using Variant = std::tuple<bool, bool, bool>;
 bool IsAPIFlagEnabled(Variant v) {
   return std::get<0>(v);
 }
 bool IsAPIKillSwitchTriggered(Variant v) {
   return std::get<1>(v);
 }
-bool IsExtensionFlagEnabled(Variant v) {
-  return std::get<2>(v);
-}
 bool IsExtensionKillSwitchTriggered(Variant v) {
-  return std::get<3>(v);
-}
-bool IsExtensionInOriginTrial(Variant v) {
-  return std::get<4>(v);
+  return std::get<2>(v);
 }
 
 // Describes the test variants in a meaningful way in the parameterized tests.
@@ -101,18 +71,11 @@
   std::string api_kill_switch = IsAPIKillSwitchTriggered(info.param)
                                     ? "WithAPIKillswitch"
                                     : "NoAPIKillswitch";
-  std::string extension_flag_enabled = IsExtensionFlagEnabled(info.param)
-                                           ? "WithExtensionFlag"
-                                           : "NoExtensionFlag";
   std::string extension_kill_switch = IsExtensionKillSwitchTriggered(info.param)
                                           ? "WithExtensionKillswitch"
                                           : "NoExtensionKillswitch";
-  std::string origin_trial =
-      IsExtensionInOriginTrial(info.param) ? "WithOTToken" : "NoOTToken";
   return base::JoinString(
-      {api_flag_enabled, api_kill_switch, extension_flag_enabled,
-       extension_kill_switch, origin_trial},
-      "_");
+      {api_flag_enabled, api_kill_switch, extension_kill_switch}, "_");
 }
 
 }  // namespace
@@ -127,14 +90,6 @@
       command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
                                       "AIPromptAPI");
     }
-    if (IsExtensionFlagEnabled(GetParam())) {
-      command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
-                                      "AIPromptAPIForExtension");
-    }
-
-    // Also specify the test public key to make the test token effective.
-    command_line->AppendSwitchASCII(embedder_support::kOriginTrialPublicKey,
-                                    kOriginTrialPublicKeyForTesting);
 
     base::flat_map<base::test::FeatureRef, bool> feature_states;
     if (IsAPIKillSwitchTriggered(GetParam())) {
@@ -146,14 +101,6 @@
     feature_list_.InitWithFeatureStates(feature_states);
   }
 
- protected:
-  std::string GetManifest() {
-    return base::StringPrintf(kManifestTemplate,
-                              IsExtensionInOriginTrial(GetParam())
-                                  ? kLanguageModelOriginTrialTokensField
-                                  : "");
-  }
-
  private:
   base::test::ScopedFeatureList feature_list_;
 };
@@ -163,11 +110,10 @@
     ExtensionAILanguageModelBrowserTest,
     testing::Combine(testing::Bool(),
                      testing::Bool(),
-                     testing::Bool(),
-                     testing::Bool(),
                      testing::Bool()),
     &DescribeTestVariant);
 
+// TODO(crbug.com/419321441): Support Built-In AI APIs on ChromeOS.
 #if BUILDFLAG(IS_CHROMEOS)
 #define MAYBE_WorkerAccess DISABLED_WorkerAccess
 #else
@@ -176,12 +122,11 @@
 IN_PROC_BROWSER_TEST_P(ExtensionAILanguageModelBrowserTest,
                        MAYBE_WorkerAccess) {
   TestExtensionDir test_dir;
-  test_dir.WriteManifest(GetManifest());
+  test_dir.WriteManifest(kManifestTemplate);
+  // Extension access is blocked by either kill switch.
   const bool is_api_exposed = IsAPIFlagEnabled(GetParam()) ||
-                              IsExtensionFlagEnabled(GetParam()) ||
                               (!IsAPIKillSwitchTriggered(GetParam()) &&
-                               !IsExtensionKillSwitchTriggered(GetParam()) &&
-                               IsExtensionInOriginTrial(GetParam()));
+                               !IsExtensionKillSwitchTriggered(GetParam()));
   test_dir.WriteFile(
       FILE_PATH_LITERAL("sw.js"),
       base::StringPrintf(kServiceWorkerScript, base::ToString(is_api_exposed)));
diff --git a/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_apitest.cc b/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_apitest.cc
index 91f5176..0c28b5813 100644
--- a/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_apitest.cc
+++ b/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_apitest.cc
@@ -5,6 +5,7 @@
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
+#include "base/strings/stringprintf.h"
 #include "base/threading/thread_restrictions.h"
 #include "build/build_config.h"
 #include "chrome/browser/extensions/extension_apitest.h"
diff --git a/chrome/browser/extensions/api/developer_private/extension_info_generator_desktop.cc b/chrome/browser/extensions/api/developer_private/extension_info_generator_desktop.cc
index c14c1d9..9758eba 100644
--- a/chrome/browser/extensions/api/developer_private/extension_info_generator_desktop.cc
+++ b/chrome/browser/extensions/api/developer_private/extension_info_generator_desktop.cc
@@ -48,14 +48,6 @@
     api::developer_private::ExtensionInfo info) {
   Profile* profile = Profile::FromBrowserContext(browser_context_);
 
-  if (extension_system_->extension_service()->allowlist()->ShouldDisplayWarning(
-          extension.id())) {
-    info.show_safe_browsing_allowlist_warning = true;
-  }
-
-  ExtensionManagement* extension_management =
-      ExtensionManagementFactory::GetForBrowserContext(browser_context_);
-
   // ControlledInfo.
   bool is_policy_location = Manifest::IsPolicyLocation(extension.location());
   if (is_policy_location) {
@@ -75,55 +67,6 @@
     }
   }
 
-  // Dependent extensions.
-  if (extension.is_shared_module()) {
-    std::unique_ptr<ExtensionSet> dependent_extensions =
-        SharedModuleService::Get(browser_context_)
-            ->GetDependentExtensions(&extension);
-    for (const scoped_refptr<const Extension>& dependent :
-         *dependent_extensions) {
-      developer::DependentExtension dependent_extension;
-      dependent_extension.id = dependent->id();
-      dependent_extension.name = dependent->name();
-      info.dependent_extensions.push_back(std::move(dependent_extension));
-    }
-  }
-  // TODO(crbug.com/413650880): Investigate if `parent_disabled_permissions`
-  // can be removed.
-  info.disable_reasons.parent_disabled_permissions = false;
-
-  // Location.
-  bool updates_from_web_store =
-      extension_management->UpdatesFromWebstore(extension);
-  if (extension.location() == mojom::ManifestLocation::kInternal &&
-      updates_from_web_store) {
-    info.location = developer::Location::kFromStore;
-  } else if (Manifest::IsUnpackedLocation(extension.location())) {
-    info.location = developer::Location::kUnpacked;
-  } else if (extension.was_installed_by_default() &&
-             !extension.was_installed_by_oem() && updates_from_web_store) {
-    info.location = developer::Location::kInstalledByDefault;
-  } else if (Manifest::IsExternalLocation(extension.location()) &&
-             updates_from_web_store) {
-    info.location = developer::Location::kThirdParty;
-  } else {
-    info.location = developer::Location::kUnknown;
-  }
-
-  ManagementPolicy* management_policy = extension_system_->management_policy();
-  info.must_remain_installed =
-      management_policy->MustRemainInstalled(&extension, nullptr);
-  info.user_may_modify =
-      management_policy->UserMayModifySettings(&extension, nullptr);
-
-  info.update_url =
-      extension_management->GetEffectiveUpdateURL(extension).spec();
-
-  // Show access requests in toolbar.
-  info.show_access_requests_in_toolbar =
-      SitePermissionsHelper(profile).ShowAccessRequestsInToolbar(
-          extension.id());
-
   // Pinned to toolbar.
   // TODO(crbug.com/40280426): Currently this information is only shown for
   // enabled extensions as only enabled extensions can have actions. However,
diff --git a/chrome/browser/extensions/api/developer_private/extension_info_generator_shared.cc b/chrome/browser/extensions/api/developer_private/extension_info_generator_shared.cc
index e6c15e15..ed888e7 100644
--- a/chrome/browser/extensions/api/developer_private/extension_info_generator_shared.cc
+++ b/chrome/browser/extensions/api/developer_private/extension_info_generator_shared.cc
@@ -23,7 +23,10 @@
 #include "chrome/browser/extensions/commands/command_service.h"
 #include "chrome/browser/extensions/error_console/error_console.h"
 #include "chrome/browser/extensions/extension_allowlist.h"
+#include "chrome/browser/extensions/extension_service.h"
 #include "chrome/browser/extensions/extension_util.h"
+#include "chrome/browser/extensions/permissions/site_permissions_helper.h"
+#include "chrome/browser/extensions/shared_module_service.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/webui/extensions/extension_icon_source.h"
 #include "chrome/common/chrome_features.h"
@@ -571,10 +574,44 @@
     info.blocklist_text = l10n_util::GetStringUTF8(blocklist_text);
   }
 
+  if (extension_system_->extension_service()->allowlist()->ShouldDisplayWarning(
+          extension.id())) {
+    info.show_safe_browsing_allowlist_warning = true;
+  }
+  ExtensionManagement* extension_management =
+      ExtensionManagementFactory::GetForBrowserContext(browser_context_);
+
+  // TODO(crbug.com/419419534): Add back ControlledInfo.
+
   Profile* profile = Profile::FromBrowserContext(browser_context_);
 
   bool is_enabled = state == developer::ExtensionState::kEnabled;
 
+  // Commands.
+  if (is_enabled) {
+    ConstructCommands(command_service_, extension.id(), &info.commands);
+  }
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+  info.is_command_registration_handled_externally =
+      ui::GlobalAcceleratorListener::GetInstance() &&
+      ui::GlobalAcceleratorListener::GetInstance()
+          ->IsRegistrationHandledExternally();
+#endif  // BUILDFLAG(ENABLE_EXTENSIONS)
+
+  // Dependent extensions.
+  if (extension.is_shared_module()) {
+    std::unique_ptr<ExtensionSet> dependent_extensions =
+        SharedModuleService::Get(browser_context_)
+            ->GetDependentExtensions(&extension);
+    for (const scoped_refptr<const Extension>& dependent :
+         *dependent_extensions) {
+      developer::DependentExtension dependent_extension;
+      dependent_extension.id = dependent->id();
+      dependent_extension.name = dependent->name();
+      info.dependent_extensions.push_back(std::move(dependent_extension));
+    }
+  }
+
   info.description = extension.description();
 
   // Disable reasons.
@@ -594,6 +631,9 @@
       disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED);
   info.disable_reasons.custodian_approval_required =
       custodian_approval_required;
+  // TODO(crbug.com/413650880): Investigate if `parent_disabled_permissions`
+  // can be removed.
+  info.disable_reasons.parent_disabled_permissions = false;
   info.disable_reasons.published_in_store_required = disable_reasons.contains(
       disable_reason::DISABLE_PUBLISHED_IN_STORE_REQUIRED_BY_POLICY);
   info.disable_reasons.unsupported_manifest_version = disable_reasons.contains(
@@ -611,6 +651,7 @@
       error_console_->IsReportingEnabledForExtension(extension.id());
 
   // File access.
+  ManagementPolicy* management_policy = extension_system_->management_policy();
   info.file_access.is_enabled =
       (extension.wants_file_access() ||
        Manifest::ShouldAlwaysAllowFileAccess(extension.location()));
@@ -682,8 +723,20 @@
   }
 
   // Location.
-  // Set it to kUnknown only if the caller didn't set it.
-  if (info.location == developer::Location::kNone) {
+  bool updates_from_web_store =
+      extension_management->UpdatesFromWebstore(extension);
+  if (extension.location() == mojom::ManifestLocation::kInternal &&
+      updates_from_web_store) {
+    info.location = developer::Location::kFromStore;
+  } else if (Manifest::IsUnpackedLocation(extension.location())) {
+    info.location = developer::Location::kUnpacked;
+  } else if (extension.was_installed_by_default() &&
+             !extension.was_installed_by_oem() && updates_from_web_store) {
+    info.location = developer::Location::kInstalledByDefault;
+  } else if (Manifest::IsExternalLocation(extension.location()) &&
+             updates_from_web_store) {
+    info.location = developer::Location::kThirdParty;
+  } else {
     info.location = developer::Location::kUnknown;
   }
 
@@ -725,6 +778,9 @@
     }
   }
 
+  info.must_remain_installed =
+      management_policy->MustRemainInstalled(&extension, nullptr);
+
   info.name = extension.name();
   info.offline_enabled = OfflineEnabledInfo::IsOfflineEnabled(&extension);
 
@@ -756,6 +812,12 @@
 
   info.type = GetExtensionType(extension.manifest()->type());
 
+  info.update_url =
+      extension_management->GetEffectiveUpdateURL(extension).spec();
+
+  info.user_may_modify =
+      management_policy->UserMayModifySettings(&extension, nullptr);
+
   info.version = extension.GetVersionForDisplay();
 
   if (state != developer::ExtensionState::kTerminated) {
@@ -763,16 +825,16 @@
         extension, is_enabled);
   }
 
-  // Commands.
-  if (is_enabled) {
-    ConstructCommands(command_service_, extension.id(), &info.commands);
-  }
-#if BUILDFLAG(ENABLE_EXTENSIONS)
-  info.is_command_registration_handled_externally =
-      ui::GlobalAcceleratorListener::GetInstance() &&
-      ui::GlobalAcceleratorListener::GetInstance()
-          ->IsRegistrationHandledExternally();
-#endif  // BUILDFLAG(ENABLE_EXTENSIONS)
+  // Show access requests in toolbar.
+  info.show_access_requests_in_toolbar =
+      SitePermissionsHelper(profile).ShowAccessRequestsInToolbar(
+          extension.id());
+  // TODO(crbug.com/419419534): Add back pinned_to_toolbar.
+
+  // TODO(crbug.com/419419534): Add back MV2 deprecation if needed, so that
+  // extension_info_generator_desktop can be removed.
+
+  // TODO(crbug.com/419419534): Add back can_upload_as_account_extension.
 
   // The icon. This section must come last as it moves `info`.
   ExtensionResource icon = IconsInfo::GetIconResource(
diff --git a/chrome/browser/extensions/api/extension_action/BUILD.gn b/chrome/browser/extensions/api/extension_action/BUILD.gn
index 524df04..d5441f6 100644
--- a/chrome/browser/extensions/api/extension_action/BUILD.gn
+++ b/chrome/browser/extensions/api/extension_action/BUILD.gn
@@ -10,8 +10,6 @@
   sources = [
     "extension_action_api.cc",
     "extension_action_api.h",
-    "extension_page_actions_api_constants.cc",
-    "extension_page_actions_api_constants.h",
   ]
 
   if (is_android) {
diff --git a/chrome/browser/extensions/api/extension_action/extension_page_actions_api_constants.cc b/chrome/browser/extensions/api/extension_action/extension_page_actions_api_constants.cc
deleted file mode 100644
index 9451961..0000000
--- a/chrome/browser/extensions/api/extension_action/extension_page_actions_api_constants.cc
+++ /dev/null
@@ -1,15 +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 "chrome/browser/extensions/api/extension_action/extension_page_actions_api_constants.h"
-
-namespace extension_page_actions_api_constants {
-
-const char kTabIdKey[] = "tabId";
-const char kTabUrlKey[] = "tabUrl";
-const char kUrlKey[] = "url";
-const char kTitleKey[] = "title";
-const char kButtonKey[] = "button";
-
-}  // namespace extension_page_actions_api_constants
diff --git a/chrome/browser/extensions/api/extension_action/extension_page_actions_api_constants.h b/chrome/browser/extensions/api/extension_action/extension_page_actions_api_constants.h
deleted file mode 100644
index 16cae46..0000000
--- a/chrome/browser/extensions/api/extension_action/extension_page_actions_api_constants.h
+++ /dev/null
@@ -1,21 +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.
-
-// Constants used for the Page Actions API.
-
-#ifndef CHROME_BROWSER_EXTENSIONS_API_EXTENSION_ACTION_EXTENSION_PAGE_ACTIONS_API_CONSTANTS_H_
-#define CHROME_BROWSER_EXTENSIONS_API_EXTENSION_ACTION_EXTENSION_PAGE_ACTIONS_API_CONSTANTS_H_
-
-namespace extension_page_actions_api_constants {
-
-// Keys.
-extern const char kTabIdKey[];
-extern const char kTabUrlKey[];
-extern const char kUrlKey[];
-extern const char kTitleKey[];
-extern const char kButtonKey[];
-
-}  // namespace extension_page_actions_api_constants
-
-#endif  // CHROME_BROWSER_EXTENSIONS_API_EXTENSION_ACTION_EXTENSION_PAGE_ACTIONS_API_CONSTANTS_H_
diff --git a/chrome/browser/extensions/api/offscreen/offscreen_apitest.cc b/chrome/browser/extensions/api/offscreen/offscreen_apitest.cc
index 0735984..6e196db 100644
--- a/chrome/browser/extensions/api/offscreen/offscreen_apitest.cc
+++ b/chrome/browser/extensions/api/offscreen/offscreen_apitest.cc
@@ -6,6 +6,7 @@
 
 #include "base/functional/callback_helpers.h"
 #include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "build/build_config.h"
 #include "chrome/browser/extensions/extension_apitest.h"
diff --git a/chrome/browser/extensions/api/scripting/scripting_apitest.cc b/chrome/browser/extensions/api/scripting/scripting_apitest.cc
index 819fab5..162366a5 100644
--- a/chrome/browser/extensions/api/scripting/scripting_apitest.cc
+++ b/chrome/browser/extensions/api/scripting/scripting_apitest.cc
@@ -4,6 +4,7 @@
 
 #include <optional>
 
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "build/build_config.h"
 #include "chrome/browser/extensions/api/scripting/scripting_api.h"
diff --git a/chrome/browser/extensions/content_verifier_browsertest.cc b/chrome/browser/extensions/content_verifier_browsertest.cc
index b2ada58..c7fb420 100644
--- a/chrome/browser/extensions/content_verifier_browsertest.cc
+++ b/chrome/browser/extensions/content_verifier_browsertest.cc
@@ -13,6 +13,7 @@
 #include "base/files/file_util.h"
 #include "base/functional/callback_helpers.h"
 #include "base/strings/string_split.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
diff --git a/chrome/browser/extensions/cross_origin_isolation_browsertest.cc b/chrome/browser/extensions/cross_origin_isolation_browsertest.cc
index e71f0f9..c8ada56 100644
--- a/chrome/browser/extensions/cross_origin_isolation_browsertest.cc
+++ b/chrome/browser/extensions/cross_origin_isolation_browsertest.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "base/files/file_path.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/to_string.h"
 #include "chrome/browser/extensions/extension_browsertest.h"
 #include "chrome/browser/profiles/profile.h"
diff --git a/chrome/browser/extensions/event_metrics_browsertest.cc b/chrome/browser/extensions/event_metrics_browsertest.cc
index 30a8c645..d9a3998e 100644
--- a/chrome/browser/extensions/event_metrics_browsertest.cc
+++ b/chrome/browser/extensions/event_metrics_browsertest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/strings/stringprintf.h"
 #include "chrome/browser/extensions/extension_apitest.h"
 #include "chrome/browser/extensions/extension_browsertest.h"
 #include "chrome/browser/profiles/profile.h"
diff --git a/chrome/browser/extensions/service_worker_installation_browsertest.cc b/chrome/browser/extensions/service_worker_installation_browsertest.cc
index 504bd216..3a7d973 100644
--- a/chrome/browser/extensions/service_worker_installation_browsertest.cc
+++ b/chrome/browser/extensions/service_worker_installation_browsertest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/strings/stringprintf.h"
 #include "chrome/browser/extensions/extension_browsertest.h"
 #include "chrome/browser/profiles/profile.h"
 #include "content/public/test/browser_test.h"
diff --git a/chrome/browser/extensions/user_host_restrictions_browsertest.cc b/chrome/browser/extensions/user_host_restrictions_browsertest.cc
index 1ff70f7..dc69d5a 100644
--- a/chrome/browser/extensions/user_host_restrictions_browsertest.cc
+++ b/chrome/browser/extensions/user_host_restrictions_browsertest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/test/scoped_feature_list.h"
 #include "chrome/browser/extensions/extension_apitest.h"
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 2aa56d5b..d4a2aeb 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -745,6 +745,14 @@
     "expiry_milestone": 140
   },
   {
+    "name": "autofill-enable-multiple-request-in-virtual-card-downstream-enrollment",
+    "owners": [
+      "siyua@chromium.org",
+      "payments-autofill-team@google.com"
+    ],
+    "expiry_milestone": 145
+  },
+  {
     "name": "autofill-enable-new-fop-display-desktop",
     "owners": [ "qihuizhao@google.com", "jsaul@google.com"],
     "expiry_milestone": 145
@@ -3847,17 +3855,17 @@
   },
   {
     "name": "enable-openxr-android",
-    "owners": [ "alcooper@chromium.org", "bajones@chromium.org", "bialpio@chromium.org", "xr-dev@chromium.org" ],
+    "owners": [ "alcooper@chromium.org", "bajones@chromium.org", "xr-dev@chromium.org" ],
     "expiry_milestone": 140
   },
   {
     "name": "enable-openxr-android-smooth-depth",
-    "owners": [ "alcooper@chromium.org", "bajones@chromium.org", "bialpio@chromium.org", "xr-dev@chromium.org" ],
+    "owners": [ "alcooper@chromium.org", "bajones@chromium.org", "xr-dev@chromium.org" ],
     "expiry_milestone": 150
   },
   {
     "name": "enable-openxr-extended",
-    "owners": [ "alcooper@chromium.org", "bajones@chromium.org", "bialpio@chromium.org", "xr-dev@chromium.org" ],
+    "owners": [ "alcooper@chromium.org", "bajones@chromium.org", "xr-dev@chromium.org" ],
     "expiry_milestone": 140
   },
   {
@@ -10220,7 +10228,7 @@
   },
   {
     "name": "zero-copy-tab-capture",
-    "owners": [ "bialpio@chromium.org", "media-capture-dev@chromium.org" ],
+    "owners": [ "media-capture-dev@chromium.org" ],
     "expiry_milestone": 140
   },
   {
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 581594cb..cd729b3 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -772,6 +772,16 @@
     "When enabled, Autofill will offer support for filling the user's loyalty "
     "cards stored in Google Wallet.";
 
+const char
+    kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollmentName[] =
+        "Enable multiple server request support for virtual card downstream "
+        "enrollment";
+const char
+    kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollmentDescription
+        [] = "When enabled, Chrome will be able to send preflight call for "
+             "enrollment earlier in the flow with the multiple server request "
+             "support.";
+
 const char kAutofillEnableNewFopDisplayDesktopName[] =
     "Enable Autofill new FOP display on Desktop";
 const char kAutofillEnableNewFopDisplayDesktopDescription[] =
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 8e3b4a3d..1df2dccf 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -462,6 +462,12 @@
 extern const char kAutofillEnableLoyaltyCardsFillingName[];
 extern const char kAutofillEnableLoyaltyCardsFillingDescription[];
 
+extern const char
+    kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollmentName[];
+extern const char
+    kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollmentDescription
+        [];
+
 extern const char kAutofillEnableNewFopDisplayDesktopName[];
 extern const char kAutofillEnableNewFopDisplayDesktopDescription[];
 
diff --git a/chrome/browser/glic/host/glic_actor_controller.cc b/chrome/browser/glic/host/glic_actor_controller.cc
index 53892dd..f4f06be 100644
--- a/chrome/browser/glic/host/glic_actor_controller.cc
+++ b/chrome/browser/glic/host/glic_actor_controller.cc
@@ -95,7 +95,8 @@
     GetActorCoordinator()->StartTask(
         action,
         base::BindOnce(&GlicActorController::OnTaskStarted, GetWeakPtr(),
-                       action, options, std::move(callback)));
+                       action, options, std::move(callback)),
+        /*tab_id=*/std::nullopt);
     return;
   }
 
diff --git a/chrome/browser/loader/keep_alive_category_request_browsertest.cc b/chrome/browser/loader/keep_alive_category_request_browsertest.cc
index 9b1de86..52e43e11 100644
--- a/chrome/browser/loader/keep_alive_category_request_browsertest.cc
+++ b/chrome/browser/loader/keep_alive_category_request_browsertest.cc
@@ -167,8 +167,10 @@
       content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
       content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
       /*keepalive_token=*/std::nullopt,
-      /*error_code=*/net::OK,
-      /*extended_error_code=*/0);
+      /*failed_error_code=*/std::nullopt,
+      /*failed_extended_error_code=*/std::nullopt,
+      /*completed_error_code=*/net::OK,
+      /*completed_extended_error_code=*/0);
   ExpectTimeSortedTimeDeltaUkm(
       {"TimeDelta.RequestStarted", "TimeDelta.ResponseReceived",
        "TimeDelta.LoaderCompleted", "TimeDelta.EventLogged"});
@@ -245,8 +247,10 @@
       content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
       content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
       /*keepalive_token=*/std::nullopt,
-      /*error_code=*/net::OK,
-      /*extended_error_code=*/0);
+      /*failed_error_code=*/std::nullopt,
+      /*failed_extended_error_code=*/std::nullopt,
+      /*completed_error_code=*/net::OK,
+      /*completed_extended_error_code=*/0);
   ExpectTimeSortedTimeDeltaUkm(
       {"TimeDelta.RequestStarted", "TimeDelta.ResponseReceived",
        "TimeDelta.LoaderCompleted", "TimeDelta.EventLogged"});
@@ -360,8 +364,10 @@
       content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
       content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
       /*keepalive_token=*/std::nullopt,
-      /*error_code=*/net::OK,
-      /*extended_error_code=*/0);
+      /*failed_error_code=*/std::nullopt,
+      /*failed_extended_error_code=*/std::nullopt,
+      /*completed_error_code=*/net::OK,
+      /*completed_extended_error_code=*/0);
   ExpectTimeSortedTimeDeltaUkm(
       {"TimeDelta.RequestStarted", "TimeDelta.ResponseReceived",
        "TimeDelta.LoaderCompleted", "TimeDelta.EventLogged"});
@@ -400,8 +406,10 @@
       content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
       content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
       /*keepalive_token=*/std::nullopt,
-      /*error_code=*/net::OK,
-      /*extended_error_code=*/0);
+      /*failed_error_code=*/std::nullopt,
+      /*failed_extended_error_code=*/std::nullopt,
+      /*completed_error_code=*/net::OK,
+      /*completed_extended_error_code=*/0);
   ExpectTimeSortedTimeDeltaUkm(
       {"TimeDelta.RequestStarted", "TimeDelta.ResponseReceived",
        "TimeDelta.LoaderCompleted", "TimeDelta.EventLogged"});
@@ -449,8 +457,10 @@
         content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
         content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
         /*keepalive_token=*/std::nullopt,
-        /*error_code=*/net::OK,
-        /*extended_error_code=*/0},
+        /*failed_error_code=*/std::nullopt,
+        /*failed_extended_error_code=*/std::nullopt,
+        /*completed_error_code=*/net::OK,
+        /*completed_extended_error_code=*/0},
        {content::KeepAliveRequestTracker::RequestType::kFetch,
         /*category_id=*/2,
         /*num_redirects=*/0,
@@ -458,8 +468,10 @@
         content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
         content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
         /*keepalive_token=*/std::nullopt,
-        /*error_code=*/net::OK,
-        /*extended_error_code=*/0}});
+        /*failed_error_code=*/std::nullopt,
+        /*failed_extended_error_code=*/std::nullopt,
+        /*completed_error_code=*/net::OK,
+        /*completed_extended_error_code=*/0}});
   // Only request with `category2` should be paired with the navigation
   // request.
   ExpectNavigationUkm(/*category_id=*/2, /*navigation_id=*/std::nullopt,
@@ -503,8 +515,10 @@
         content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
         content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
         /*keepalive_token=*/std::nullopt,
-        /*error_code=*/net::OK,
-        /*extended_error_code=*/0},
+        /*failed_error_code=*/std::nullopt,
+        /*failed_extended_error_code=*/std::nullopt,
+        /*completed_error_code=*/net::OK,
+        /*completed_extended_error_code=*/0},
        {content::KeepAliveRequestTracker::RequestType::kFetch,
         /*category_id=*/1,
         /*num_redirects=*/0,
@@ -512,8 +526,10 @@
         content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
         content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
         /*keepalive_token=*/std::nullopt,
-        /*error_code=*/net::OK,
-        /*extended_error_code=*/0}});
+        /*failed_error_code=*/std::nullopt,
+        /*failed_extended_error_code=*/std::nullopt,
+        /*completed_error_code=*/net::OK,
+        /*completed_extended_error_code=*/0}});
   // Only one request should be paired with the navigation request, event though
   // both requests have the same category ID.
   ExpectNavigationUkm(/*category_id=*/1, /*navigation_id=*/std::nullopt,
@@ -550,8 +566,10 @@
       content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
       content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
       /*keepalive_token=*/std::nullopt,
-      /*error_code=*/net::OK,
-      /*extended_error_code=*/0);
+      /*failed_error_code=*/std::nullopt,
+      /*failed_extended_error_code=*/std::nullopt,
+      /*completed_error_code=*/net::OK,
+      /*completed_extended_error_code=*/0);
   ExpectTimeSortedTimeDeltaUkm(
       {"TimeDelta.RequestStarted", "TimeDelta.ResponseReceived",
        "TimeDelta.LoaderCompleted", "TimeDelta.EventLogged"});
@@ -594,8 +612,10 @@
       content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
       content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
       /*keepalive_token=*/std::nullopt,
-      /*error_code=*/net::OK,
-      /*extended_error_code=*/0);
+      /*failed_error_code=*/std::nullopt,
+      /*failed_extended_error_code=*/std::nullopt,
+      /*completed_error_code=*/net::OK,
+      /*completed_extended_error_code=*/0);
   ExpectTimeSortedTimeDeltaUkm(
       {"TimeDelta.RequestStarted", "TimeDelta.ResponseReceived",
        "TimeDelta.LoaderCompleted", "TimeDelta.EventLogged"});
@@ -647,8 +667,10 @@
         content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
         content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
         /*keepalive_token=*/std::nullopt,
-        /*error_code=*/net::OK,
-        /*extended_error_code=*/0},
+        /*failed_error_code=*/std::nullopt,
+        /*failed_extended_error_code=*/std::nullopt,
+        /*completed_error_code=*/net::OK,
+        /*completed_extended_error_code=*/0},
        {content::KeepAliveRequestTracker::RequestType::kFetch,
         /*category_id=*/2,
         /*num_redirects=*/0,
@@ -656,8 +678,10 @@
         content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
         content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
         /*keepalive_token=*/std::nullopt,
-        /*error_code=*/net::OK,
-        /*extended_error_code=*/0}});
+        /*failed_error_code=*/std::nullopt,
+        /*failed_extended_error_code=*/std::nullopt,
+        /*completed_error_code=*/net::OK,
+        /*completed_extended_error_code=*/0}});
   // Only one navigation should be paired with the fetch keepalive request.
   ExpectNavigationUkms({{/*category_id=*/1, /*navigation_id=*/std::nullopt,
                          /*keepalive_token=*/std::nullopt},
@@ -695,8 +719,10 @@
       content::KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
       content::KeepAliveRequestTracker::RequestStageType::kResponseReceived,
       /*keepalive_token=*/std::nullopt,
-      /*error_code=*/net::OK,
-      /*extended_error_code=*/0);
+      /*failed_error_code=*/std::nullopt,
+      /*failed_extended_error_code=*/std::nullopt,
+      /*completed_error_code=*/net::OK,
+      /*completed_extended_error_code=*/0);
   ExpectTimeSortedTimeDeltaUkm(
       {"TimeDelta.RequestStarted", "TimeDelta.ResponseReceived",
        "TimeDelta.LoaderCompleted", "TimeDelta.EventLogged"});
diff --git a/chrome/browser/loader/keep_alive_request_browsertest_util.cc b/chrome/browser/loader/keep_alive_request_browsertest_util.cc
index a92fbbb..da7e784 100644
--- a/chrome/browser/loader/keep_alive_request_browsertest_util.cc
+++ b/chrome/browser/loader/keep_alive_request_browsertest_util.cc
@@ -5,6 +5,7 @@
 #include "chrome/browser/loader/keep_alive_request_browsertest_util.h"
 
 #include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
 #include "chrome/browser/ui/browser.h"
 #include "components/network_session_configurator/common/network_switches.h"
 #include "content/public/test/back_forward_cache_util.h"
diff --git a/chrome/browser/loader/keep_alive_request_tracker.cc b/chrome/browser/loader/keep_alive_request_tracker.cc
index eab5b91..90ddf4d6 100644
--- a/chrome/browser/loader/keep_alive_request_tracker.cc
+++ b/chrome/browser/loader/keep_alive_request_tracker.cc
@@ -144,8 +144,8 @@
     case RequestStageType::kRequestFailed:
       ukm_builder_.SetTimeDelta_RequestFailed(
           relative_to_created_time.InMilliseconds());
-      ukm_builder_.SetCompletionStatus_ErrorCode(stage.status->error_code);
-      ukm_builder_.SetCompletionStatus_ExtendedErrorCode(
+      ukm_builder_.SetRequestFailed_ErrorCode(stage.status->error_code);
+      ukm_builder_.SetRequestFailed_ExtendedErrorCode(
           stage.status->extended_error_code);
       break;
 
@@ -172,8 +172,8 @@
     case RequestStageType::kLoaderCompleted:
       ukm_builder_.SetTimeDelta_LoaderCompleted(
           relative_to_created_time.InMilliseconds());
-      ukm_builder_.SetCompletionStatus_ErrorCode(stage.status->error_code);
-      ukm_builder_.SetCompletionStatus_ExtendedErrorCode(
+      ukm_builder_.SetLoaderCompleted_ErrorCode(stage.status->error_code);
+      ukm_builder_.SetLoaderCompleted_ExtendedErrorCode(
           stage.status->extended_error_code);
       break;
   }
diff --git a/chrome/browser/loader/keep_alive_request_tracker_unittest.cc b/chrome/browser/loader/keep_alive_request_tracker_unittest.cc
index b94743a..d3ebd1c 100644
--- a/chrome/browser/loader/keep_alive_request_tracker_unittest.cc
+++ b/chrome/browser/loader/keep_alive_request_tracker_unittest.cc
@@ -438,6 +438,8 @@
                   /*is_context_detached=*/false,
                   RequestStageType::kLoaderCompleted,
                   RequestStageType::kResponseReceived, *request.keepalive_token,
+                  /*failed_error_code=*/std::nullopt,
+                  /*failed_extended_error_code=*/std::nullopt,
                   status.error_code, status.extended_error_code);
   ExpectTimeSortedTimeDeltaUkm(
 
@@ -466,6 +468,8 @@
                   /*is_context_detached=*/false,
                   RequestStageType::kLoaderCompleted,
                   RequestStageType::kRequestStarted, *request.keepalive_token,
+                  /*failed_error_code=*/std::nullopt,
+                  /*failed_extended_error_code=*/std::nullopt,
                   failed_status.error_code, failed_status.extended_error_code);
   ExpectTimeSortedTimeDeltaUkm(
 
diff --git a/chrome/browser/media/media_session_browsertest.cc b/chrome/browser/media/media_session_browsertest.cc
index ed25d5c..65bd686 100644
--- a/chrome/browser/media/media_session_browsertest.cc
+++ b/chrome/browser/media/media_session_browsertest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/test/base/in_process_browser_test.h"
diff --git a/chrome/browser/media/prefs/OWNERS b/chrome/browser/media/prefs/OWNERS
index 73795e6..9780be5 100644
--- a/chrome/browser/media/prefs/OWNERS
+++ b/chrome/browser/media/prefs/OWNERS
@@ -1,3 +1,2 @@
 bryantchandler@chromium.org
 mfoltz@chromium.org
-bialpio@chromium.org
diff --git a/chrome/browser/media_effects/OWNERS b/chrome/browser/media_effects/OWNERS
index d4987c9..58c950b 100644
--- a/chrome/browser/media_effects/OWNERS
+++ b/chrome/browser/media_effects/OWNERS
@@ -1,4 +1,3 @@
 bryantchandler@chromium.org
 mfoltz@chromium.org
-bialpio@chromium.org
 ahmedmoussa@google.com
diff --git a/chrome/browser/net/websocket_browsertest.cc b/chrome/browser/net/websocket_browsertest.cc
index 9e1877b..90ee7c3 100644
--- a/chrome/browser/net/websocket_browsertest.cc
+++ b/chrome/browser/net/websocket_browsertest.cc
@@ -17,6 +17,7 @@
 #include "base/path_service.h"
 #include "base/run_loop.h"
 #include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/bind.h"
 #include "base/test/run_until.h"
diff --git a/chrome/browser/platform_experience/win b/chrome/browser/platform_experience/win
index 1c5aab8..e050381 160000
--- a/chrome/browser/platform_experience/win
+++ b/chrome/browser/platform_experience/win
@@ -1 +1 @@
-Subproject commit 1c5aab84b6952ed67196e6a56788e9ef9a6e886e
+Subproject commit e0503816507543e3b732a5a46109e9f71d74c3e1
diff --git a/chrome/browser/policy/test/ipv6_reachability_override_policy_browsertest.cc b/chrome/browser/policy/test/ipv6_reachability_override_policy_browsertest.cc
index a27b10e..abb9768 100644
--- a/chrome/browser/policy/test/ipv6_reachability_override_policy_browsertest.cc
+++ b/chrome/browser/policy/test/ipv6_reachability_override_policy_browsertest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/strings/stringprintf.h"
 #include "chrome/browser/policy/policy_test_utils.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
diff --git a/chrome/browser/private_network_access/OWNERS b/chrome/browser/private_network_access/OWNERS
index fbd73a5..c8542ff 100644
--- a/chrome/browser/private_network_access/OWNERS
+++ b/chrome/browser/private_network_access/OWNERS
@@ -1,6 +1,5 @@
-lyf@chromium.org
-titouan@chromium.org
-phao@chromium.org
 clamy@chromium.org
+cthomp@chromium.org
 estark@chromium.org
+hchao@chromium.org
 jdeblasio@chromium.org
diff --git a/chrome/browser/resources/ash/settings/os_languages_page/input_method_types.ts b/chrome/browser/resources/ash/settings/os_languages_page/input_method_types.ts
index 624504dc..975ee99b7 100644
--- a/chrome/browser/resources/ash/settings/os_languages_page/input_method_types.ts
+++ b/chrome/browser/resources/ash/settings/os_languages_page/input_method_types.ts
@@ -35,11 +35,9 @@
 }
 
 export enum JapaneseKeymapStyle {
-  CUSTOM = 'Custom',
   ATOK = 'Atok',
   MS_IME = 'MsIme',
   KOTOERI = 'Kotoeri',
-  MOBILE = 'Mobile',
   CHROME_OS = 'ChromeOs',
 }
 
diff --git a/chrome/browser/resources/ash/settings/os_languages_page/input_method_util.ts b/chrome/browser/resources/ash/settings/os_languages_page/input_method_util.ts
index 72ed196..d00e832 100644
--- a/chrome/browser/resources/ash/settings/os_languages_page/input_method_util.ts
+++ b/chrome/browser/resources/ash/settings/os_languages_page/input_method_util.ts
@@ -166,7 +166,7 @@
   [OptionType.JAPANESE_SPACE_INPUT_STYLE]: JapaneseSpaceInputStyle.INPUT_MODE,
   [OptionType.JAPANESE_SECTION_SHORTCUT]:
       JapaneseSectionShortcut.DIGITS_123456789,
-  [OptionType.JAPANESE_KEYMAP_STYLE]: JapaneseKeymapStyle.CUSTOM,
+  [OptionType.JAPANESE_KEYMAP_STYLE]: JapaneseKeymapStyle.CHROME_OS,
   [OptionType.JAPANESE_DISABLE_PERSONALIZED_SUGGESTIONS]: false,
   // LINT.ThenChange(/chrome/browser/ash/input_method/japanese/japanese_settings.cc:JpPrefDefaults)
 
@@ -945,10 +945,6 @@
     case OptionType.JAPANESE_KEYMAP_STYLE:
       return [
         {
-          value: JapaneseKeymapStyle.CUSTOM,
-          name: 'inputMethodOptionsJapaneseKeymapStyleCustom',
-        },
-        {
           value: JapaneseKeymapStyle.ATOK,
           name: 'inputMethodOptionsJapaneseKeymapStyleAtok',
         },
diff --git a/chrome/browser/resources/lens/overlay/post_selection_renderer.html b/chrome/browser/resources/lens/overlay/post_selection_renderer.html
index c2c9640..ae16f29d 100644
--- a/chrome/browser/resources/lens/overlay/post_selection_renderer.html
+++ b/chrome/browser/resources/lens/overlay/post_selection_renderer.html
@@ -10,18 +10,24 @@
     background-color: var(--color-scrim);
     height: 100%;
     opacity: 20%;
-    transition: opacity cubic-bezier(0.2, 0.0, 0, 1.0) 400ms;
+    transition: opacity cubic-bezier(0.2, 0, 0, 1) 400ms;
     width: 100%;
   }
 
+  :host([region-selected-glow-enabled]) #postSelectionScrim {
+    /* Hide this scrim because its superfluous here - the region_selection scrim
+    * is already active and has transitioned to its post-selection opacity. */
+    display: none;
+  }
+
   /** Render the selected part of the image again so it appears glowing over
       the scrim */
   #backgroundImageCanvas {
-    clip-path: rect(var(--selection-top)
-                    calc(var(--selection-left) + var(--selection-width))
-                    calc(var(--selection-top) + var(--selection-height))
-                    var(--selection-left)
-                    round var(--post-selection-cutout-corner-radius));
+    clip-path: rect(
+      var(--selection-top) calc(var(--selection-left) + var(--selection-width))
+        calc(var(--selection-top) + var(--selection-height)) var(--selection-left) round
+        var(--post-selection-cutout-corner-radius)
+    );
     height: 100%;
     inset: 0;
     object-fit: contain;
@@ -29,6 +35,12 @@
     width: 100%;
   }
 
+  :host([region-selected-glow-enabled]) #backgroundImageCanvas {
+    /* Do not use z-index once region-selection-glow-enabled is launched.
+     * Instead, move the backgroundImageCanvas higher in the DOM. */
+    z-index: 2;
+  }
+
   #postSelection {
     /* Scrim is rendered here too, so that the rendered screenshot is dark
      * as if it were under the scrim as well. */
@@ -44,6 +56,31 @@
     width: var(--selection-width);
   }
 
+  :host([region-selected-glow-enabled]) #postSelection {
+    background-color: transparent;
+    opacity: 1;
+    transition: none;
+  }
+
+  :host([region-selected-glow-enabled]) #postSelection:before {
+    background: conic-gradient(
+      from 90deg at center,
+      var(--gradient-blue) 0deg,
+      var(--gradient-blue) 162deg,
+      var(--gradient-red) 216deg,
+      var(--gradient-yellow) 274deg,
+      var(--gradient-green) 331deg
+    );
+    content: "";
+    /* Generally blur should be avoided for performance reasons but it's ok here
+     * because it's only being calculated once */
+    filter: blur(40px);
+    inset: 0;
+    opacity: 1;
+    position: absolute;
+    transition: opacity 166ms cubic-bezier(0.3, 0, 1, 1);
+  }
+
   :host([should-darken-scrim]) #postSelection {
     opacity: 20%;
   }
@@ -51,13 +88,11 @@
   #selectionCorners {
     background-image: paint(post-selection);
     forced-color-adjust: none;
-    height: calc(var(--selection-height) +
-                    (2 * var(--post-selection-corner-width)));
+    height: calc(var(--selection-height) + (2 * var(--post-selection-corner-width)));
     left: calc(var(--selection-left) - var(--post-selection-corner-width));
     position: absolute;
     top: calc(var(--selection-top) - var(--post-selection-corner-width));
-    width: calc(var(--selection-width) +
-                   (2 * var(--post-selection-corner-width)));
+    width: calc(var(--selection-width) + (2 * var(--post-selection-corner-width)));
     z-index: 5; /* Position above words. */
   }
 
@@ -80,28 +115,28 @@
     cursor: nw-resize;
     left: 0;
     top: 0;
-    transform: translate(-25%, -25%)
+    transform: translate(-25%, -25%);
   }
 
   #topRight {
     cursor: ne-resize;
     top: 0;
     right: 0;
-    transform: translate(25%, -25%)
+    transform: translate(25%, -25%);
   }
 
   #bottomRight {
     cursor: se-resize;
     bottom: 0;
     right: 0;
-    transform: translate(25%, 25%)
+    transform: translate(25%, 25%);
   }
 
   #bottomLeft {
     cursor: sw-resize;
     bottom: 0;
     left: 0;
-    transform: translate(-25%, 25%)
+    transform: translate(-25%, 25%);
   }
 
   .slider {
@@ -112,24 +147,33 @@
     opacity: 0;
   }
 </style>
-<div id="postSelectionScrim"
-    style$="[[getScrimStyleProperties(height, width)]]">
-</div>
+<div id="postSelectionScrim" style$="[[getScrimStyleProperties(height, width)]]"></div>
 <div hidden$="[[!hasSelection(height, width)]]">
-  <canvas id="backgroundImageCanvas" height="[[canvasPhysicalHeight]]"
-      width="[[canvasPhysicalWidth]]"
-      style$="height: [[canvasHeight]]px; width: [[canvasWidth]]px;">
+  <canvas
+    id="backgroundImageCanvas"
+    height="[[canvasPhysicalHeight]]"
+    width="[[canvasPhysicalWidth]]"
+    style$="height: [[canvasHeight]]px; width: [[canvasWidth]]px;"
+  >
   </canvas>
-  <div id="postSelection"></div>
+  <div style$="[[getHexColorStyles()]]" id="postSelection"></div>
   <div id="selectionCorners">
     <template is="dom-repeat" items="[[cornerIds]]">
       <div id="[[item]]" class="corner-hit-box">
         <template is="dom-if" if="[[cornerSlidersEnabled]]">
-          <input id="[[item]]Slider" type="range" tabindex="0" min="-1"
-              max="101" step="1" class="slider" on-change="handleSliderChange"
-              data-corner-id$="[[item]]">
+          <input
+            id="[[item]]Slider"
+            type="range"
+            tabindex="0"
+            min="-1"
+            max="101"
+            step="1"
+            class="slider"
+            on-change="handleSliderChange"
+            data-corner-id$="[[item]]"
+          />
         </template>
       </div>
     </template>
   </div>
-</div>
\ No newline at end of file
+</div>
diff --git a/chrome/browser/resources/lens/overlay/post_selection_renderer.ts b/chrome/browser/resources/lens/overlay/post_selection_renderer.ts
index 98ca370d..d7e1caa 100644
--- a/chrome/browser/resources/lens/overlay/post_selection_renderer.ts
+++ b/chrome/browser/resources/lens/overlay/post_selection_renderer.ts
@@ -10,6 +10,7 @@
 
 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';
@@ -119,6 +120,11 @@
       canvasWidth: Number,
       canvasPhysicalHeight: Number,
       canvasPhysicalWidth: Number,
+      regionSelectedGlowEnabled: {
+        type: Boolean,
+        reflectToAttribute: true,
+        value: () => loadTimeData.getBoolean('enableRegionSelectedGlow'),
+      },
       selectionOverlayRect: Object,
       shouldDarkenScrim: {
         type: Boolean,
@@ -149,6 +155,8 @@
   declare private canvasPhysicalWidth: number;
   // The bounds of the parent element. This is updated by the parent to avoid
   // this class needing to call getBoundingClientRect().
+  // Whether the region selected glow is enabled via feature flag.
+  declare private regionSelectedGlowEnabled: boolean;
   declare private selectionOverlayRect: DOMRect;
 
   private context: CanvasRenderingContext2D;
@@ -458,6 +466,16 @@
     }, this.sliderChangedTimeout);
   }
 
+  private getHexColorStyles() {
+    const style: string[] = [
+      `--gradient-blue: ${GLIF_HEX_COLORS.blue}`,
+      `--gradient-red: ${GLIF_HEX_COLORS.red}`,
+      `--gradient-yellow: ${GLIF_HEX_COLORS.yellow}`,
+      `--gradient-green: ${GLIF_HEX_COLORS.green}`,
+    ];
+    return style.join('; ');
+  }
+
   private setDimensions(
       top: number, left: number, height: number, width: number) {
     this.top = top;
diff --git a/chrome/browser/resources/lens/overlay/region_selection.html b/chrome/browser/resources/lens/overlay/region_selection.html
index 5735a4f..d4262762 100644
--- a/chrome/browser/resources/lens/overlay/region_selection.html
+++ b/chrome/browser/resources/lens/overlay/region_selection.html
@@ -25,7 +25,7 @@
 
   :host([border-glow-enabled][has-selected]) #regionSelectionCanvas {
     /* 20% scrim */
-    background: rgba(0, 0, 0, 0.2);
+    background: rgba(0, 0, 0, 0.1);
     opacity: 1;
   }
 </style>
diff --git a/chrome/browser/resources/lens/overlay/selection_overlay.html b/chrome/browser/resources/lens/overlay/selection_overlay.html
index a2e7f89..177d2e3f 100644
--- a/chrome/browser/resources/lens/overlay/selection_overlay.html
+++ b/chrome/browser/resources/lens/overlay/selection_overlay.html
@@ -380,6 +380,7 @@
     <post-selection-renderer
       id="postSelectionRenderer"
       selection-overlay-rect="[[selectionOverlayRect]]"
+      region-selected-glow-enabled="[[enableRegionSelectedGlow]]"
     >
     </post-selection-renderer>
     <lens-object-layer
diff --git a/chrome/browser/resources/print_preview/BUILD.gn b/chrome/browser/resources/print_preview/BUILD.gn
index a382400e..86eefc8 100644
--- a/chrome/browser/resources/print_preview/BUILD.gn
+++ b/chrome/browser/resources/print_preview/BUILD.gn
@@ -119,7 +119,6 @@
   icons_html_files = [ "ui/icons.html" ]
   html_to_wrapper_template = "detect"
 
-  ts_tsconfig_base = "tsconfig_base.json"
   ts_composite = true
   ts_definitions = [
     "//tools/typescript/definitions/chrome_event.d.ts",
diff --git a/chrome/browser/resources/print_preview/data/model.ts b/chrome/browser/resources/print_preview/data/model.ts
index 45664dc..8caffcb 100644
--- a/chrome/browser/resources/print_preview/data/model.ts
+++ b/chrome/browser/resources/print_preview/data/model.ts
@@ -492,7 +492,7 @@
   }
 
   accessor settingsManaged: boolean = false;
-  accessor destination: Destination;
+  accessor destination: Destination|null = null;
   accessor documentSettings: DocumentSettings = createDocumentSettings();
   accessor margins: Margins|null = null;
   accessor pageSize: Size = new Size(612, 792);
@@ -681,6 +681,7 @@
   }
 
   private updateSettingsAvailabilityFromDestination_() {
+    assert(this.destination);
     const caps = this.destination.capabilities ?
         this.destination.capabilities.printer :
         null;
@@ -710,12 +711,14 @@
     this.setSettingPath_(
         'vendorItems.available', !!caps && !!caps.vendor_capability);
 
-    if (this.documentSettings) {
-      this.updateSettingsAvailabilityFromDestinationAndDocumentSettings_();
-    }
+    this.updateSettingsAvailabilityFromDestinationAndDocumentSettings_();
   }
 
   private updateSettingsAvailabilityFromDestinationAndDocumentSettings_() {
+    if (!this.documentSettings || !this.destination) {
+      return;
+    }
+
     const isSaveAsPDF = this.destination.type === PrinterType.PDF_PRINTER;
     const knownSizeToSaveAsPdf = isSaveAsPDF &&
         (!this.documentSettings.isModifiable ||
@@ -728,9 +731,7 @@
     this.setSettingPath_(
         'scalingTypePdf.available',
         scalingAvailable && !this.documentSettings.isModifiable);
-    const caps = this.destination && this.destination.capabilities ?
-        this.destination.capabilities.printer :
-        null;
+    const caps = this.destination.capabilities?.printer || null;
     this.setSettingPath_(
         'mediaSize.available',
         !!caps && !!caps.media_size && !knownSizeToSaveAsPdf);
@@ -762,9 +763,7 @@
             this.settings_.headerFooter.available ||
             this.settings_.rasterize.available);
 
-    if (this.destination) {
-      this.updateSettingsAvailabilityFromDestinationAndDocumentSettings_();
-    }
+    this.updateSettingsAvailabilityFromDestinationAndDocumentSettings_();
   }
 
   private updateHeaderFooterAvailable_() {
@@ -857,6 +856,7 @@
   }
 
   private updateSettingsValues_() {
+    assert(this.destination);
     const caps = this.destination.capabilities ?
         this.destination.capabilities.printer :
         null;
@@ -1342,6 +1342,7 @@
       return;
     }
 
+    assert(this.destination);
     const mediaSizePolicy = this.policySettings_['mediaSize'].value;
     const matchingOption = this.destination.getMediaSize(
         mediaSizePolicy.width, mediaSizePolicy.height);
diff --git a/chrome/browser/resources/print_preview/tsconfig_base.json b/chrome/browser/resources/print_preview/tsconfig_base.json
deleted file mode 100644
index 8bda7687..0000000
--- a/chrome/browser/resources/print_preview/tsconfig_base.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "extends": "../../../../tools/typescript/tsconfig_base_polymer.json",
-  "compilerOptions": {
-    "target": "ES2024",
-    "lib": ["ES2024", "DOM", "DOM.Iterable"],
-    "noUncheckedIndexedAccess": true,
-    "noUnusedLocals": true
-  }
-}
diff --git a/chrome/browser/resources/print_preview/ui/advanced_options_settings.ts b/chrome/browser/resources/print_preview/ui/advanced_options_settings.ts
index 50b61c0..e2cad9b 100644
--- a/chrome/browser/resources/print_preview/ui/advanced_options_settings.ts
+++ b/chrome/browser/resources/print_preview/ui/advanced_options_settings.ts
@@ -42,7 +42,7 @@
   }
 
   accessor disabled: boolean = false;
-  accessor destination: Destination;
+  accessor destination: Destination|null = null;
   protected accessor showAdvancedDialog_: boolean = false;
 
   protected onButtonClick_() {
diff --git a/chrome/browser/resources/print_preview/ui/advanced_settings_dialog.html b/chrome/browser/resources/print_preview/ui/advanced_settings_dialog.html
index ff5058a..696b041 100644
--- a/chrome/browser/resources/print_preview/ui/advanced_settings_dialog.html
+++ b/chrome/browser/resources/print_preview/ui/advanced_settings_dialog.html
@@ -1,6 +1,6 @@
 <cr-dialog id="dialog" @close="${this.onCloseOrCancel_}">
   <div slot="title">
-    ${this.i18n('advancedSettingsDialogTitle', this.destination.displayName)}
+    ${this.i18n('advancedSettingsDialogTitle', this.destination?.displayName || '')}
   </div>
   <div slot="body">
     <print-preview-search-box id="searchBox"
diff --git a/chrome/browser/resources/print_preview/ui/advanced_settings_dialog.ts b/chrome/browser/resources/print_preview/ui/advanced_settings_dialog.ts
index 3694f3f..423c19aa 100644
--- a/chrome/browser/resources/print_preview/ui/advanced_settings_dialog.ts
+++ b/chrome/browser/resources/print_preview/ui/advanced_settings_dialog.ts
@@ -59,7 +59,7 @@
     };
   }
 
-  accessor destination: Destination;
+  accessor destination: Destination|null = null;
   protected accessor searchQuery_: RegExp|null = null;
   private accessor hasMatching_: boolean = false;
 
@@ -117,6 +117,10 @@
    * @return Whether there is more than one vendor item to display.
    */
   protected hasMultipleItems_(): boolean {
+    if (!this.destination) {
+      return false;
+    }
+
     return this.destination.capabilities!.printer.vendor_capability!.length > 1;
   }
 
@@ -187,7 +191,7 @@
   }
 
   protected getVendorCapabilities_(): VendorCapability[] {
-    return this.destination.capabilities?.printer.vendor_capability || [];
+    return this.destination?.capabilities?.printer.vendor_capability || [];
   }
 
   protected onSearchQueryChanged_(e: CustomEvent<{value: RegExp | null}>) {
diff --git a/chrome/browser/resources/print_preview/ui/advanced_settings_item.ts b/chrome/browser/resources/print_preview/ui/advanced_settings_item.ts
index 520f040f..87ab6285 100644
--- a/chrome/browser/resources/print_preview/ui/advanced_settings_item.ts
+++ b/chrome/browser/resources/print_preview/ui/advanced_settings_item.ts
@@ -42,7 +42,7 @@
     };
   }
 
-  accessor capability: VendorCapability;
+  accessor capability: VendorCapability = {id: '', type: ''};
   private accessor currentValue_: string = '';
 
   override connectedCallback() {
diff --git a/chrome/browser/resources/print_preview/ui/app.ts b/chrome/browser/resources/print_preview/ui/app.ts
index 9ad8d6e..8d174f7e 100644
--- a/chrome/browser/resources/print_preview/ui/app.ts
+++ b/chrome/browser/resources/print_preview/ui/app.ts
@@ -84,7 +84,7 @@
 
   accessor state: State = State.NOT_READY;
   protected accessor controlsManaged_: boolean = false;
-  protected accessor destination_: Destination;
+  protected accessor destination_: Destination|null = null;
   private accessor destinationsManaged_: boolean = false;
   protected accessor documentSettings_: DocumentSettings =
       createDocumentSettings();
@@ -324,7 +324,7 @@
         this.startPreviewWhenReady_ = true;
 
         if (this.state === State.NOT_READY &&
-            this.destination_.type !== PrinterType.PDF_PRINTER) {
+            this.destination_!.type !== PrinterType.PDF_PRINTER) {
           this.nativeLayer_!.recordBooleanHistogram(
               'PrintPreview.TransitionedToReadyState', true);
         }
@@ -333,7 +333,7 @@
         break;
       case DestinationState.ERROR:
         if (this.state === State.NOT_READY &&
-            this.destination_.type !== PrinterType.PDF_PRINTER) {
+            this.destination_!.type !== PrinterType.PDF_PRINTER) {
           this.nativeLayer_!.recordBooleanHistogram(
               'PrintPreview.TransitionedToReadyState', false);
         }
@@ -358,8 +358,8 @@
       // is synced across print-preview-app, print-preview-model and
       // print-preview-area.
       await this.updateComplete;
-      assert(this.destination_.id === this.$.previewArea.destination.id);
-      assert(this.destination_.id === this.$.model.destination.id);
+      assert(this.destination_!.id === this.$.previewArea.destination!.id);
+      assert(this.destination_!.id === this.$.model.destination!.id);
       this.$.previewArea.startPreview(false);
       this.startPreviewWhenReady_ = false;
     } else {
@@ -382,12 +382,14 @@
       this.remove();
       this.nativeLayer_!.dialogClose(this.cancelled_);
     } else if (this.state === State.PRINT_PENDING) {
+      assert(this.destination_);
       if (this.destination_.type !== PrinterType.PDF_PRINTER) {
         // Only hide the preview for local, non PDF destinations.
         this.nativeLayer_!.hidePreview();
         this.$.state.transitTo(State.HIDDEN);
       }
     } else if (this.state === State.PRINTING) {
+      assert(this.destination_);
       const whenPrintDone =
           this.nativeLayer_!.doPrint(this.$.model.createPrintTicket(
               this.destination_, this.openPdfInPreview_,
diff --git a/chrome/browser/resources/print_preview/ui/button_strip.ts b/chrome/browser/resources/print_preview/ui/button_strip.ts
index 5669f05..de2f8e9 100644
--- a/chrome/browser/resources/print_preview/ui/button_strip.ts
+++ b/chrome/browser/resources/print_preview/ui/button_strip.ts
@@ -42,7 +42,7 @@
     };
   }
 
-  accessor destination: Destination;
+  accessor destination: Destination|null = null;
   accessor firstLoad: boolean = false;
   accessor state: State = State.NOT_READY;
   protected accessor printButtonEnabled_: boolean = false;
@@ -72,7 +72,7 @@
   }
 
   private isPdf_(): boolean {
-    return this.destination &&
+    return !!this.destination &&
         this.destination.type === PrinterType.PDF_PRINTER;
   }
 
diff --git a/chrome/browser/resources/print_preview/ui/destination_dialog.ts b/chrome/browser/resources/print_preview/ui/destination_dialog.ts
index 37bcbad..4b53bfcf 100644
--- a/chrome/browser/resources/print_preview/ui/destination_dialog.ts
+++ b/chrome/browser/resources/print_preview/ui/destination_dialog.ts
@@ -56,7 +56,7 @@
     };
   }
 
-  accessor destinationStore: DestinationStore;
+  accessor destinationStore: DestinationStore|null = null;
   protected accessor loadingDestinations_: boolean = false;
   protected accessor searchQuery_: RegExp|null = null;
 
@@ -93,6 +93,7 @@
 
   private onDestinationStoreSet_() {
     assert(!this.initialized_);
+    assert(this.destinationStore);
     this.tracker_.add(
         this.destinationStore, DestinationStoreEventType.DESTINATIONS_INSERTED,
         this.updateDestinations_.bind(this));
@@ -104,7 +105,7 @@
   }
 
   private updateDestinations_() {
-    if (this.destinationStore === undefined || !this.initialized_) {
+    if (!this.destinationStore || !this.initialized_) {
       return;
     }
 
@@ -132,13 +133,14 @@
   }
 
   private selectDestination_(destination: Destination) {
+    assert(this.destinationStore);
     this.destinationStore.selectDestination(destination);
     this.$.dialog.close();
   }
 
   show() {
     this.$.dialog.showModal();
-    const loading = this.destinationStore === undefined ||
+    const loading = !this.destinationStore ||
         this.destinationStore.isPrintDestinationSearchInProgress;
     if (!loading) {
       // All destinations have already loaded.
diff --git a/chrome/browser/resources/print_preview/ui/destination_select.ts b/chrome/browser/resources/print_preview/ui/destination_select.ts
index c6ecb66e..ee627cc 100644
--- a/chrome/browser/resources/print_preview/ui/destination_select.ts
+++ b/chrome/browser/resources/print_preview/ui/destination_select.ts
@@ -48,7 +48,7 @@
   }
 
   accessor dark: boolean = false;
-  accessor destination: Destination;
+  accessor destination: Destination|null = null;
   accessor disabled: boolean = false;
   accessor loaded: boolean = false;
   accessor noDestinations: boolean = false;
@@ -62,7 +62,7 @@
 
   /** Sets the select to the current value of |destination|. */
   updateDestination() {
-    this.selectedValue = this.destination.key;
+    this.selectedValue = this.destination?.key || '';
   }
 
   /**
diff --git a/chrome/browser/resources/print_preview/ui/header.ts b/chrome/browser/resources/print_preview/ui/header.ts
index dfd2334..db3d721 100644
--- a/chrome/browser/resources/print_preview/ui/header.ts
+++ b/chrome/browser/resources/print_preview/ui/header.ts
@@ -46,7 +46,7 @@
     };
   }
 
-  accessor destination: Destination;
+  accessor destination: Destination|null = null;
   accessor error: Error|null = null;
   accessor state: State = State.NOT_READY;
   accessor managed: boolean = false;
@@ -84,7 +84,7 @@
   }
 
   private isPdf_(): boolean {
-    return this.destination &&
+    return !!this.destination &&
         this.destination.type === PrinterType.PDF_PRINTER;
   }
 
diff --git a/chrome/browser/resources/print_preview/ui/preview_area.ts b/chrome/browser/resources/print_preview/ui/preview_area.ts
index dc3f781..1abcaa3b 100644
--- a/chrome/browser/resources/print_preview/ui/preview_area.ts
+++ b/chrome/browser/resources/print_preview/ui/preview_area.ts
@@ -6,6 +6,7 @@
 
 import {I18nMixinLit} from 'chrome://resources/cr_elements/i18n_mixin_lit.js';
 import {WebUiListenerMixinLit} from 'chrome://resources/cr_elements/web_ui_listener_mixin_lit.js';
+import {assert} from 'chrome://resources/js/assert.js';
 import {hasKeyModifiers} from 'chrome://resources/js/util.js';
 import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
 import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';
@@ -95,7 +96,7 @@
     };
   }
 
-  accessor destination: Destination;
+  accessor destination: Destination|null = null;
   accessor documentModifiable: boolean = false;
   accessor error: Error|null = null;
   accessor margins: Margins|null = null;
@@ -520,6 +521,8 @@
       return true;
     }
 
+    assert(this.destination);
+
     const lastTicket = this.lastTicket_;
 
     // Margins
@@ -611,6 +614,7 @@
 
   /** @return Native color model of the destination. */
   private getColorForTicket_(): number {
+    assert(this.destination);
     return this.destination.getNativeColorModel(
         this.getSettingValue('color') as boolean);
   }
@@ -673,6 +677,7 @@
    *     generated.
    */
   private getPreview_(): Promise<number> {
+    assert(this.destination);
     this.inFlightRequestId_++;
     const ticket: PreviewTicket = {
       pageRange: this.getSettingValue('ranges'),
diff --git a/chrome/browser/resources/side_panel/customize_chrome/appearance.html.ts b/chrome/browser/resources/side_panel/customize_chrome/appearance.html.ts
index fb592736..6cb2cf7c 100644
--- a/chrome/browser/resources/side_panel/customize_chrome/appearance.html.ts
+++ b/chrome/browser/resources/side_panel/customize_chrome/appearance.html.ts
@@ -32,7 +32,7 @@
     label="$i18n{yourSearchedImage}"
     label-description="$i18n{currentTheme}">
 </customize-chrome-hover-button>
-${(!this.isSourceTabFirstPartyNtp_() && !!this.ntpManagedByName_) ? html`
+${this.showManagedButton_ ? html`
   <customize-chrome-hover-button id="thirdPartyManageLinkButton"
       aria-button-label="${this.i18n('newTabPageManagedByA11yLabel',
                            this.ntpManagedByName_)}"
diff --git a/chrome/browser/resources/side_panel/customize_chrome/appearance.ts b/chrome/browser/resources/side_panel/customize_chrome/appearance.ts
index b7cdc39..a27ff3e 100644
--- a/chrome/browser/resources/side_panel/customize_chrome/appearance.ts
+++ b/chrome/browser/resources/side_panel/customize_chrome/appearance.ts
@@ -22,8 +22,8 @@
 import {getCss} from './appearance.css.js';
 import {getHtml} from './appearance.html.js';
 import {CustomizeChromeAction, recordCustomizeChromeAction} from './common.js';
-import type {CustomizeChromePageCallbackRouter, CustomizeChromePageHandlerInterface, Theme} from './customize_chrome.mojom-webui.js';
 import {NewTabPageType} from './customize_chrome.mojom-webui.js';
+import type {CustomizeChromePageCallbackRouter, CustomizeChromePageHandlerInterface, Theme} from './customize_chrome.mojom-webui.js';
 import {CustomizeChromeApiProxy} from './customize_chrome_api_proxy.js';
 
 export interface AppearanceElement {
@@ -77,6 +77,7 @@
       showThemeSnapshot_: {type: Boolean},
       showUploadedImageButton_: {type: Boolean},
       showSearchedImageButton_: {type: Boolean},
+      showManagedButton_: {type: Boolean},
       showManagedDialog_: {type: Boolean},
       showEditTheme_: {type: Boolean},
       newTabPageType_: {type: NewTabPageType},
@@ -87,6 +88,7 @@
       },
 
       wallpaperSearchEnabled_: {type: Boolean},
+      footerEnabled_: {type: Boolean},
     };
   }
 
@@ -101,11 +103,14 @@
   protected accessor showThemeSnapshot_: boolean = false;
   protected accessor showUploadedImageButton_: boolean = false;
   protected accessor showSearchedImageButton_: boolean = false;
+  protected accessor showManagedButton_: boolean = false;
   protected accessor showManagedDialog_: boolean = false;
   protected accessor wallpaperSearchButtonEnabled_: boolean =
       loadTimeData.getBoolean('wallpaperSearchButtonEnabled');
   private accessor wallpaperSearchEnabled_: boolean =
       loadTimeData.getBoolean('wallpaperSearchEnabled');
+  private accessor footerEnabled_: boolean =
+      loadTimeData.getBoolean('footerEnabled');
   protected accessor newTabPageType_: NewTabPageType =
       NewTabPageType.kFirstPartyWebUI;
   protected accessor showEditTheme_: boolean = true;
@@ -189,6 +194,7 @@
       this.showThemeSnapshot_ = this.computeShowThemeSnapshot_();
       this.showUploadedImageButton_ = this.computeShowUploadedImageButton_();
       this.showSearchedImageButton_ = this.computeShowSearchedImageButton_();
+      this.showManagedButton_ = this.computeShowManagedButton_();
     }
 
     this.showBottomDivider_ = this.computeShowBottomDivider_();
@@ -241,6 +247,12 @@
   }
 
   private computeShowClassicChromeButton_(): boolean {
+    if (this.footerEnabled_) {
+      return !!(
+          this.theme_ && this.theme_.backgroundImage &&
+          (this.newTabPageType_ === NewTabPageType.kFirstPartyWebUI ||
+           this.newTabPageType_ === NewTabPageType.kThirdPartyWebUI));
+    }
     return !!(
         this.theme_ &&
         (this.theme_.backgroundImage || this.theme_.thirdPartyThemeInfo));
@@ -261,7 +273,7 @@
            this.theme_.backgroundImage.isUploadedImage)) &&
         // TODO(crbug.com/404247286) Enable snapshots for extension NTP with 1P
         // theme.
-        this.isSourceTabFirstPartyNtp_();
+        this.newTabPageType_ === NewTabPageType.kFirstPartyWebUI;
   }
 
   private computeShowUploadedImageButton_(): boolean {
@@ -277,8 +289,9 @@
         this.theme_.backgroundImage.localBackgroundId);
   }
 
-  protected isSourceTabFirstPartyNtp_(): boolean {
-    return this.newTabPageType_ === NewTabPageType.kFirstPartyWebUI;
+  private computeShowManagedButton_(): boolean {
+    return this.newTabPageType_ !== NewTabPageType.kFirstPartyWebUI &&
+        !!this.ntpManagedByName_;
   }
 
   protected onEditThemeClicked_() {
diff --git a/chrome/browser/resources/side_panel/read_anything/app.ts b/chrome/browser/resources/side_panel/read_anything/app.ts
index ecb3566..a0cd1a3 100644
--- a/chrome/browser/resources/side_panel/read_anything/app.ts
+++ b/chrome/browser/resources/side_panel/read_anything/app.ts
@@ -692,7 +692,7 @@
   }
 
   protected updateImages_() {
-    if (!this.shadowRoot) {
+    if (!this.shadowRoot || !chrome.readingMode.imagesFeatureEnabled) {
       return;
     }
 
diff --git a/chrome/browser/safe_browsing/safe_browsing_navigation_observer_interactive_test.cc b/chrome/browser/safe_browsing/safe_browsing_navigation_observer_interactive_test.cc
index 2ca9030..09825157 100644
--- a/chrome/browser/safe_browsing/safe_browsing_navigation_observer_interactive_test.cc
+++ b/chrome/browser/safe_browsing/safe_browsing_navigation_observer_interactive_test.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "base/functional/callback.h"
+#include "base/strings/stringprintf.h"
 #include "build/build_config.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/safe_browsing/test_safe_browsing_navigation_observer_manager.h"
diff --git a/chrome/browser/ssl/https_upgrades_browsertest.cc b/chrome/browser/ssl/https_upgrades_browsertest.cc
index 1f6ffc4f..522dcbd 100644
--- a/chrome/browser/ssl/https_upgrades_browsertest.cc
+++ b/chrome/browser/ssl/https_upgrades_browsertest.cc
@@ -8,6 +8,7 @@
 
 #include "base/memory/raw_ptr.h"
 #include "base/strings/strcat.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/simple_test_clock.h"
diff --git a/chrome/browser/supervised_user/kids_profile_interactive_uitest.cc b/chrome/browser/supervised_user/kids_profile_interactive_uitest.cc
index 223480e3..a4eec88 100644
--- a/chrome/browser/supervised_user/kids_profile_interactive_uitest.cc
+++ b/chrome/browser/supervised_user/kids_profile_interactive_uitest.cc
@@ -45,11 +45,12 @@
                                             std::u16string_view tab_title,
                                             std::string_view iframe_name) {
   content::WebContents* web_contents = nullptr;
-  for (int i = 0; i < browser.GetTabStripModel()->GetTabCount(); ++i) {
-    std::u16string wc_title =
-        TabUIHelper::FromWebContents(
-            browser.GetTabStripModel()->GetWebContentsAt(i))
-            ->GetTitle();
+  TabStripModel* const tab_strip_model = browser.tab_strip_model();
+  for (int i = 0; i < tab_strip_model->GetTabCount(); ++i) {
+    const std::u16string wc_title = tab_strip_model->GetTabAtIndex(i)
+                                        ->GetTabFeatures()
+                                        ->tab_ui_helper()
+                                        ->GetTitle();
     if (wc_title == tab_title) {
       web_contents = browser.GetTabStripModel()->GetWebContentsAt(i);
       break;
diff --git a/chrome/browser/trusted_vault/trusted_vault_encryption_keys_tab_helper_browsertest.cc b/chrome/browser/trusted_vault/trusted_vault_encryption_keys_tab_helper_browsertest.cc
index a983798..1cae61c 100644
--- a/chrome/browser/trusted_vault/trusted_vault_encryption_keys_tab_helper_browsertest.cc
+++ b/chrome/browser/trusted_vault/trusted_vault_encryption_keys_tab_helper_browsertest.cc
@@ -9,6 +9,7 @@
 
 #include "base/feature_list.h"
 #include "base/path_service.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "build/build_config.h"
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 2e783f8..cc5375c7 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -1873,6 +1873,7 @@
       "//chrome/browser/ui/toasts:impl",
       "//chrome/browser/ui/views/toolbar",
       "//chrome/browser/ui/views/page_action",
+      "//chrome/browser/ui/views/new_tab_footer",
       "//chrome/browser/ui/webui/searchbox",
       "//chrome/browser/ui/webui/top_chrome:impl",
       "//chrome/browser/ui/search:impl",
@@ -4150,6 +4151,8 @@
       "views/location_bar/intent_chip_button.h",
       "views/location_bar/intent_picker_view.cc",
       "views/location_bar/intent_picker_view.h",
+      "views/location_bar/lens_overlay_homework_page_action_icon_view.cc",
+      "views/location_bar/lens_overlay_homework_page_action_icon_view.h",
       "views/location_bar/lens_overlay_page_action_icon_view.cc",
       "views/location_bar/lens_overlay_page_action_icon_view.h",
       "views/location_bar/location_bar_bubble_delegate_view.cc",
diff --git a/chrome/browser/ui/android/toolbar/java/res/layout/toolbar_tablet.xml b/chrome/browser/ui/android/toolbar/java/res/layout/toolbar_tablet.xml
index 633a77eae..86f199c 100644
--- a/chrome/browser/ui/android/toolbar/java/res/layout/toolbar_tablet.xml
+++ b/chrome/browser/ui/android/toolbar/java/res/layout/toolbar_tablet.xml
@@ -90,6 +90,13 @@
             android:layout_marginHorizontal="@dimen/incognito_indicator_lateral_margin"
             android:visibility="gone" />
 
+        <ViewStub
+            android:id="@+id/extension_toolbar_container_stub"
+            android:inflatedId="@+id/extension_toolbar_container"
+            android:layout="@layout/extension_toolbar_container"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent" />
+
         <org.chromium.chrome.browser.toolbar.top.ToggleTabStackButton
             android:id="@+id/tab_switcher_button"
             style="@style/ToolbarHoverableButton"
@@ -98,13 +105,6 @@
             app:menuMaxWidth="@dimen/tab_switcher_menu_width"
             app:menuVerticalOverlapAnchor="false" />
 
-        <ViewStub
-            android:id="@+id/extension_toolbar_container_stub"
-            android:inflatedId="@+id/extension_toolbar_container"
-            android:layout="@layout/extension_toolbar_container"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent" />
-
         <include layout="@layout/menu_button"/>
     </LinearLayout>
 </org.chromium.chrome.browser.toolbar.top.ToolbarTablet>
diff --git a/chrome/browser/ui/browser.cc b/chrome/browser/ui/browser.cc
index 5bcef58..8c5e577e 100644
--- a/chrome/browser/ui/browser.cc
+++ b/chrome/browser/ui/browser.cc
@@ -1277,10 +1277,6 @@
   return window_->GetLensOverlayView();
 }
 
-new_tab_footer::NewTabFooterWebView* Browser::NewTabFooterWebView() {
-  return GetBrowserView().new_tab_footer_web_view();
-}
-
 base::CallbackListSubscription Browser::RegisterActiveTabDidChange(
     ActiveTabChangeCallback callback) {
   return did_active_tab_change_callback_list_.Add(std::move(callback));
diff --git a/chrome/browser/ui/browser.h b/chrome/browser/ui/browser.h
index 553762b..1de7f95 100644
--- a/chrome/browser/ui/browser.h
+++ b/chrome/browser/ui/browser.h
@@ -129,10 +129,6 @@
 class View;
 }
 
-namespace new_tab_footer {
-class NewTabFooterWebView;
-}  // namespace new_tab_footer
-
 // This enum is not a member of `Browser` so that it can be forward
 // declared in `unload_controller.h` to avoid circular includes.
 enum class BrowserClosingStatus {
@@ -875,7 +871,6 @@
   bool IsVisible() const override;
   base::WeakPtr<BrowserWindowInterface> GetWeakPtr() override;
   views::View* LensOverlayView() override;
-  new_tab_footer::NewTabFooterWebView* NewTabFooterWebView() override;
   base::CallbackListSubscription RegisterActiveTabDidChange(
       ActiveTabChangeCallback callback) override;
   tabs::TabInterface* GetActiveTabInterface() override;
diff --git a/chrome/browser/ui/browser_element_identifiers.cc b/chrome/browser/ui/browser_element_identifiers.cc
index 50a7d10..f8afae0e 100644
--- a/chrome/browser/ui/browser_element_identifiers.cc
+++ b/chrome/browser/ui/browser_element_identifiers.cc
@@ -52,6 +52,7 @@
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kInstallPwaElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kIntentChipElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kLeftAlignedSidePanelSeparatorViewElementId);
+DEFINE_ELEMENT_IDENTIFIER_VALUE(kLensOverlayHomeworkPageActionIconElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kLensOverlayPageActionIconElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kLensOverlayTranslateButtonElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kLensPermissionDialogCancelButtonElementId);
@@ -61,6 +62,7 @@
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kLensSidePanelSearchBoxElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kLocalWebParentApprovalDialogId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kLocalWebParentApprovalDialogErrorId);
+DEFINE_ELEMENT_IDENTIFIER_VALUE(kLocationBarElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kLocationIconElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kNewTabButtonElementId);
 DEFINE_ELEMENT_IDENTIFIER_VALUE(kOfferNotificationChipElementId);
diff --git a/chrome/browser/ui/browser_element_identifiers.h b/chrome/browser/ui/browser_element_identifiers.h
index 64d7524..ecfa5f7 100644
--- a/chrome/browser/ui/browser_element_identifiers.h
+++ b/chrome/browser/ui/browser_element_identifiers.h
@@ -61,6 +61,7 @@
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kInstallPwaElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kIntentChipElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kLeftAlignedSidePanelSeparatorViewElementId);
+DECLARE_ELEMENT_IDENTIFIER_VALUE(kLensOverlayHomeworkPageActionIconElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kLensOverlayPageActionIconElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kLensOverlayTranslateButtonElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kLensPermissionDialogCancelButtonElementId);
@@ -70,6 +71,7 @@
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kLensSidePanelSearchBoxElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kLocalWebParentApprovalDialogId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kLocalWebParentApprovalDialogErrorId);
+DECLARE_ELEMENT_IDENTIFIER_VALUE(kLocationBarElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kLocationIconElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kNewTabButtonElementId);
 DECLARE_ELEMENT_IDENTIFIER_VALUE(kOfferNotificationChipElementId);
diff --git a/chrome/browser/ui/browser_tabrestore.cc b/chrome/browser/ui/browser_tabrestore.cc
index a1ca3719..6ba7198 100644
--- a/chrome/browser/ui/browser_tabrestore.cc
+++ b/chrome/browser/ui/browser_tabrestore.cc
@@ -18,12 +18,14 @@
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_window.h"
 #include "chrome/browser/ui/tab_ui_helper.h"
+#include "chrome/browser/ui/tabs/public/tab_features.h"
 #include "chrome/browser/ui/tabs/tab_enums.h"
 #include "chrome/browser/ui/tabs/tab_group_model.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "components/sessions/content/content_serialized_navigation_builder.h"
 #include "components/tab_groups/tab_group_id.h"
 #include "components/tabs/public/tab_group.h"
+#include "components/tabs/public/tab_interface.h"
 #include "content/public/browser/navigation_entry.h"
 #include "content/public/browser/render_widget_host_view.h"
 #include "content/public/browser/restore_type.h"
@@ -75,15 +77,6 @@
   std::unique_ptr<WebContents> web_contents =
       WebContents::CreateWithSessionStorage(create_params,
                                             session_storage_namespace_map);
-  if (from_session_restore) {
-    // Indicate that the tab is created by session restore. This is used to hide
-    // the throbber when a background restored tab is loading. TabUIHelper is
-    // created by TabHelpers::AttachTabHelpers, but this happens later, so we
-    // explicitly create it early here.
-    TabUIHelper::CreateForWebContents(web_contents.get());
-    TabUIHelper::FromWebContents(web_contents.get())
-        ->set_created_by_session_restore(true);
-  }
   apps::SetAppIdForWebContents(browser->profile(), web_contents.get(),
                                extension_app_id);
 
@@ -187,6 +180,15 @@
                                          add_types, group);
   }
 
+  if (from_session_restore) {
+    // Indicate that the tab is created by session restore. This is used to hide
+    // the throbber when a background restored tab is loading.
+    tabs::TabInterface* const tab_interface =
+        tabs::TabInterface::GetFromContents(raw_web_contents);
+    tabs::TabFeatures* const tab_features = tab_interface->GetTabFeatures();
+    tab_features->tab_ui_helper()->set_created_by_session_restore(true);
+  }
+
   // We set the size of the view here, before Blink does its initial layout.
   // If we don't, the initial layout of background tabs will be performed
   // with a view width of 0, which may cause script outputs and anchor link
@@ -303,6 +305,16 @@
       insertion_index + 1, std::move(web_contents),
       AddTabTypes::ADD_ACTIVE | AddTabTypes::ADD_INHERIT_OPENER,
       tab_strip->GetTabGroupForTab(insertion_index));
+
+  if (from_session_restore) {
+    // Indicate that the tab is created by session restore. This is used to hide
+    // the throbber when a background restored tab is loading.
+    tabs::TabInterface* const tab_interface =
+        tabs::TabInterface::GetFromContents(raw_web_contents);
+    tabs::TabFeatures* const tab_features = tab_interface->GetTabFeatures();
+    tab_features->tab_ui_helper()->set_created_by_session_restore(true);
+  }
+
   tab_strip->CloseWebContentsAt(insertion_index, TabCloseTypes::CLOSE_NONE);
 
   LoadRestoredTabIfVisible(browser, raw_web_contents);
diff --git a/chrome/browser/ui/browser_window/BUILD.gn b/chrome/browser/ui/browser_window/BUILD.gn
index 422ba98..bcd1ef7 100644
--- a/chrome/browser/ui/browser_window/BUILD.gn
+++ b/chrome/browser/ui/browser_window/BUILD.gn
@@ -41,6 +41,7 @@
     "//chrome/browser/ui/tabs/tab_strip_api",
     "//chrome/browser/ui/toasts",
     "//chrome/browser/ui/views/download",
+    "//chrome/browser/ui/views/new_tab_footer",
     "//chrome/browser/ui/views/side_panel",
     "//chrome/browser/ui/views/toolbar",
     "//components/collaboration/public:public",
diff --git a/chrome/browser/ui/browser_window/browser_window_features.cc b/chrome/browser/ui/browser_window/browser_window_features.cc
index bfd41dbe..4dce6d3 100644
--- a/chrome/browser/ui/browser_window/browser_window_features.cc
+++ b/chrome/browser/ui/browser_window/browser_window_features.cc
@@ -48,6 +48,7 @@
 #include "chrome/browser/ui/views/location_bar/cookie_controls/cookie_controls_bubble_coordinator.h"
 #include "chrome/browser/ui/views/location_bar/location_bar_view.h"
 #include "chrome/browser/ui/views/media_router/cast_browser_controller.h"
+#include "chrome/browser/ui/views/new_tab_footer/footer_controller.h"
 #include "chrome/browser/ui/views/send_tab_to_self/send_tab_to_self_toolbar_bubble_controller.h"
 #include "chrome/browser/ui/views/side_panel/bookmarks/bookmarks_side_panel_coordinator.h"
 #include "chrome/browser/ui/views/side_panel/extensions/extension_side_panel_manager.h"
@@ -65,6 +66,7 @@
 #include "components/lens/lens_features.h"
 #include "components/profile_metrics/browser_profile_type.h"
 #include "components/saved_tab_groups/public/features.h"
+#include "components/search/ntp_features.h"
 #include "components/search/search.h"
 
 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
@@ -324,6 +326,12 @@
         std::make_unique<tab_groups::SharedTabGroupFeedbackController>(
             browser_view);
   }
+
+  if (base::FeatureList::IsEnabled(ntp_features::kNtpFooter)) {
+    new_tab_footer_controller_ =
+        std::make_unique<new_tab_footer::NewTabFooterController>(
+            browser_view->browser(), browser_view->new_tab_footer_web_view());
+  }
 }
 
 void BrowserWindowFeatures::TearDownPreBrowserViewDestruction() {
@@ -356,6 +364,10 @@
   if (chrome_labs_coordinator_) {
     chrome_labs_coordinator_->TearDown();
   }
+
+  if (new_tab_footer_controller_) {
+    new_tab_footer_controller_->TearDown();
+  }
 }
 
 SidePanelUI* BrowserWindowFeatures::side_panel_ui() {
diff --git a/chrome/browser/ui/browser_window/public/browser_window_features.h b/chrome/browser/ui/browser_window/public/browser_window_features.h
index 8f3ab47..f72f878 100644
--- a/chrome/browser/ui/browser_window/public/browser_window_features.h
+++ b/chrome/browser/ui/browser_window/public/browser_window_features.h
@@ -71,6 +71,10 @@
 class MemorySaverBubbleController;
 }  // namespace memory_saver
 
+namespace new_tab_footer {
+class NewTabFooterController;
+}  // namespace new_tab_footer
+
 namespace tab_groups {
 class SessionServiceTabGroupSyncObserver;
 class SharedTabGroupFeedbackController;
@@ -234,6 +238,10 @@
     return tab_strip_service_.get();
   }
 
+  new_tab_footer::NewTabFooterController* new_tab_footer_controller() {
+    return new_tab_footer_controller_.get();
+  }
+
  protected:
   BrowserWindowFeatures();
 
@@ -321,6 +329,9 @@
 
   std::unique_ptr<TabMenuModelDelegate> tab_menu_model_delegate_;
 
+  std::unique_ptr<new_tab_footer::NewTabFooterController>
+      new_tab_footer_controller_;
+
   // This is an experimental API that interacts with the TabStripModel.
   std::unique_ptr<TabStripServiceRegister> tab_strip_service_;
 };
diff --git a/chrome/browser/ui/browser_window/public/browser_window_interface.h b/chrome/browser/ui/browser_window/public/browser_window_interface.h
index 8fda121..fbb7152 100644
--- a/chrome/browser/ui/browser_window/public/browser_window_interface.h
+++ b/chrome/browser/ui/browser_window/public/browser_window_interface.h
@@ -18,10 +18,6 @@
 // your feature needs. This comment will be deleted after there are 10+ features
 // in BrowserWindowFeatures.
 
-namespace new_tab_footer {
-class NewTabFooterWebView;
-}  // namespace new_tab_footer
-
 namespace tabs {
 class TabInterface;
 }  // namespace tabs
@@ -117,9 +113,6 @@
   // Returns the view that houses the Lens overlay.
   virtual views::View* LensOverlayView() = 0;
 
-  // Returns the view that houses the New Tab Footer.
-  virtual new_tab_footer::NewTabFooterWebView* NewTabFooterWebView() = 0;
-
   using ActiveTabChangeCallback =
       base::RepeatingCallback<void(BrowserWindowInterface*)>;
   virtual base::CallbackListSubscription RegisterActiveTabDidChange(
diff --git a/chrome/browser/ui/browser_window/test/mock_browser_window_interface.h b/chrome/browser/ui/browser_window/test/mock_browser_window_interface.h
index 1b52a92..0240ee6 100644
--- a/chrome/browser/ui/browser_window/test/mock_browser_window_interface.h
+++ b/chrome/browser/ui/browser_window/test/mock_browser_window_interface.h
@@ -37,10 +37,6 @@
               (),
               (override));
   MOCK_METHOD(views::View*, LensOverlayView, (), (override));
-  MOCK_METHOD(new_tab_footer::NewTabFooterWebView*,
-              NewTabFooterWebView,
-              (),
-              (override));
   MOCK_METHOD(base::CallbackListSubscription,
               RegisterActiveTabDidChange,
               (ActiveTabChangeCallback callback),
diff --git a/chrome/browser/ui/cocoa/tab_menu_bridge.mm b/chrome/browser/ui/cocoa/tab_menu_bridge.mm
index b185534..5660c0f7 100644
--- a/chrome/browser/ui/cocoa/tab_menu_bridge.mm
+++ b/chrome/browser/ui/cocoa/tab_menu_bridge.mm
@@ -10,11 +10,14 @@
 #include "base/strings/sys_string_conversions.h"
 #include "chrome/browser/ui/recently_audible_helper.h"
 #include "chrome/browser/ui/tab_ui_helper.h"
+#include "chrome/browser/ui/tabs/public/tab_features.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/tabs/tab_strip_user_gesture_details.h"
 #include "chrome/grit/generated_resources.h"
+#include "components/tabs/public/tab_interface.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/base/l10n/l10n_util_mac.h"
+#include "ui/base/models/image_model.h"
 #include "ui/gfx/image/image_skia_util_mac.h"
 
 using MenuItemCallback = base::RepeatingCallback<void(NSMenuItem*)>;
@@ -23,7 +26,10 @@
 
 void UpdateItemForWebContents(NSMenuItem* item,
                               content::WebContents* web_contents) {
-  TabUIHelper* tab_ui_helper = TabUIHelper::FromWebContents(web_contents);
+  tabs::TabInterface* const tab_interface =
+      tabs::TabInterface::GetFromContents(web_contents);
+  TabUIHelper* const tab_ui_helper =
+      tab_interface->GetTabFeatures()->tab_ui_helper();
 
   auto* audio_helper = RecentlyAudibleHelper::FromWebContents(web_contents);
   if (audio_helper && audio_helper->WasRecentlyAudible()) {
diff --git a/chrome/browser/ui/cocoa/tab_menu_bridge_unittest.mm b/chrome/browser/ui/cocoa/tab_menu_bridge_unittest.mm
index c07988d..c62a75f 100644
--- a/chrome/browser/ui/cocoa/tab_menu_bridge_unittest.mm
+++ b/chrome/browser/ui/cocoa/tab_menu_bridge_unittest.mm
@@ -6,21 +6,27 @@
 
 #import <Cocoa/Cocoa.h>
 
+#include <memory>
+
+#include "base/functional/callback_helpers.h"
 #include "base/strings/sys_string_conversions.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/task_environment.h"
 #include "chrome/browser/favicon/favicon_utils.h"
 #include "chrome/browser/ui/tab_ui_helper.h"
+#include "chrome/browser/ui/tabs/public/tab_features.h"
 #include "chrome/browser/ui/tabs/tab_enums.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/tabs/test_tab_strip_model_delegate.h"
 #include "chrome/browser/ui/tabs/test_util.h"
 #include "chrome/test/base/testing_profile.h"
+#include "components/tabs/public/tab_interface.h"
 #include "content/public/test/test_renderer_host.h"
 #include "content/public/test/web_contents_tester.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "testing/gtest_mac.h"
 
+namespace {
 constexpr int kStaticItemCount = 4;
 
 class TabStripModelUiHelperDelegate : public TestTabStripModelDelegate {
@@ -29,10 +35,11 @@
     TestTabStripModelDelegate::WillAddWebContents(contents);
 
     favicon::CreateContentFaviconDriverForWebContents(contents);
-    TabUIHelper::CreateForWebContents(contents);
   }
 };
 
+}  // namespace
+
 class TabMenuBridgeTest : public ::testing::Test {
  public:
   void SetUp() override {
@@ -75,8 +82,15 @@
     return model()->GetWebContentsAt(index);
   }
 
-  void AddModelTabNamed(const std::string& name) {
-    model()->AppendWebContents(CreateWebContents(name), true);
+  void AddModelTabNamed(const std::string& name,
+                        TabStripModel* tab_strip_model) {
+    std::unique_ptr<tabs::TabModel> tab_model =
+        std::make_unique<tabs::TabModel>(CreateWebContents(name),
+                                         tab_strip_model);
+    tabs::TabFeatures* const tab_features = tab_model->GetTabFeatures();
+    tab_features->SetTabUIHelperForTesting(
+        std::make_unique<TabUIHelper>(*tab_model));
+    tab_strip_model->AppendTab(std::move(tab_model), true);
   }
 
   int ModelIndexForTabNamed(const std::string& name) {
@@ -190,18 +204,19 @@
 }
 
 TEST_F(TabMenuBridgeTest, TracksModelUpdates) {
+  TabStripModel* const tab_strip_model = model();
   TabMenuBridge bridge(model(), menu_root());
   bridge.BuildMenu();
 
-  AddModelTabNamed("Tab 1");
-  AddModelTabNamed("Tab 2");
-  AddModelTabNamed("Tab 3");
+  AddModelTabNamed("Tab 1", tab_strip_model);
+  AddModelTabNamed("Tab 2", tab_strip_model);
+  AddModelTabNamed("Tab 3", tab_strip_model);
   ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 2", "Tab 3"});
 
   RemoveModelTabNamed("Tab 2");
   ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 3"});
 
-  AddModelTabNamed("Tab 2");
+  AddModelTabNamed("Tab 2", tab_strip_model);
   ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 3", "Tab 2"});
 
   RenameModelTabNamed("Tab 1", "Tab 4");
@@ -216,13 +231,14 @@
 // Tests that dynamic menu items added by the bridge are removed on
 // bridge destruction.
 TEST_F(TabMenuBridgeTest, RemoveDynamicMenuItemsOnDestruct) {
+  TabStripModel* const tab_strip_model = model();
   std::unique_ptr<TabMenuBridge> bridge =
-      std::make_unique<TabMenuBridge>(model(), menu_root());
+      std::make_unique<TabMenuBridge>(tab_strip_model, menu_root());
   bridge->BuildMenu();
 
-  AddModelTabNamed("Tab 1");
-  AddModelTabNamed("Tab 2");
-  AddModelTabNamed("Tab 3");
+  AddModelTabNamed("Tab 1", tab_strip_model);
+  AddModelTabNamed("Tab 2", tab_strip_model);
+  AddModelTabNamed("Tab 3", tab_strip_model);
   ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 2", "Tab 3"});
 
   bridge.reset();
@@ -231,11 +247,12 @@
 }
 
 TEST_F(TabMenuBridgeTest, ClickingMenuActivatesTab) {
-  TabMenuBridge bridge(model(), menu_root());
+  TabStripModel* const tab_strip_model = model();
+  TabMenuBridge bridge(tab_strip_model, menu_root());
   bridge.BuildMenu();
 
-  AddModelTabNamed("Tab 1");
-  AddModelTabNamed("Tab 2");
+  AddModelTabNamed("Tab 1", tab_strip_model);
+  AddModelTabNamed("Tab 2", tab_strip_model);
   EXPECT_EQ(ActiveTabName(), "Tab 2");
 
   NSMenuItem* tab1_item = MenuItemForTabNamed("Tab 1");
@@ -262,13 +279,14 @@
 // static items, and ended up with incorrect indexes. This test exercises that
 // behavior.
 TEST_F(TabMenuBridgeTest, SwappingBridgeRecreatesMenu) {
-  auto bridge = std::make_unique<TabMenuBridge>(model(), menu_root());
+  TabStripModel* const tab_strip_model = model();
+  auto bridge = std::make_unique<TabMenuBridge>(tab_strip_model, menu_root());
   bridge->BuildMenu();
 
-  AddModelTabNamed("Tab 1");
+  AddModelTabNamed("Tab 1", tab_strip_model);
 
   auto model2 = std::make_unique<TabStripModel>(delegate(), profile());
-  model2->AppendWebContents(CreateWebContents("Tab 2"), true);
+  AddModelTabNamed("Tab 2", model2.get());
 
   bridge = std::make_unique<TabMenuBridge>(model2.get(), menu_root());
   bridge->BuildMenu();
@@ -288,12 +306,13 @@
 }
 
 TEST_F(TabMenuBridgeTest, ActiveItemTracksChanges) {
-  TabMenuBridge bridge(model(), menu_root());
+  TabStripModel* const tab_strip_model = model();
+  TabMenuBridge bridge(tab_strip_model, menu_root());
   bridge.BuildMenu();
 
-  AddModelTabNamed("Tab 1");
-  AddModelTabNamed("Tab 2");
-  AddModelTabNamed("Tab 3");
+  AddModelTabNamed("Tab 1", tab_strip_model);
+  AddModelTabNamed("Tab 2", tab_strip_model);
+  AddModelTabNamed("Tab 3", tab_strip_model);
   ExpectActiveMenuItemNameIs("Tab 3");
 
   ActivateModelTabNamed("Tab 2");
diff --git a/chrome/browser/ui/lens/lens_overlay_controller.cc b/chrome/browser/ui/lens/lens_overlay_controller.cc
index f231058..bed44d0 100644
--- a/chrome/browser/ui/lens/lens_overlay_controller.cc
+++ b/chrome/browser/ui/lens/lens_overlay_controller.cc
@@ -135,11 +135,6 @@
 // taking a screenshot.
 constexpr base::TimeDelta kReflowWaitTimeout = base::Milliseconds(200);
 
-// The amount of change in bytes that is considered a significant change and
-// should trigger a page content update request. This provides tolerance in
-// case there is slight variation in the retrievied bytes in between calls.
-constexpr float kByteChangeTolerancePercent = 0.01;
-
 // The maximum length of the DOM text to consider for OCR similarity.
 // Currently 50 MB
 constexpr int kMaxDomTextLengthForOcrSimilarity = 50 * 1000 * 1000;
@@ -460,7 +455,6 @@
   }
 
   lens_selection_type_ = lens::UNKNOWN_SELECTION_TYPE;
-  should_show_overlay_ = true;
   is_page_context_eligible_ = true;
   should_send_screenshot_on_init_ = false;
 
@@ -1024,9 +1018,10 @@
     CHECK(lens_overlay_query_controller_);
     GetContextualizationController()->StartContextualization(
         invocation_source,
-        base::BindOnce(&LensOverlayController::OnPageContextUpdated,
-                       weak_factory_.GetWeakPtr(), destination_url, match_type,
-                       is_zero_prefix_suggestion, invocation_source));
+        base::BindOnce(
+            &LensOverlayController::OnPageContextUpdatedForSuggestion,
+            weak_factory_.GetWeakPtr(), destination_url, match_type,
+            is_zero_prefix_suggestion, invocation_source));
     return;
   }
 
@@ -1045,9 +1040,10 @@
     // search request.
     CHECK(lens_overlay_query_controller_);
     GetContextualizationController()->TryUpdatePageContextualization(
-        base::BindOnce(&LensOverlayController::OnPageContextUpdated,
-                       weak_factory_.GetWeakPtr(), destination_url, match_type,
-                       is_zero_prefix_suggestion, invocation_source));
+        base::BindOnce(
+            &LensOverlayController::OnPageContextUpdatedForSuggestion,
+            weak_factory_.GetWeakPtr(), destination_url, match_type,
+            is_zero_prefix_suggestion, invocation_source));
     return;
   }
 
@@ -1058,13 +1054,6 @@
       destination_url, match_type, is_zero_prefix_suggestion);
 }
 
-void LensOverlayController::StartContextualizationWithoutOverlay(
-    lens::LensOverlayInvocationSource invocation_source,
-    lens::LensOverlayQueryController* lens_overlay_query_controller) {
-  should_show_overlay_ = false;
-  ShowUI(invocation_source, lens_overlay_query_controller);
-}
-
 void LensOverlayController::ShowUIWithPendingRegion(
     lens::LensOverlayQueryController* lens_overlay_query_controller,
     lens::LensOverlayInvocationSource invocation_source,
@@ -1170,7 +1159,10 @@
       // If the live page is showing and the searchbox becomes focused, showing
       // intent to issue a new query, upload the new page content for
       // contextualization.
-      TryUpdatePageContextualization();
+      // TODO(crbug.com/418856988): Replace this with a call that starts
+      // contextualization without the unneeded callback.
+      GetContextualizationController()->TryUpdatePageContextualization(
+          base::DoNothing());
     }
   }
 }
@@ -1255,13 +1247,10 @@
 
   // If contextual searchbox is enabled, make sure the page bytes are current
   // prior to issuing the search box request.
-  GetContextualizationController()->GetPageContextualization(
-      base::BindOnce(&LensOverlayController::UpdatePageContextualization,
-                     weak_factory_.GetWeakPtr())
-          .Then(base::BindOnce(
-              &LensOverlayController::IssueSearchBoxRequestPart2,
-              weak_factory_.GetWeakPtr(), query_start_time, search_box_text,
-              match_type, is_zero_prefix_suggestion, additional_query_params)));
+  GetContextualizationController()->TryUpdatePageContextualization(
+      base::BindOnce(&LensOverlayController::IssueSearchBoxRequestPart2,
+                     weak_factory_.GetWeakPtr(), query_start_time, search_box_text, match_type,
+                     is_zero_prefix_suggestion, additional_query_params));
 }
 
 void LensOverlayController::IssueContextualTextRequest(
@@ -1269,6 +1258,7 @@
     const std::string& text_query,
     lens::LensOverlaySelectionType selection_type) {
   if (is_page_context_eligible_) {
+    lens_selection_type_ = selection_type;
     lens_overlay_query_controller_->SendContextualTextQuery(
         query_start_time, text_query, selection_type,
         initialization_data_->additional_search_query_params_);
@@ -1582,8 +1572,6 @@
   initialization_data->primary_content_type_ = primary_content_type;
   initialization_data->pdf_page_count_ = page_count;
   InitializeOverlay(std::move(initialization_data));
-
-  RecordDocumentMetrics(page_count);
 }
 
 std::vector<lens::mojom::CenterRotatedBoxPtr>
@@ -1628,195 +1616,6 @@
   return significant_region_boxes;
 }
 
-void LensOverlayController::TryUpdatePageContextualization() {
-  // If there is already an upload, do not send another request.
-  // TODO(crbug.com/399154548): Ideally, there could be two uploads in progress
-  // at a time, however, the current query controller implementation does not
-  // support this.
-  if (lens_overlay_query_controller_->IsPageContentUploadInProgress()) {
-    return;
-  }
-
-  GetContextualizationController()->GetPageContextualization(
-      base::BindOnce(&LensOverlayController::UpdatePageContextualization,
-                     weak_factory_.GetWeakPtr()));
-}
-
-void LensOverlayController::UpdatePageContextualization(
-    std::vector<lens::PageContent> page_contents,
-    lens::MimeType primary_content_type,
-    std::optional<uint32_t> page_count) {
-  if (!lens::features::IsLensOverlayContextualSearchboxEnabled()) {
-    return;
-  }
-
-  // If the protected page is showing, then return early as none of the content
-  // will be sent.
-  if (results_side_panel_coordinator_->IsShowingProtectedErrorPage()) {
-    return;
-  }
-
-  // Do not capture a new screenshot if the feature param is not enabled or if
-  // the user is not viewing the live page, meaning the viewport cannot have
-  // changed.
-  if (!lens::features::UpdateViewportEachQueryEnabled() ||
-      state_ != State::kLivePageAndResults) {
-    UpdatePageContextualizationPart2(page_contents, primary_content_type,
-                                     page_count, SkBitmap());
-    return;
-  }
-
-  // Begin the process of grabbing a screenshot.
-  content::RenderWidgetHostView* view = tab_->GetContents()
-                                            ->GetPrimaryMainFrame()
-                                            ->GetRenderViewHost()
-                                            ->GetWidget()
-                                            ->GetView();
-  if (!IsScreenshotPossible(view)) {
-    UpdatePageContextualizationPart2(page_contents, primary_content_type,
-                                     page_count, SkBitmap());
-    return;
-  }
-  view->CopyFromSurface(
-      /*src_rect=*/gfx::Rect(), /*output_size=*/gfx::Size(),
-      base::BindPostTask(
-          base::SequencedTaskRunner::GetCurrentDefault(),
-          base::BindOnce(
-              &LensOverlayController::UpdatePageContextualizationPart2,
-              weak_factory_.GetWeakPtr(), page_contents, primary_content_type,
-              page_count)));
-}
-
-void LensOverlayController::UpdatePageContextualizationPart2(
-    std::vector<lens::PageContent> page_contents,
-    lens::MimeType primary_content_type,
-    std::optional<uint32_t> page_count,
-    const SkBitmap& bitmap) {
-#if BUILDFLAG(ENABLE_PDF)
-  if (lens::features::SendPdfCurrentPageEnabled()) {
-    pdf::PDFDocumentHelper* pdf_helper =
-        pdf::PDFDocumentHelper::MaybeGetForWebContents(tab_->GetContents());
-    if (pdf_helper) {
-      pdf_helper->GetMostVisiblePageIndex(base::BindOnce(
-          &LensOverlayController::UpdatePageContextualizationPart3,
-          weak_factory_.GetWeakPtr(), page_contents, primary_content_type,
-          page_count, bitmap));
-      return;
-    }
-  }
-#endif  // BUILDFLAG(ENABLE_PDF)
-
-  UpdatePageContextualizationPart3(page_contents, primary_content_type,
-                                   page_count, bitmap,
-                                   /*most_visible_page=*/std::nullopt);
-}
-
-void LensOverlayController::UpdatePageContextualizationPart3(
-    std::vector<lens::PageContent> page_contents,
-    lens::MimeType primary_content_type,
-    std::optional<uint32_t> page_count,
-    const SkBitmap& bitmap,
-    std::optional<uint32_t> most_visible_page) {
-  bool sending_bitmap = false;
-  if (!bitmap.drawsNothing() &&
-      (initialization_data_->updated_screenshot_.drawsNothing() ||
-       !lens::AreBitmapsEqual(initialization_data_->updated_screenshot_,
-                              bitmap))) {
-    initialization_data_->updated_screenshot_ = bitmap;
-    sending_bitmap = true;
-  }
-  initialization_data_->last_retrieved_most_visible_page_ = most_visible_page;
-
-  // TODO(crbug.com/399215935): Ideally, this check should ensure that any of
-  // the content date has not changed. For now, we only check if the
-  // primary_content_type bytes have changed.
-  auto old_page_content_it = std::ranges::find_if(
-      initialization_data_->page_contents_,
-      [&primary_content_type](const auto& page_content) {
-        return page_content.content_type_ == primary_content_type;
-      });
-  auto new_page_content_it = std::ranges::find_if(
-      page_contents, [&primary_content_type](const auto& page_content) {
-        return page_content.content_type_ == primary_content_type;
-      });
-  const lens::PageContent* old_page_content =
-      old_page_content_it != initialization_data_->page_contents_.end()
-          ? &(*old_page_content_it)
-          : nullptr;
-  const lens::PageContent* new_page_content =
-      new_page_content_it != page_contents.end() ? &(*new_page_content_it)
-                                                 : nullptr;
-
-  if (initialization_data_->primary_content_type_ == primary_content_type &&
-      old_page_content && new_page_content) {
-    const float old_size = old_page_content->bytes_.size();
-    const float new_size = new_page_content->bytes_.size();
-    const float percent_changed = abs((new_size - old_size) / old_size);
-    if (percent_changed < kByteChangeTolerancePercent) {
-      if (!sending_bitmap) {
-        // If the bytes have not changed more than our threshold and the
-        // screenshot has not changed, exit early. Notify the query controller
-        // that the user may be issuing a search request, and therefore the
-        // query should be restarted if TTL expired. If the bytes did change,
-        // this will happen automatically as a result of the
-        // SendUpdatedPageContent call below.
-        lens_overlay_query_controller_->MaybeRestartQueryFlow();
-        return;
-      }
-
-      // If the screenshot has changed but the bytes have not, send only the
-      // screenshot.
-      lens_overlay_query_controller_->SendUpdatedPageContent(
-          std::nullopt, std::nullopt, std::nullopt, std::nullopt,
-          initialization_data_->last_retrieved_most_visible_page_,
-          sending_bitmap ? bitmap : SkBitmap());
-      return;
-    }
-  }
-
-  // Since the page content has changed, let the query controller know to avoid
-  // dangling pointers.
-  lens_overlay_query_controller_->ResetPageContentData();
-
-  initialization_data_->page_contents_ = page_contents;
-  initialization_data_->primary_content_type_ = primary_content_type;
-
-  // If no bytes were retrieved from the page, the query won't be able to be
-  // contextualized. Notify the side panel so the ghost loader isn't shown. No
-  // need to update update the overlay as this update only happens on navigation
-  // where the side panel will already be open.
-  if (!new_page_content || new_page_content->bytes_.empty()) {
-    SuppressGhostLoader();
-  }
-
-#if BUILDFLAG(ENABLE_PDF)
-  // If the new page is a PDF, fetch the text from the page to be used as early
-  // suggest signals.
-  if (new_page_content &&
-      new_page_content->content_type_ == lens::MimeType::kPdf) {
-    CHECK(lens_overlay_query_controller_);
-    GetContextualizationController()->FetchVisiblePageIndexAndGetPartialPdfText(
-        page_count.value_or(0),
-        base::BindOnce(&LensOverlayController::OnPdfPartialPageTextRetrieved,
-                       weak_factory_.GetWeakPtr()));
-  }
-#endif
-
-  is_upload_progress_bar_shown_ = true;
-  is_first_upload_handler_event_ = true;
-  lens_overlay_query_controller_->SendUpdatedPageContent(
-      initialization_data_->page_contents_,
-      initialization_data_->primary_content_type_,
-      lens_search_controller_->GetPageURL(),
-      lens_search_controller_->GetPageTitle(),
-      initialization_data_->last_retrieved_most_visible_page_,
-      sending_bitmap ? bitmap : SkBitmap());
-
-  GetLensSessionMetricsLogger()->OnFollowUpPageContentRetrieved(
-      primary_content_type);
-  RecordDocumentMetrics(page_count);
-}
-
 void LensOverlayController::SuppressGhostLoader() {
   if (page_) {
     page_->SuppressGhostLoader();
@@ -1841,16 +1640,10 @@
   // Grab the tab contents web view and disable mouse and keyboard inputs to it.
   auto* contents_web_view = tab_->GetBrowserWindowInterface()->GetWebView();
   CHECK(contents_web_view);
-  if (should_show_overlay_) {
-    contents_web_view->SetEnabled(false);
-  }
+  contents_web_view->SetEnabled(false);
 
   // If the view already exists, we just need to reshow it.
   if (overlay_view_) {
-    // Exit early to avoid reshowing the overlay if it should not be shown.
-    if (!should_show_overlay_) {
-      return;
-    }
     // Restore the state to show the overlay.
     overlay_view_->SetVisible(true);
     preselection_widget_anchor_->SetVisible(true);
@@ -1865,11 +1658,9 @@
     return;
   }
 
-  // Create the views that will house our UI. The overlay view might not
-  // actually be shown, as dictated by `should_show_overlay_`. It still needs to
-  // be created so the initialization process completes.
+  // Create the views that will house our UI.
   overlay_view_ = CreateViewForOverlay();
-  overlay_view_->SetVisible(should_show_overlay_);
+  overlay_view_->SetVisible(true);
 
   // Sanity check that the overlay view is above the contents web view.
   auto* parent_view = overlay_view_->parent();
@@ -1895,10 +1686,8 @@
 
   // The overlay needs to be focused on show to immediately begin
   // receiving key events.
-  if (should_show_overlay_) {
-    CHECK(overlay_web_view_);
-    overlay_web_view_->RequestFocus();
-  }
+  CHECK(overlay_web_view_);
+  overlay_web_view_->RequestFocus();
 
   // Listen to the render process housing out overlay.
   overlay_web_view_->GetWebContents()
@@ -1971,43 +1760,20 @@
   InitializeOverlayUI(*initialization_data_);
   base::UmaHistogramBoolean("Lens.Overlay.Shown", true);
 
-#if BUILDFLAG(ENABLE_PDF)
-  // If PDF content was extracted from the page, fetch the text from the PDF to
-  // be used as early suggest signals.
-  if (!initialization_data_->page_contents_.empty() &&
-      initialization_data_->page_contents_.front().content_type_ ==
-          lens::MimeType::kPdf) {
-    CHECK(initialization_data_->pdf_page_count_.has_value());
-    CHECK(lens_overlay_query_controller_);
-    GetContextualizationController()->FetchVisiblePageIndexAndGetPartialPdfText(
-        initialization_data_->pdf_page_count_.value(),
-        base::BindOnce(&LensOverlayController::OnPdfPartialPageTextRetrieved,
-                       weak_factory_.GetWeakPtr()));
-  }
-#endif
-
   // If the StartQueryFlow optimization is enabled, the page contents will not
   // be sent with the initial image request, so we need to send it here.
   if (lens::features::IsLensOverlayContextualSearchboxEnabled() &&
       lens::features::IsLensOverlayEarlyStartQueryFlowOptimizationEnabled() &&
       is_page_context_eligible_) {
-    // The screenshot is not sent here unless forced by
-    // `should_send_screenshot_on_init_` as it should have been sent in the
-    // original StartQueryFlow call.
-    lens_overlay_query_controller_->SendUpdatedPageContent(
-        initialization_data_->page_contents_,
-        initialization_data_->primary_content_type_,
-        lens_search_controller_->GetPageURL(),
-        lens_search_controller_->GetPageTitle(),
-        initialization_data_->last_retrieved_most_visible_page_,
-        should_send_screenshot_on_init_
-            ? initialization_data_->initial_screenshot_
-            : SkBitmap());
+    // TODO(crbug.com/418856988): Replace this with a call that starts
+    // contextualization without the unneeded callback.
+    GetContextualizationController()->TryUpdatePageContextualization(
+        base::DoNothing());
   }
 
   // Show the preselection overlay now that the overlay is initialized and ready
   // to be shown.
-  if (!pending_region_ && should_show_overlay_) {
+  if (!pending_region_) {
     ShowPreselectionBubble();
   }
 
@@ -2592,7 +2358,7 @@
 
 void LensOverlayController::ShowPreselectionBubble() {
   // Don't show the preselection bubble if the overlay is not being shown.
-  if (!should_show_overlay_ || state() == State::kOverlayAndResults) {
+  if (state() == State::kOverlayAndResults) {
     return;
   }
 
@@ -3152,7 +2918,7 @@
   initialization_data_->pdf_pages_text_ = std::move(pdf_pages_text);
 }
 
-void LensOverlayController::OnPageContextUpdated(
+void LensOverlayController::OnPageContextUpdatedForSuggestion(
     const GURL& destination_url,
     AutocompleteMatchType::Type match_type,
     bool is_zero_prefix_suggestion,
diff --git a/chrome/browser/ui/lens/lens_overlay_controller.h b/chrome/browser/ui/lens/lens_overlay_controller.h
index 30bf5c85..6f2e7c2 100644
--- a/chrome/browser/ui/lens/lens_overlay_controller.h
+++ b/chrome/browser/ui/lens/lens_overlay_controller.h
@@ -356,6 +356,16 @@
   // Gets string for invocation source enum, used for logging metrics.
   std::string GetInvocationSourceString();
 
+  // Records the UMA for the metrics relating to the document where the
+  // contextual search box was shown. If this is a webpage, records the size of
+  // the innerHtml and the innerText. If this is a PDF, records the byte size of
+  // the PDF and the number of pages. `pdf_page_count` is only used for PDFs.
+  // TODO(crbug.com/404941800): Move the document metrics to where the page
+  // content bytes are stored. Temporarily public and should be made a private
+  // function again after contextualization controller is fully migrated to
+  // avoid overlay dependencies.
+  void RecordDocumentMetrics(std::optional<uint32_t> pdf_page_count);
+
   // Gets the WebContents housed in the side panel for testing.
   content::WebContents* GetSidePanelWebContentsForTesting();
 
@@ -443,19 +453,6 @@
       bool is_zero_prefix_suggestion,
       lens::LensOverlayInvocationSource invocation_source);
 
-  // Starts the contextualization flow without the overlay being shown to the
-  // user.
-  // TODO(crbug.com/404941800): This still goes through the entire
-  // TODO(crbug.com/404941800): This still goes through the entire
-  // initialization flow for the overlay. This is not efficient, but is being
-  // done to unblock the contextual searchbox prototype. This should be
-  // refactored to be done in the LensSearchController to not go through the
-  // overlay controller.
-  // Virtual for testing.
-  virtual void StartContextualizationWithoutOverlay(
-      lens::LensOverlayInvocationSource invocation_source,
-      lens::LensOverlayQueryController* lens_overlay_query_controller);
-
   // Sets a region to search after the overlay loads, then calls ShowUI().
   // All units are in device pixels. region_bitmap contains the high definition
   // image bytes to use for the search instead of cropping the region from the
@@ -720,34 +717,6 @@
   std::vector<lens::mojom::CenterRotatedBoxPtr> ConvertSignificantRegionBoxes(
       const std::vector<gfx::Rect>& all_bounds);
 
-  // Tries to fetch the underlying page content bytes and update the query flow
-  // with them.
-  void TryUpdatePageContextualization();
-
-  // Begin updating page contextualization by potentially taking a new
-  // screenshot.
-  void UpdatePageContextualization(std::vector<lens::PageContent> page_contents,
-                                   lens::MimeType primary_content_type,
-                                   std::optional<uint32_t> pdf_page_count);
-
-  // Continue updating page contextualization by potentially getting the current
-  // PDF page.
-  void UpdatePageContextualizationPart2(
-      std::vector<lens::PageContent> page_contents,
-      lens::MimeType primary_content_type,
-      std::optional<uint32_t> pdf_page_count,
-      const SkBitmap& bitmap);
-
-  // Updates the query flow with the new page content bytes and/or screenshot. A
-  // request will only be sent if the bytes are different from the previous
-  // bytes sent or the screenshot is different from the previous screenshot.
-  void UpdatePageContextualizationPart3(
-      std::vector<lens::PageContent> page_contents,
-      lens::MimeType primary_content_type,
-      std::optional<uint32_t> pdf_page_count,
-      const SkBitmap& bitmap,
-      std::optional<uint32_t> pdf_current_page);
-
   // Updates state of the ghost loader. |suppress_ghost_loader| is true when
   // the page bytes can't be uploaded.
   void SuppressGhostLoader();
@@ -929,14 +898,6 @@
       bool is_zero_prefix_suggestion,
       std::map<std::string, std::string> additional_query_params);
 
-  // Records the UMA for the metrics relating to the document where the
-  // contextual search box was shown. If this is a webpage, records the size of
-  // the innerHtml and the innerText. If this is a PDF, records the byte size of
-  // the PDF and the number of pages. `pdf_page_count` is only used for PDFs.
-  // TODO(crbug.com/404941800): Move the document metrics to where the page
-  // content bytes are stored.
-  void RecordDocumentMetrics(std::optional<uint32_t> pdf_page_count);
-
   // Posts a task to the background thread to calculate the OCR DOM similarity
   // and then records the result. Only records the similarity once per session.
   // Only records the similarity if the OCR text and page content are available.
@@ -975,8 +936,9 @@
   void OnPdfPartialPageTextRetrieved(
       std::vector<std::u16string> pdf_pages_text);
 
-  // Callback to run when the page context has been updated.
-  void OnPageContextUpdated(
+  // Callback to run when the page context has been updated and the suggestion
+  // query should now be issued.
+  void OnPageContextUpdatedForSuggestion(
       const GURL& destination_url,
       AutocompleteMatchType::Type match_type,
       bool is_zero_prefix_suggestion,
@@ -1016,11 +978,6 @@
   lens::LensOverlayInvocationSource invocation_source_ =
       lens::LensOverlayInvocationSource::kAppMenu;
 
-  // Whether the overlay should be visible when it is opened. This is a hacky
-  // approach to start contextual searchbox flow without the overlay UI being
-  // shown. See StartContextualizationWithoutOverlay todo for more details.
-  bool should_show_overlay_ = true;
-
   // A contextual search request to be issued once the overlay is initialized.
   base::OnceClosure pending_contextual_search_request_;
 
diff --git a/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc b/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
index 20c675b2..d2eede5 100644
--- a/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
+++ b/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
@@ -235,6 +235,10 @@
     "  === $1; return iframeSrcLoaded && stickPresent && "
     "  searchboxInputLoaded;})();";
 
+// TODO(crbug.com/417812533): Tests are currently failing in branded builds due
+// to missing metrics. Will be fixed as part of work to move recording document
+// metrics to the contextualization controller.
+#if !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 constexpr char kCheckSidePanelThumbnailShownScript[] =
     "(function() {const appRoot = "
     "document.getElementsByTagName('lens-side-panel-app')[0].shadowRoot;"
@@ -245,6 +249,7 @@
     "const imageSrc = thumbnailRoot.getElementById('image').src;"
     "return window.getComputedStyle(thumbContainer).display !== 'none' && "
     "       imageSrc.startsWith('data:image/jpeg');})();";
+#endif  // !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
 constexpr char kCheckSidePanelToastShownScript[] =
     "(function() {const appRoot = "
@@ -1458,6 +1463,10 @@
   ASSERT_TRUE(base::test::RunUntil([&]() { return observer.request_shown(); }));
 }
 
+// TODO(crbug.com/417812533): Tests are currently failing in branded builds due
+// to missing metrics. Will be fixed as part of work to move recording document
+// metrics to the contextualization controller.
+#if !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 // TODO(b/335801964): Test flaky on Mac.
 #if BUILDFLAG(IS_MAC)
 #define MAYBE_SidePanelInteractionsAfterRegionSelection \
@@ -1519,7 +1528,7 @@
   EXPECT_FALSE(controller->get_selected_region_for_testing().is_null());
   EXPECT_TRUE(base::StartsWith(controller->GetThumbnailForTesting(), "data:"));
   EXPECT_EQ(controller->GetPageClassificationForTesting(),
-            metrics::OmniboxEventProto::LENS_SIDE_PANEL_SEARCHBOX);
+            metrics::OmniboxEventProto::CONTEXTUAL_SEARCHBOX);
 
   // Verify that after text selection, the controller has a copy of the text,
   // the thumbnail is no longer shown and the controller's copy of the
@@ -1537,7 +1546,7 @@
   EXPECT_TRUE(controller->get_selected_region_for_testing().is_null());
   EXPECT_TRUE(controller->GetThumbnailForTesting().empty());
   EXPECT_EQ(controller->GetPageClassificationForTesting(),
-            metrics::OmniboxEventProto::SEARCH_SIDE_PANEL_SEARCHBOX);
+            metrics::OmniboxEventProto::CONTEXTUAL_SEARCHBOX);
 
   // Verify that after a signal from the searchbox that the text was modified,
   // no text selection is present.
@@ -1547,6 +1556,7 @@
   fake_controller->FlushForTesting();
   EXPECT_TRUE(fake_controller->fake_overlay_page_.did_clear_text_selection_);
 }
+#endif  // !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
 // TODO(b/350991033): Test flaky on Mac.
 #if BUILDFLAG(IS_MAC)
@@ -3506,188 +3516,6 @@
 }
 
 IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest,
-                       PopAndLoadTranslateQueryFromHistory) {
-  WaitForPaint();
-
-  // State should start in off.
-  auto* controller = GetLensOverlayController();
-  ASSERT_EQ(controller->state(), State::kOff);
-
-  // Showing UI should change the state to screenshot and eventually to overlay.
-  OpenLensOverlayWithPendingRegion(
-      LensOverlayInvocationSource::kContentAreaContextMenuImage,
-      kTestRegion->Clone(), CreateNonEmptyBitmap(100, 100));
-  ASSERT_EQ(controller->state(), State::kScreenshot);
-  ASSERT_TRUE(base::test::RunUntil(
-      [&]() { return controller->state() == State::kOverlayAndResults; }));
-  EXPECT_TRUE(content::WaitForLoadStop(GetOverlayWebContents()));
-  EXPECT_TRUE(content::WaitForLoadStop(
-      controller->GetSidePanelWebContentsForTesting()));
-  EXPECT_TRUE(controller->GetOverlayViewForTesting()->GetVisible());
-  auto* fake_query_controller =
-      static_cast<lens::TestLensOverlayQueryController*>(
-          controller->get_lens_overlay_query_controller_for_testing());
-  EXPECT_EQ(fake_query_controller->last_lens_selection_type(),
-            lens::INJECTED_IMAGE);
-
-  const GURL first_search_url(
-      "https://www.google.com/"
-      "search?source=chrome.cr.ctxi&q=&lns_fp=1&lns_mode=un"
-      "&gsc=2&hl=en-US&cs=0");
-
-  // The search query history stack should be empty and the currently loaded
-  // query should be set.
-  EXPECT_TRUE(GetSearchQueryHistory().empty());
-  const auto first_search_query = GetLoadedSearchQuery();
-  EXPECT_TRUE(first_search_query);
-  EXPECT_TRUE(first_search_query->search_query_text_.empty());
-  VerifySearchQueryParameters(first_search_query->search_query_url_);
-  VerifyTextQueriesAreEqual(first_search_query->search_query_url_,
-                            first_search_url);
-  EXPECT_FALSE(first_search_query->selected_region_thumbnail_uri_.empty());
-  EXPECT_EQ(first_search_query->selected_region_, kTestRegion);
-  EXPECT_FALSE(first_search_query->selected_region_bitmap_.drawsNothing());
-  EXPECT_EQ(first_search_query->selected_region_bitmap_.width(), 100);
-  EXPECT_EQ(first_search_query->selected_region_bitmap_.height(), 100);
-  EXPECT_FALSE(first_search_query->selected_text_);
-  EXPECT_EQ(first_search_query->lens_selection_type_, lens::INJECTED_IMAGE);
-
-  // Loading a url in the side panel should show the results page.
-  const GURL second_search_url(
-      "https://www.google.com/"
-      "search?source=chrome.cr.menu&q=oranges&lns_fp=1&lns_mode=text"
-      "&gsc=2&hl=en-US&cs=0");
-  // Do the full image translate query first so it gets attached to the query.
-  const std::string source_lang = "auto";
-  const std::string target_lang = "fi";
-  content::TestNavigationObserver second_observer(
-      controller->GetSidePanelWebContentsForTesting());
-  controller->IssueTranslateFullPageRequestForTesting(source_lang, target_lang);
-  controller->IssueTextSelectionRequestForTesting("oranges", 20, 200);
-  second_observer.Wait();
-
-  // The search query history stack should be size 1.
-  EXPECT_EQ(GetSearchQueryHistory().size(), 1UL);
-  const auto second_search_query = GetLoadedSearchQuery();
-  EXPECT_TRUE(second_search_query);
-  EXPECT_EQ(second_search_query->search_query_text_, "oranges");
-  VerifySearchQueryParameters(second_search_query->search_query_url_);
-  VerifyTextQueriesAreEqual(second_search_query->search_query_url_,
-                            second_search_url);
-  EXPECT_TRUE(second_search_query->selected_region_thumbnail_uri_.empty());
-  EXPECT_TRUE(second_search_query->selected_region_bitmap_.drawsNothing());
-  EXPECT_FALSE(second_search_query->selected_region_);
-  EXPECT_TRUE(second_search_query->selected_text_);
-  EXPECT_EQ(second_search_query->selected_text_->first, 20);
-  EXPECT_EQ(second_search_query->selected_text_->second, 200);
-  EXPECT_TRUE(second_search_query->translate_options_);
-  EXPECT_EQ(second_search_query->translate_options_->source_language,
-            source_lang);
-  EXPECT_EQ(second_search_query->translate_options_->target_language,
-            target_lang);
-  EXPECT_EQ(second_search_query->lens_selection_type_,
-            lens::SELECT_TEXT_HIGHLIGHT);
-  VerifySearchQueryParameters(second_observer.last_navigation_url());
-  VerifyTextQueriesAreEqual(second_observer.last_navigation_url(),
-                            second_search_url);
-
-  // Loading a second url in the side panel should show the results page.
-  const GURL third_search_url(
-      "https://www.google.com/"
-      "search?source=chrome.cr.menu&q=kiwi&lns_fp=1&lns_mode=text&gsc=2"
-      "&hl=en-US&cs=0");
-  // We can't use content::WaitForLoadStop here since the last navigation is
-  // successful.
-  content::TestNavigationObserver third_observer(
-      controller->GetSidePanelWebContentsForTesting());
-  controller->IssueEndTranslateModeRequestForTesting();
-  controller->results_side_panel_coordinator()->LoadURLInResultsFrameForTesting(
-      third_search_url);
-  third_observer.Wait();
-
-  // The search query history stack should have 2 entries and the currently
-  // loaded query should be set to the new query
-  EXPECT_EQ(GetSearchQueryHistory().size(), 2UL);
-  const auto third_search_query = GetLoadedSearchQuery();
-  EXPECT_TRUE(third_search_query);
-  EXPECT_EQ(third_search_query->search_query_text_, "kiwi");
-  VerifySearchQueryParameters(third_search_query->search_query_url_);
-  VerifyTextQueriesAreEqual(third_search_query->search_query_url_,
-                            third_search_url);
-  EXPECT_TRUE(third_search_query->selected_region_thumbnail_uri_.empty());
-  EXPECT_FALSE(third_search_query->selected_region_);
-  EXPECT_FALSE(third_search_query->selected_text_);
-  EXPECT_FALSE(third_search_query->translate_options_);
-  EXPECT_EQ(third_search_query->lens_selection_type_,
-            lens::UNKNOWN_SELECTION_TYPE);
-  VerifySearchQueryParameters(third_observer.last_navigation_url());
-  VerifyTextQueriesAreEqual(third_observer.last_navigation_url(),
-                            third_search_url);
-
-  // Popping the query should load the previous query into the results frame.
-  content::TestNavigationObserver fourth_observer(
-      controller->GetSidePanelWebContentsForTesting());
-  controller->results_side_panel_coordinator()->PopAndLoadQueryFromHistory();
-  fourth_observer.Wait();
-
-  // The search query history stack should have 1 entry and the currently loaded
-  // query should be set to the previous query.
-  EXPECT_EQ(GetSearchQueryHistory().size(), 1UL);
-  const auto first_popped_query = GetLoadedSearchQuery();
-  EXPECT_TRUE(first_popped_query);
-  EXPECT_EQ(first_popped_query->search_query_text_, "oranges");
-  VerifySearchQueryParameters(first_popped_query->search_query_url_);
-  VerifyTextQueriesAreEqual(first_popped_query->search_query_url_,
-                            second_search_url);
-  EXPECT_TRUE(first_popped_query->selected_region_thumbnail_uri_.empty());
-  EXPECT_FALSE(first_popped_query->selected_region_);
-  EXPECT_TRUE(first_popped_query->selected_text_);
-  EXPECT_EQ(first_popped_query->selected_text_->first, 20);
-  EXPECT_EQ(first_popped_query->selected_text_->second, 200);
-  EXPECT_TRUE(first_popped_query->translate_options_);
-  EXPECT_EQ(first_popped_query->translate_options_->source_language,
-            source_lang);
-  EXPECT_EQ(first_popped_query->translate_options_->target_language,
-            target_lang);
-  EXPECT_EQ(first_popped_query->lens_selection_type_,
-            lens::SELECT_TEXT_HIGHLIGHT);
-  VerifySearchQueryParameters(fourth_observer.last_navigation_url());
-  VerifyTextQueriesAreEqual(fourth_observer.last_navigation_url(),
-                            second_search_url);
-  auto* fake_controller = static_cast<LensOverlayControllerFake*>(controller);
-  fake_controller->FlushForTesting();
-  EXPECT_EQ(fake_controller->fake_overlay_page_.source_language_, source_lang);
-  EXPECT_EQ(fake_controller->fake_overlay_page_.target_language_, target_lang);
-
-  // Popping the query should load the previous query into the results frame.
-  content::TestNavigationObserver fifth_observer(
-      controller->GetSidePanelWebContentsForTesting());
-  controller->results_side_panel_coordinator()->PopAndLoadQueryFromHistory();
-  fifth_observer.Wait();
-
-  // The search query history stack should be empty and the currently loaded
-  // query should be set.
-  EXPECT_TRUE(GetSearchQueryHistory().empty());
-  const auto second_popped_query = GetLoadedSearchQuery();
-  EXPECT_TRUE(second_popped_query);
-  EXPECT_TRUE(second_popped_query->search_query_text_.empty());
-  VerifySearchQueryParameters(second_popped_query->search_query_url_);
-  VerifyTextQueriesAreEqual(second_popped_query->search_query_url_,
-                            first_search_url);
-  EXPECT_FALSE(second_popped_query->selected_region_thumbnail_uri_.empty());
-  EXPECT_EQ(second_popped_query->selected_region_, kTestRegion);
-  EXPECT_FALSE(second_popped_query->selected_region_bitmap_.drawsNothing());
-  EXPECT_EQ(second_popped_query->selected_region_bitmap_.width(), 100);
-  EXPECT_EQ(second_popped_query->selected_region_bitmap_.height(), 100);
-  EXPECT_FALSE(second_popped_query->selected_text_);
-  EXPECT_EQ(second_popped_query->lens_selection_type_, lens::INJECTED_IMAGE);
-  EXPECT_FALSE(second_popped_query->translate_options_);
-  fake_controller->FlushForTesting();
-  EXPECT_TRUE(fake_controller->fake_overlay_page_.source_language_.empty());
-  EXPECT_TRUE(fake_controller->fake_overlay_page_.target_language_.empty());
-}
-
-IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest,
                        PopAndLoadQueryFromHistoryWithRegionAndTextSelection) {
   WaitForPaint();
 
@@ -3739,7 +3567,7 @@
       controller->GetSidePanelWebContentsForTesting());
   controller->IssueLensRegionRequestForTesting(kTestRegion->Clone(),
                                                /*is_click=*/false);
-  second_search_observer.Wait();
+  second_search_observer.WaitForNavigationFinished();
 
   // The search query history stack should have 1 entry and the currently loaded
   // region should be set.
@@ -3761,7 +3589,7 @@
   content::TestNavigationObserver third_search_observer(
       controller->GetSidePanelWebContentsForTesting());
   controller->IssueTextSelectionRequestForTesting("kiwi", 1, 100);
-  third_search_observer.Wait();
+  third_search_observer.WaitForNavigationFinished();
 
   // The search query history stack should have 2 entries and the currently
   // loaded query should be set to the new query
@@ -3801,7 +3629,7 @@
   EXPECT_EQ(fake_query_controller->last_lens_selection_type(),
             lens::REGION_SEARCH);
 
-  first_pop_observer.Wait();
+  first_pop_observer.WaitForNavigationFinished();
 
   // The search query history stack should have 1 entry and the previously
   // loaded region should be present.
@@ -3820,7 +3648,7 @@
   content::TestNavigationObserver second_pop_observer(
       controller->GetSidePanelWebContentsForTesting());
   controller->results_side_panel_coordinator()->PopAndLoadQueryFromHistory();
-  second_pop_observer.Wait();
+  second_pop_observer.WaitForNavigationFinished();
 
   // The search query history stack should be empty and the currently loaded
   // query should be set to the original query.
@@ -3831,7 +3659,9 @@
   EXPECT_EQ(loaded_search_query->search_query_text_, "oranges");
   url_without_start_time_or_size =
       RemoveStartTimeAndSizeParams(loaded_search_query->search_query_url_);
-  EXPECT_EQ(url_without_start_time_or_size, first_search_url);
+  VerifySearchQueryParameters(loaded_search_query->search_query_url_);
+  VerifyTextQueriesAreEqual(loaded_search_query->search_query_url_,
+                            first_search_url);
   EXPECT_TRUE(loaded_search_query->selected_region_thumbnail_uri_.empty());
   EXPECT_FALSE(loaded_search_query->selected_region_);
   EXPECT_TRUE(loaded_search_query->selected_text_);
@@ -3839,10 +3669,9 @@
             lens::SELECT_TEXT_HIGHLIGHT);
   EXPECT_EQ(loaded_search_query->selected_text_->first, 20);
   EXPECT_EQ(loaded_search_query->selected_text_->second, 200);
-  url_without_start_time_or_size =
-      RemoveStartTimeAndSizeParams(second_pop_observer.last_navigation_url());
-  EXPECT_EQ(url_without_start_time_or_size, first_search_url);
-
+  VerifySearchQueryParameters(second_pop_observer.last_navigation_url());
+  VerifyTextQueriesAreEqual(second_pop_observer.last_navigation_url(),
+                            first_search_url);
   // Verify the text selection was sent back to mojo and any old selections
   // were cleared.
   auto* fake_controller = static_cast<LensOverlayControllerFake*>(controller);
@@ -5737,6 +5566,10 @@
                   .last_received_should_show_contextual_searchbox_);
 }
 
+// TODO(crbug.com/417812533): Tests are currently failing in branded builds due
+// to missing metrics. Will be fixed as part of work to move recording document
+// metrics to the contextualization controller.
+#if !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 IN_PROC_BROWSER_TEST_P(LensOverlayControllerBrowserPDFContextualizationTest,
                        PartialPdfIncludedInRequest) {
   // Open the PDF document and wait for it to finish loading.
@@ -5767,6 +5600,7 @@
       fake_query_controller->last_sent_partial_content().pages(0).text_segments(
           0));
 }
+#endif  // !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
 IN_PROC_BROWSER_TEST_P(LensOverlayControllerBrowserPDFContextualizationTest,
                        PageUrlIncludedInRequest) {
@@ -5820,6 +5654,10 @@
                    .pdf_page_number());
 }
 
+// TODO(crbug.com/417812533): Tests are currently failing in branded builds due
+// to missing metrics. Will be fixed as part of work to move recording document
+// metrics to the contextualization controller.
+#if !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 IN_PROC_BROWSER_TEST_P(LensOverlayControllerBrowserPDFContextualizationTest,
                        PdfBytesInFollowUpRequest) {
   base::HistogramTester histogram_tester;
@@ -5886,6 +5724,7 @@
       "TimeFromNavigationToFirstInteraction",
       /*expected_count=*/1);
 }
+#endif  // !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
 IN_PROC_BROWSER_TEST_P(LensOverlayControllerBrowserPDFContextualizationTest,
                        LargePdfNotIncludedInRequest) {
@@ -6008,6 +5847,10 @@
   }));
 }
 
+// TODO(crbug.com/417812533): Tests are currently failing in branded builds due
+// to missing metrics. Will be fixed as part of work to move recording document
+// metrics to the contextualization controller.
+#if !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 IN_PROC_BROWSER_TEST_P(LensOverlayControllerBrowserPDFContextualizationTest,
                        Histograms) {
   ukm::TestAutoSetUkmRecorder test_ukm_recorder;
@@ -6145,6 +5988,7 @@
           kPageContentTypeName,
       static_cast<int64_t>(lens::MimeType::kPdf));
 }
+#endif  // !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
 IN_PROC_BROWSER_TEST_P(LensOverlayControllerBrowserPDFContextualizationTest,
                        RecordSearchboxFocusedInSessionNavigationHistograms) {
@@ -6291,6 +6135,10 @@
   EXPECT_EQ(tab_count, browser()->tab_strip_model()->count());
 }
 
+// TODO(crbug.com/417812533): Tests are currently failing in branded builds due
+// to missing metrics. Will be fixed as part of work to move recording document
+// metrics to the contextualization controller.
+#if !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 class LensOverlayControllerBrowserPDFUpdatedContentFieldsTest
     : public LensOverlayControllerBrowserPDFContextualizationTest {
  public:
@@ -6409,13 +6257,17 @@
 
 // TODO(crbug.com/40268279): Stop testing both modes after OOPIF PDF viewer
 // launches.
+INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
+  LensOverlayControllerBrowserPDFUpdatedContentFieldsTest);
+INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
+  LensOverlayControllerBrowserPDFIncreaseLimitTest);
+#endif  // !BUILDFLAG(GOOGLE_CHROME_BRANDING)
+
+// TODO(crbug.com/40268279): Stop testing both modes after OOPIF PDF viewer
+// launches.
 INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(LensOverlayControllerBrowserPDFTest);
 INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
     LensOverlayControllerBrowserPDFContextualizationTest);
-INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
-    LensOverlayControllerBrowserPDFUpdatedContentFieldsTest);
-INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
-    LensOverlayControllerBrowserPDFIncreaseLimitTest);
 
 // Test with --enable-pixel-output-in-tests enabled, required to actually grab
 // screenshots for color extraction.
@@ -6703,8 +6555,9 @@
   auto* fake_query_controller =
       static_cast<lens::TestLensOverlayQueryController*>(
           controller->get_lens_overlay_query_controller_for_testing());
-  ASSERT_FALSE(
-      fake_query_controller->last_sent_underlying_content_bytes().empty());
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return !fake_query_controller->last_sent_underlying_content_bytes().empty();
+  }));
   ASSERT_EQ(lens::MimeType::kPlainText,
             fake_query_controller->last_sent_underlying_content_type());
 
@@ -6716,6 +6569,10 @@
                         last_sent_underlying_content_bytes.end()));
 }
 
+// TODO(crbug.com/417812533): Tests are currently failing in branded builds due
+// to missing metrics. Will be fixed as part of work to move recording document
+// metrics to the contextualization controller.
+#if !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest,
                        InnerTextBytesInFollowUpRequest) {
   base::HistogramTester histogram_tester;
@@ -6807,6 +6664,7 @@
   // Verify the similarity score was only recorded once for the session.
   histogram_tester.ExpectTotalCount("Lens.Overlay.OcrDomSimilarity", 1);
 }
+#endif  // !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
 IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest,
                        UpdateScreenshotOnSearchboxFocus) {
@@ -6864,6 +6722,10 @@
                   .has_image_data());
 }
 
+// TODO(crbug.com/417812533): Tests are currently failing in branded builds due
+// to missing metrics. Will be fixed as part of work to move recording document
+// metrics to the contextualization controller.
+#if !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest,
                        WebDocumentTypeHistograms) {
   ukm::TestAutoSetUkmRecorder test_ukm_recorder;
@@ -6940,6 +6802,7 @@
                                            80) == 1;
   }));
 }
+#endif  // !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
 IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest,
                        RecordSearchboxFocusedInSessionHistograms) {
@@ -6961,8 +6824,9 @@
   auto* fake_query_controller =
       static_cast<lens::TestLensOverlayQueryController*>(
           controller->get_lens_overlay_query_controller_for_testing());
-  ASSERT_FALSE(
-      fake_query_controller->last_sent_underlying_content_bytes().empty());
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return !fake_query_controller->last_sent_underlying_content_bytes().empty();
+  }));
 
   // Simulate the searchbox being focused.
   controller->OnFocusChangedForTesting(true);
@@ -7022,8 +6886,9 @@
   auto* fake_query_controller =
       static_cast<lens::TestLensOverlayQueryController*>(
           controller->get_lens_overlay_query_controller_for_testing());
-  ASSERT_FALSE(
-      fake_query_controller->last_sent_underlying_content_bytes().empty());
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return !fake_query_controller->last_sent_underlying_content_bytes().empty();
+  }));
 
   // Close the overlay and assert that the histogram was recorded once and
   // that the searchbox was not focused in the session.
@@ -7080,8 +6945,10 @@
   auto* fake_query_controller =
       static_cast<lens::TestLensOverlayQueryController*>(
           controller->get_lens_overlay_query_controller_for_testing());
-  ASSERT_FALSE(
-      fake_query_controller->last_sent_underlying_content_bytes().empty());
+  // Verify bytes was updated.
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return !fake_query_controller->last_sent_underlying_content_bytes().empty();
+  }));
 
   controller->OnZeroSuggestShownForTesting();
 
@@ -7217,8 +7084,9 @@
   auto* fake_query_controller =
       static_cast<lens::TestLensOverlayQueryController*>(
           controller->get_lens_overlay_query_controller_for_testing());
-  ASSERT_FALSE(
-      fake_query_controller->last_sent_underlying_content_bytes().empty());
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return !fake_query_controller->last_sent_underlying_content_bytes().empty();
+  }));
 
   controller->OnZeroSuggestShownForTesting();
 
@@ -7324,8 +7192,9 @@
   auto* fake_query_controller =
       static_cast<lens::TestLensOverlayQueryController*>(
           controller->get_lens_overlay_query_controller_for_testing());
-  ASSERT_FALSE(
-      fake_query_controller->last_sent_underlying_content_bytes().empty());
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return !fake_query_controller->last_sent_underlying_content_bytes().empty();
+  }));
 
   // Close the overlay and assert that the histogram was recorded once and
   // that zps was not shown in the session.
@@ -7997,8 +7866,9 @@
   auto* fake_query_controller =
       static_cast<lens::TestLensOverlayQueryController*>(
           controller->get_lens_overlay_query_controller_for_testing());
-  ASSERT_FALSE(
-      fake_query_controller->last_sent_underlying_content_bytes().empty());
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return !fake_query_controller->last_sent_underlying_content_bytes().empty();
+  }));
   ASSERT_EQ(lens::MimeType::kHtml,
             fake_query_controller->last_sent_underlying_content_type());
 
@@ -8339,6 +8209,10 @@
                   .last_received_should_show_contextual_searchbox_);
 }
 
+// TODO(crbug.com/417812533): Tests are currently failing in branded builds due
+// to missing metrics. Will be fixed as part of work to move recording document
+// metrics to the contextualization controller.
+#if !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 IN_PROC_BROWSER_TEST_F(LensOverlayControllerInnerHtmlWithInnerTextAndApc,
                        PageContentTypeHistograms) {
   ukm::TestAutoSetUkmRecorder test_ukm_recorder;
@@ -8418,6 +8292,7 @@
                                            80) == 1;
   }));
 }
+#endif  // !BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
 IN_PROC_BROWSER_TEST_F(LensOverlayControllerInnerHtmlWithInnerTextAndApc,
                        PageNotContextEligibleError) {
diff --git a/chrome/browser/ui/lens/lens_overlay_entry_point_controller.cc b/chrome/browser/ui/lens/lens_overlay_entry_point_controller.cc
index 3197a16..1b6821c 100644
--- a/chrome/browser/ui/lens/lens_overlay_entry_point_controller.cc
+++ b/chrome/browser/ui/lens/lens_overlay_entry_point_controller.cc
@@ -189,7 +189,7 @@
   UpdatePageActionState();
 }
 
-bool LensOverlayEntryPointController::IsUrlEduEligible(const GURL& url) {
+bool LensOverlayEntryPointController::IsUrlEduEligible(const GURL& url) const {
   if (!IsEnabled()) {
     return false;
   }
diff --git a/chrome/browser/ui/lens/lens_overlay_entry_point_controller.h b/chrome/browser/ui/lens/lens_overlay_entry_point_controller.h
index 8b29c9b..111b54d 100644
--- a/chrome/browser/ui/lens/lens_overlay_entry_point_controller.h
+++ b/chrome/browser/ui/lens/lens_overlay_entry_point_controller.h
@@ -67,7 +67,7 @@
 
   // Returns true if the given URL is eligible for EDU promos present on some
   // entrypoints.
-  bool IsUrlEduEligible(const GURL& url);
+  bool IsUrlEduEligible(const GURL& url) const;
 
   // Invokes the entrypoint action.
   static void InvokeAction(tabs::TabInterface* active_tab,
diff --git a/chrome/browser/ui/lens/lens_overlay_query_controller.cc b/chrome/browser/ui/lens/lens_overlay_query_controller.cc
index dce4004..191e6fd 100644
--- a/chrome/browser/ui/lens/lens_overlay_query_controller.cc
+++ b/chrome/browser/ui/lens/lens_overlay_query_controller.cc
@@ -330,6 +330,12 @@
       return lens::LensOverlayClientLogs::IMAGE_CONTEXT_MENU;
     case lens::LensOverlayInvocationSource::kOmnibox:
       return lens::LensOverlayClientLogs::OMNIBOX_BUTTON;
+    case lens::LensOverlayInvocationSource::kOmniboxContextualSuggestion:
+      return lens::LensOverlayClientLogs::OMNIBOX_CONTEXTUAL_SUGGESTION;
+    case lens::LensOverlayInvocationSource::kOmniboxPageAction:
+      return lens::LensOverlayClientLogs::OMNIBOX_PAGE_ACTION;
+    case lens::LensOverlayInvocationSource::kHomeworkActionChip:
+      return lens::LensOverlayClientLogs::HOMEWORK_ACTION_CHIP;
     case lens::LensOverlayInvocationSource::kToolbar:
       return lens::LensOverlayClientLogs::TOOLBAR_BUTTON;
     case lens::LensOverlayInvocationSource::kFindInPage:
diff --git a/chrome/browser/ui/lens/lens_overlay_untrusted_ui.cc b/chrome/browser/ui/lens/lens_overlay_untrusted_ui.cc
index fbc35eb4..4c0af91 100644
--- a/chrome/browser/ui/lens/lens_overlay_untrusted_ui.cc
+++ b/chrome/browser/ui/lens/lens_overlay_untrusted_ui.cc
@@ -246,6 +246,9 @@
   html_source->AddBoolean(
       "enableGradientRegionStroke",
       lens::features::GetVisualSelectionUpdatesEnableGradientRegionStroke());
+  html_source->AddBoolean(
+      "enableRegionSelectedGlow",
+      lens::features::GetVisualSelectionUpdatesEnableRegionSelectedGlow());
   html_source->AddBoolean("autoFocusSearchbox",
                           lens::features::ShouldAutoFocusSearchbox());
   html_source->AddBoolean("cornerSlidersEnabled",
diff --git a/chrome/browser/ui/lens/lens_overlay_url_builder.cc b/chrome/browser/ui/lens/lens_overlay_url_builder.cc
index ffb774e..48acfb7f 100644
--- a/chrome/browser/ui/lens/lens_overlay_url_builder.cc
+++ b/chrome/browser/ui/lens/lens_overlay_url_builder.cc
@@ -80,6 +80,10 @@
 inline constexpr char kInvocationSourceFindInPage[] = "chrome.cr.find";
 inline constexpr char kInvocationSourceToolbarIcon[] = "chrome.cr.tbic";
 inline constexpr char kInvocationSourceOmniboxIcon[] = "chrome.cr.obic";
+inline constexpr char kInvocationSourceOmniboxPageAction[] = "chrome.cr.obpa";
+inline constexpr char kInvocationSourceOmniboxContextualSuggestion[] =
+    "chrome.cr.obcs";
+inline constexpr char kInvocationSourceHomeworkActionChip[] = "chrome.cr.hwac";
 
 // The url query param for the viewport width and height.
 inline constexpr char kViewportWidthQueryParamKey[] = "biw";
@@ -254,6 +258,15 @@
     case lens::LensOverlayInvocationSource::kOmnibox:
       param_value = kInvocationSourceOmniboxIcon;
       break;
+    case lens::LensOverlayInvocationSource::kOmniboxPageAction:
+      param_value = kInvocationSourceOmniboxPageAction;
+      break;
+    case lens::LensOverlayInvocationSource::kOmniboxContextualSuggestion:
+      param_value = kInvocationSourceOmniboxContextualSuggestion;
+      break;
+    case lens::LensOverlayInvocationSource::kHomeworkActionChip:
+      param_value = kInvocationSourceHomeworkActionChip;
+      break;
     case lens::LensOverlayInvocationSource::kLVFShutterButton:
     case lens::LensOverlayInvocationSource::kLVFGallery:
     case lens::LensOverlayInvocationSource::kContextMenu:
diff --git a/chrome/browser/ui/lens/lens_search_contextualization_controller.cc b/chrome/browser/ui/lens/lens_search_contextualization_controller.cc
index 3c807881..28556f11 100644
--- a/chrome/browser/ui/lens/lens_search_contextualization_controller.cc
+++ b/chrome/browser/ui/lens/lens_search_contextualization_controller.cc
@@ -128,6 +128,10 @@
 void LensSearchContextualizationController::TryUpdatePageContextualization(
     OnPageContextUpdatedCallback callback) {
   if (state_ == State::kOff) {
+    // TODO(crbug.com/418825720): The viewport screenshot should be only be set
+    // in this controller in the future.
+    viewport_screenshot_ = lens_search_controller_->lens_overlay_controller()
+                               ->initial_screenshot();
     state_ = State::kActive;
   }
   CHECK(state_ == State::kActive);
@@ -226,6 +230,13 @@
     lens::MimeType primary_content_type,
     std::optional<uint32_t> page_count,
     const SkBitmap& bitmap) {
+  // It's possible the Lens session could have been closed while updating the
+  // page context. Return early and do not run the callback as it should have
+  // been cleared.
+  if (state_ == State::kOff || !on_page_context_updated_callback_) {
+    return;
+  }
+
 #if BUILDFLAG(ENABLE_PDF)
   if (lens::features::SendPdfCurrentPageEnabled()) {
     pdf::PDFDocumentHelper* pdf_helper =
@@ -253,6 +264,13 @@
     std::optional<uint32_t> page_count,
     const SkBitmap& bitmap,
     std::optional<uint32_t> most_visible_page) {
+  // It's possible the Lens session could have been closed while updating the
+  // page context. Return early and do not run the callback as it should have
+  // been cleared.
+  if (state_ == State::kOff || !on_page_context_updated_callback_) {
+    return;
+  }
+
   bool sending_bitmap = false;
   if (!bitmap.drawsNothing() &&
       (viewport_screenshot_.drawsNothing() ||
@@ -345,7 +363,12 @@
       lens_search_controller_->GetPageURL(),
       lens_search_controller_->GetPageTitle(),
       last_retrieved_most_visible_page_, sending_bitmap ? bitmap : SkBitmap());
-  // TODO(crbug.com/417812533): Record document metrics.
+  // TODO(crbug.com/417812533): Record document metrics in metrics helper or in
+  // this controller instead.
+  if (lens_search_controller_->lens_overlay_controller()->IsOverlayActive()) {
+    lens_search_controller_->lens_overlay_controller()->RecordDocumentMetrics(
+        page_count.value_or(0));
+  }
   lens_search_controller_->lens_session_metrics_logger()
       ->OnFollowUpPageContentRetrieved(primary_content_type);
 
diff --git a/chrome/browser/ui/lens/lens_search_controller.cc b/chrome/browser/ui/lens/lens_search_controller.cc
index 6b7c459e..1e9edf7 100644
--- a/chrome/browser/ui/lens/lens_search_controller.cc
+++ b/chrome/browser/ui/lens/lens_search_controller.cc
@@ -187,29 +187,30 @@
   // Setup all state necessary for this Lens session.
   StartLensSession(invocation_source);
 
-  // TODO(crbug.com/404941800): This flow should not start the overlay once
-  // contextualization is separated from the overlay.
-  lens_overlay_controller_->StartContextualizationWithoutOverlay(
-      invocation_source, lens_overlay_query_controller_.get());
+  // TODO(crbug.com/418856988): Replace this with a call that starts
+  // contextualization without the unneeded callback.
+  lens_contextualization_controller_->StartContextualization(invocation_source,
+                                                             base::DoNothing());
 }
 
 void LensSearchController::IssueContextualSearchRequest(
+    lens::LensOverlayInvocationSource invocation_source,
     const GURL& destination_url,
     AutocompleteMatchType::Type match_type,
     bool is_zero_prefix_suggestion) {
-  // TODO(crbug.com/402497756): For prototyping, reusing the existing
-  // omnibox entry point. However, for production, create a new invocation
-  // source for this new entry point.
-  lens::LensOverlayInvocationSource invocation_source =
-      lens::LensOverlayInvocationSource::kOmnibox;
+  // This method should only be used by the omnibox contextual suggestion flow.
+  // There is no dependency on the omnibox, so this check is solely to ensure a
+  // new flow is not accidentally added.
+  CHECK(invocation_source ==
+        lens::LensOverlayInvocationSource::kOmniboxContextualSuggestion);
 
   // If the eligibility checks fail, do not procced with opening any UI.
   if (!RunLensEligibilityChecks(
           invocation_source,
           /*permission_granted_callback=*/base::BindRepeating(
               &LensSearchController::IssueContextualSearchRequest,
-              weak_ptr_factory_.GetWeakPtr(), destination_url, match_type,
-              is_zero_prefix_suggestion))) {
+              weak_ptr_factory_.GetWeakPtr(), invocation_source,
+              destination_url, match_type, is_zero_prefix_suggestion))) {
     return;
   }
 
diff --git a/chrome/browser/ui/lens/lens_search_controller.h b/chrome/browser/ui/lens/lens_search_controller.h
index 39938898..83f1cb0c 100644
--- a/chrome/browser/ui/lens/lens_search_controller.h
+++ b/chrome/browser/ui/lens/lens_search_controller.h
@@ -114,9 +114,11 @@
   // is fully opened.
   // TODO(crbug.com/403629222): Revisit if it makes sense to pass the
   // destination URL instead of the query text directly.
-  void IssueContextualSearchRequest(const GURL& destination_url,
-                                    AutocompleteMatchType::Type match_type,
-                                    bool is_zero_prefix_suggestion);
+  void IssueContextualSearchRequest(
+      lens::LensOverlayInvocationSource invocation_source,
+      const GURL& destination_url,
+      AutocompleteMatchType::Type match_type,
+      bool is_zero_prefix_suggestion);
 
   // Starts the closing process of the overlay. This is an asynchronous process
   // with the following sequence:
diff --git a/chrome/browser/ui/lens/lens_url_matcher.cc b/chrome/browser/ui/lens/lens_url_matcher.cc
index 15a0839..7ad399b 100644
--- a/chrome/browser/ui/lens/lens_url_matcher.cc
+++ b/chrome/browser/ui/lens/lens_url_matcher.cc
@@ -88,7 +88,7 @@
 void LensUrlMatcher::InitializePathAllowMatcher(
     std::string path_match_allow_filters,
     base::MatcherStringPattern::ID* id) {
-  auto allow_strings = JSONArrayToVector(path_match_allow_filters);
+  const auto allow_strings = JSONArrayToVector(path_match_allow_filters);
   std::vector<base::MatcherStringPattern> allow_patterns;
   std::vector<const base::MatcherStringPattern*> allow_pointers;
   allow_patterns.reserve(allow_strings.size());
@@ -106,7 +106,7 @@
 void LensUrlMatcher::InitializePathBlockMatcher(
     std::string path_match_block_filters,
     base::MatcherStringPattern::ID* id) {
-  auto block_strings = JSONArrayToVector(path_match_block_filters);
+  const auto block_strings = JSONArrayToVector(path_match_block_filters);
   std::vector<base::MatcherStringPattern> block_patterns;
   std::vector<const base::MatcherStringPattern*> block_pointers;
   block_patterns.reserve(block_strings.size());
diff --git a/chrome/browser/ui/page_action/page_action_icon_type.h b/chrome/browser/ui/page_action/page_action_icon_type.h
index f3a06be6..8027e04e 100644
--- a/chrome/browser/ui/page_action/page_action_icon_type.h
+++ b/chrome/browser/ui/page_action/page_action_icon_type.h
@@ -47,7 +47,8 @@
   kOptimizationGuide = 31,
   kCollaborationMessaging = 32,
   kChangePassword = 33,
-  kMaxValue = kChangePassword,
+  kLensOverlayHomework = 34,
+  kMaxValue = kLensOverlayHomework,
 };
 // LINT.ThenChange(//tools/metrics/histograms/metadata/page/enums.xml:PageActionIconType)
 
@@ -83,6 +84,7 @@
 static_assert(static_cast<int>(PageActionIconType::kCollaborationMessaging) ==
               32);
 static_assert(static_cast<int>(PageActionIconType::kChangePassword) == 33);
+static_assert(static_cast<int>(PageActionIconType::kLensOverlayHomework) == 34);
 
 // Returns a bool indicating whether the given page action type has been
 // migrated to the new framework, which is based on ActionItems instead of
diff --git a/chrome/browser/ui/tab_helpers.cc b/chrome/browser/ui/tab_helpers.cc
index 47e03592..697ab224 100644
--- a/chrome/browser/ui/tab_helpers.cc
+++ b/chrome/browser/ui/tab_helpers.cc
@@ -94,7 +94,6 @@
 #include "chrome/browser/ui/search_engines/search_engine_tab_helper.h"
 #include "chrome/browser/ui/tab_contents/core_tab_helper.h"
 #include "chrome/browser/ui/tab_dialogs.h"
-#include "chrome/browser/ui/tab_ui_helper.h"
 #include "chrome/browser/ui/thumbnails/thumbnail_tab_helper.h"
 #include "chrome/browser/v8_compile_hints/v8_compile_hints_tab_helper.h"
 #include "chrome/browser/vr/vr_tab_helper.h"
@@ -567,7 +566,6 @@
   }
 #endif
   HttpErrorTabHelper::CreateForWebContents(web_contents);
-  TabUIHelper::CreateForWebContents(web_contents);
   tasks::TaskTabHelper::CreateForWebContents(web_contents);
   tpcd::metadata::TpcdMetadataDevtoolsObserver::CreateForWebContents(
       web_contents);
diff --git a/chrome/browser/ui/tab_ui_helper.cc b/chrome/browser/ui/tab_ui_helper.cc
index 43c21ca8..701bbe8e 100644
--- a/chrome/browser/ui/tab_ui_helper.cc
+++ b/chrome/browser/ui/tab_ui_helper.cc
@@ -24,9 +24,8 @@
 
 }  // namespace
 
-TabUIHelper::TabUIHelper(content::WebContents* contents)
-    : WebContentsObserver(contents),
-      content::WebContentsUserData<TabUIHelper>(*contents) {}
+TabUIHelper::TabUIHelper(tabs::TabInterface& tab_interface)
+    : ContentsObservingTabFeature(tab_interface) {}
 
 TabUIHelper::~TabUIHelper() = default;
 
@@ -78,5 +77,3 @@
     was_active_at_least_once_ = true;
   }
 }
-
-WEB_CONTENTS_USER_DATA_KEY_IMPL(TabUIHelper);
diff --git a/chrome/browser/ui/tab_ui_helper.h b/chrome/browser/ui/tab_ui_helper.h
index b7db8ed1..b581bd0 100644
--- a/chrome/browser/ui/tab_ui_helper.h
+++ b/chrome/browser/ui/tab_ui_helper.h
@@ -7,19 +7,22 @@
 
 #include <string>
 
-#include "content/public/browser/web_contents_observer.h"
-#include "content/public/browser/web_contents_user_data.h"
-#include "ui/base/models/image_model.h"
+#include "chrome/browser/ui/tabs/contents_observing_tab_feature.h"
+
+namespace tabs {
+class TabInterface;
+}
+
+namespace ui {
+class ImageModel;
+}  // namespace ui
 
 // TabUIHelper is used by UI code to obtain the title and favicon for a
 // WebContents. The values returned by TabUIHelper differ from the WebContents
 // when the WebContents hasn't loaded.
-class TabUIHelper : public content::WebContentsObserver,
-                    public content::WebContentsUserData<TabUIHelper> {
+class TabUIHelper : public tabs::ContentsObservingTabFeature {
  public:
-  TabUIHelper(const TabUIHelper&) = delete;
-  TabUIHelper& operator=(const TabUIHelper&) = delete;
-
+  explicit TabUIHelper(tabs::TabInterface& tab);
   ~TabUIHelper() override;
 
   // Get the title of the tab. When the associated WebContents' title is empty,
@@ -35,7 +38,7 @@
 
   void SetWasActiveAtLeastOnce();
 
-  // content::WebContentsObserver implementation
+  // tabs::ContentsObservingTabFeature override:
   void DidStopLoading() override;
   void OnVisibilityChanged(content::Visibility visiblity) override;
 
@@ -47,14 +50,8 @@
   }
 
  private:
-  friend class content::WebContentsUserData<TabUIHelper>;
-
-  explicit TabUIHelper(content::WebContents* contents);
-
   bool was_active_at_least_once_ = false;
   bool created_by_session_restore_ = false;
-
-  WEB_CONTENTS_USER_DATA_KEY_DECL();
 };
 
 #endif  // CHROME_BROWSER_UI_TAB_UI_HELPER_H_
diff --git a/chrome/browser/ui/tab_ui_helper_browsertest.cc b/chrome/browser/ui/tab_ui_helper_browsertest.cc
index 20c00bc..6f266f8 100644
--- a/chrome/browser/ui/tab_ui_helper_browsertest.cc
+++ b/chrome/browser/ui/tab_ui_helper_browsertest.cc
@@ -5,8 +5,10 @@
 #include "chrome/browser/ui/tab_ui_helper.h"
 
 #include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/tabs/public/tab_features.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/ui_test_utils.h"
+#include "components/tabs/public/tab_interface.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/prerender_test_util.h"
@@ -50,15 +52,19 @@
 
 IN_PROC_BROWSER_TEST_F(TabUIHelperWithPrerenderingTest,
                        ShouldNotAffectTabUIHelperOnPrerendering) {
-  GURL initial_url = embedded_test_server()->GetURL("/empty.html");
-  GURL prerender_url =
+  const GURL initial_url = embedded_test_server()->GetURL("/empty.html");
+  const GURL prerender_url =
       embedded_test_server()->GetURL("/favicon/title2_with_favicon.html");
   ASSERT_NE(ui_test_utils::NavigateToURL(browser(), initial_url), nullptr);
 
-  TabUIHelper* tab_ui_helper = TabUIHelper::FromWebContents(GetWebContents());
-  std::u16string primary_title = tab_ui_helper->GetTitle();
-  ui::ImageModel primary_favicon = tab_ui_helper->GetFavicon();
-  bool primary_should_hide_throbber = tab_ui_helper->ShouldHideThrobber();
+  TabUIHelper* const tab_ui_helper = browser()
+                                         ->tab_strip_model()
+                                         ->GetActiveTab()
+                                         ->GetTabFeatures()
+                                         ->tab_ui_helper();
+  const std::u16string primary_title = tab_ui_helper->GetTitle();
+  const ui::ImageModel primary_favicon = tab_ui_helper->GetFavicon();
+  const bool primary_should_hide_throbber = tab_ui_helper->ShouldHideThrobber();
 
   // 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
diff --git a/chrome/browser/ui/tabs/BUILD.gn b/chrome/browser/ui/tabs/BUILD.gn
index de83f6ff..fd43655d 100644
--- a/chrome/browser/ui/tabs/BUILD.gn
+++ b/chrome/browser/ui/tabs/BUILD.gn
@@ -342,7 +342,6 @@
       "//chrome/browser/ui/commerce",
       "//chrome/browser/ui/lens",
       "//chrome/browser/ui/views/intent_picker:intent_picker_page_action",
-      "//chrome/browser/ui/views/new_tab_footer",
       "//chrome/browser/ui/views/page_action",
       "//chrome/browser/ui/views/side_panel",
       "//chrome/browser/ui/webui:webui_util",
@@ -361,7 +360,6 @@
       "//components/performance_manager",
       "//components/permissions",
       "//components/pref_registry",
-      "//components/search",
       "//components/security_interstitials/content:security_interstitial_page",
       "//components/sessions",
       "//components/sync_sessions",
diff --git a/chrome/browser/ui/tabs/existing_tab_group_sub_menu_model.cc b/chrome/browser/ui/tabs/existing_tab_group_sub_menu_model.cc
index 4e61f6b1..fa42c2c75c 100644
--- a/chrome/browser/ui/tabs/existing_tab_group_sub_menu_model.cc
+++ b/chrome/browser/ui/tabs/existing_tab_group_sub_menu_model.cc
@@ -47,8 +47,7 @@
       IDS_TAB_CXMENU_PLACEHOLDER_GROUP_TITLE, group.tab_count() - 1);
   std::u16string short_title;
   gfx::ElideString(
-      TabUIHelper::FromWebContents(group.GetFirstTab()->GetContents())
-          ->GetTitle(),
+      group.GetFirstTab()->GetTabFeatures()->tab_ui_helper()->GetTitle(),
       kContextMenuTabTitleMaxLength, &short_title);
   return base::ReplaceStringPlaceholders(format_string, short_title, nullptr);
 }
diff --git a/chrome/browser/ui/tabs/public/tab_features.h b/chrome/browser/ui/tabs/public/tab_features.h
index 25ab24b..d36afb6 100644
--- a/chrome/browser/ui/tabs/public/tab_features.h
+++ b/chrome/browser/ui/tabs/public/tab_features.h
@@ -26,6 +26,7 @@
 class ReadAnythingSidePanelController;
 class SidePanelRegistry;
 class TabResourceUsageTabHelper;
+class TabUIHelper;
 class TranslatePageActionController;
 
 namespace commerce {
@@ -101,10 +102,6 @@
 class CollaborationMessagingTabData;
 }  // namespace tab_groups
 
-namespace new_tab_footer {
-class NewTabFooterController;
-}  // namespace new_tab_footer
-
 namespace tabs {
 
 class TabAlertController;
@@ -231,10 +228,6 @@
     return inactive_window_mouse_event_controller_.get();
   }
 
-  new_tab_footer::NewTabFooterController* new_tab_footer_controller() {
-    return new_tab_footer_controller_.get();
-  }
-
   TabResourceUsageTabHelper* resource_usage_helper() {
     return resource_usage_helper_.get();
   }
@@ -243,11 +236,16 @@
     return memory_saver_chip_helper_.get();
   }
 
+  TabUIHelper* tab_ui_helper() { return tab_ui_helper_.get(); }
+
   // Note: Temporary until there is a more uniform way to swap out features for
   // testing.
   TabResourceUsageTabHelper* SetResourceUsageHelperForTesting(
       std::unique_ptr<TabResourceUsageTabHelper> resource_usage_helper);
 
+  TabUIHelper* SetTabUIHelperForTesting(
+      std::unique_ptr<TabUIHelper> tab_ui_helper);
+
 #if BUILDFLAG(ENABLE_GLIC)
   glic::GlicPageContextEligibilityObserver*
   glic_page_context_eligibility_observer() {
@@ -390,15 +388,14 @@
   std::unique_ptr<FromGWSNavigationAndKeepAliveRequestObserver>
       from_gws_navigation_and_keep_alive_request_observer_;
 
-  std::unique_ptr<new_tab_footer::NewTabFooterController>
-      new_tab_footer_controller_;
-
   std::unique_ptr<TabResourceUsageTabHelper> resource_usage_helper_;
 
   std::unique_ptr<MemorySaverChipTabHelper> memory_saver_chip_helper_;
 
   std::unique_ptr<TabAlertController> tab_alert_controller_;
 
+  std::unique_ptr<TabUIHelper> tab_ui_helper_;
+
   // Must be the last member.
   base::WeakPtrFactory<TabFeatures> weak_factory_{this};
 };
diff --git a/chrome/browser/ui/tabs/tab_features.cc b/chrome/browser/ui/tabs/tab_features.cc
index 971fcaba..49c1d30 100644
--- a/chrome/browser/ui/tabs/tab_features.cc
+++ b/chrome/browser/ui/tabs/tab_features.cc
@@ -40,6 +40,7 @@
 #include "chrome/browser/ui/performance_controls/memory_saver_chip_controller.h"
 #include "chrome/browser/ui/performance_controls/memory_saver_chip_tab_helper.h"
 #include "chrome/browser/ui/performance_controls/tab_resource_usage_tab_helper.h"
+#include "chrome/browser/ui/tab_ui_helper.h"
 #include "chrome/browser/ui/tabs/alert/tab_alert_controller.h"
 #include "chrome/browser/ui/tabs/inactive_window_mouse_event_controller.h"
 #include "chrome/browser/ui/tabs/public/tab_dialog_manager.h"
@@ -55,7 +56,6 @@
 #include "chrome/browser/ui/views/commerce/price_insights_page_action_view_controller.h"
 #include "chrome/browser/ui/views/file_system_access/file_system_access_page_action_controller.h"
 #include "chrome/browser/ui/views/intent_picker/intent_picker_view_page_action_controller.h"
-#include "chrome/browser/ui/views/new_tab_footer/footer_controller.h"
 #include "chrome/browser/ui/views/page_action/action_ids.h"
 #include "chrome/browser/ui/views/page_action/page_action_controller.h"
 #include "chrome/browser/ui/views/page_action/page_action_properties_provider.h"
@@ -78,7 +78,6 @@
 #include "components/metrics/content/dwa_web_contents_observer.h"
 #include "components/passage_embeddings/passage_embeddings_features.h"
 #include "components/permissions/permission_indicators_tab_data.h"
-#include "components/search/ntp_features.h"
 #include "components/tabs/public/tab_interface.h"
 #include "net/base/features.h"
 
@@ -272,11 +271,6 @@
               tab.GetContents());
     }
 #endif  // BUILDFLAG(ENABLE_GLIC)
-
-    if (base::FeatureList::IsEnabled(ntp_features::kNtpFooter)) {
-      new_tab_footer_controller_ =
-          std::make_unique<new_tab_footer::NewTabFooterController>(&tab);
-    }
   }     // IsInNormalWindow() end.
 
   customize_chrome_side_panel_controller_ =
@@ -341,6 +335,8 @@
   tab_alert_controller_ =
       std::make_unique<TabAlertController>(tab.GetContents());
 
+  tab_ui_helper_ = std::make_unique<TabUIHelper>(tab);
+
   task_manager::WebContentsTags::CreateForTabContents(tab.GetContents());
 
 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \
@@ -358,6 +354,12 @@
   return resource_usage_helper_.get();
 }
 
+TabUIHelper* TabFeatures::SetTabUIHelperForTesting(
+    std::unique_ptr<TabUIHelper> tab_ui_helper) {
+  tab_ui_helper_ = std::move(tab_ui_helper);
+  return tab_ui_helper_.get();
+}
+
 std::unique_ptr<LensSearchController> TabFeatures::CreateLensController(
     TabInterface* tab) {
   return std::make_unique<LensSearchController>(tab);
diff --git a/chrome/browser/ui/tabs/tab_renderer_data.cc b/chrome/browser/ui/tabs/tab_renderer_data.cc
index db8f64f..6de221d 100644
--- a/chrome/browser/ui/tabs/tab_renderer_data.cc
+++ b/chrome/browser/ui/tabs/tab_renderer_data.cc
@@ -76,13 +76,13 @@
       !security_interstitial_tab_helper->IsDisplayingInterstitial() ||
       security_interstitial_tab_helper->ShouldDisplayURL();
   TabRendererData data;
-  TabUIHelper* const tab_ui_helper = TabUIHelper::FromWebContents(contents);
 
+  tabs::TabFeatures* const features = tab->GetTabFeatures();
+  TabUIHelper* const tab_ui_helper = features->tab_ui_helper();
   data.favicon = tab_ui_helper->GetFavicon();
   data.title = tab_ui_helper->GetTitle();
 
   // If the tab is in a deferred state, override favicon and title data.
-  const tabs::TabFeatures* features = tab->GetTabFeatures();
   if (features) {
     const tab_groups::SavedTabGroupWebContentsListener* wc_listener =
         features->saved_tab_group_web_contents_listener();
diff --git a/chrome/browser/ui/tabs/tab_renderer_data_unittest.cc b/chrome/browser/ui/tabs/tab_renderer_data_unittest.cc
index c6c11ed..aca4632 100644
--- a/chrome/browser/ui/tabs/tab_renderer_data_unittest.cc
+++ b/chrome/browser/ui/tabs/tab_renderer_data_unittest.cc
@@ -13,11 +13,13 @@
 #include "chrome/browser/ui/performance_controls/tab_resource_usage_tab_helper.h"
 #include "chrome/browser/ui/tab_ui_helper.h"
 #include "chrome/browser/ui/tabs/alert/tab_alert.h"
+#include "chrome/browser/ui/tabs/public/tab_features.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/tabs/test_tab_strip_model_delegate.h"
 #include "chrome/browser/ui/tabs/test_util.h"
 #include "chrome/browser/ui/thumbnails/thumbnail_tab_helper.h"
 #include "chrome/test/base/testing_profile.h"
+#include "components/tabs/public/tab_interface.h"
 #include "content/public/browser/navigation_entry.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_task_environment.h"
@@ -71,12 +73,15 @@
     if (pinned) {
       tab_strip_model_.SetTabPinned(index, true);
     }
+    TabInterface* const tab_interface = tab_strip_model_.GetTabAtIndex(index);
+    tab_interface->GetTabFeatures()->SetTabUIHelperForTesting(
+        std::make_unique<TabUIHelper>(*tab_interface));
     return index;
   }
 };
 
 TEST_F(TabRendererDataTest, FromTabInModel) {
-  int index = AddTab();
+  const int index = AddTab();
 
   TabRendererData data =
       TabRendererData::FromTabInModel(&tab_strip_model_, index);
@@ -88,8 +93,7 @@
   EXPECT_EQ(data.visible_url, GURL());
   EXPECT_EQ(data.last_committed_url, GURL());
   EXPECT_EQ(data.title,
-            TabUIHelper::FromWebContents(data.tab_interface->GetContents())
-                ->GetTitle());
+            data.tab_interface->GetTabFeatures()->tab_ui_helper()->GetTitle());
   EXPECT_FALSE(data.incognito);
   EXPECT_FALSE(data.blocked);
   EXPECT_FALSE(data.should_hide_throbber);
@@ -175,11 +179,12 @@
 
 TEST_F(TabRendererDataTest, FaviconAndIconFlags) {
   {  // Initial favicon data matches default
-    int default_index = AddTab();
-    content::WebContents* wc = tab_strip_model_.GetWebContentsAt(default_index);
+    const int default_index = AddTab();
     TabRendererData data =
         TabRendererData::FromTabInModel(&tab_strip_model_, default_index);
-    EXPECT_EQ(data.favicon, TabUIHelper::FromWebContents(wc)->GetFavicon());
+    EXPECT_EQ(
+        data.favicon,
+        data.tab_interface->GetTabFeatures()->tab_ui_helper()->GetFavicon());
     EXPECT_FALSE(data.should_themify_favicon);
     EXPECT_FALSE(data.is_monochrome_favicon);
     EXPECT_TRUE(data.show_icon);
@@ -293,9 +298,9 @@
 }
 
 TEST_F(TabRendererDataTest, ShouldHideThrobber) {
-  int index = AddTab();
-  content::WebContents* wc = tab_strip_model_.GetWebContentsAt(index);
-  TabUIHelper* helper = TabUIHelper::FromWebContents(wc);
+  const int index = AddTab();
+  TabUIHelper* const helper =
+      tab_strip_model_.GetTabAtIndex(index)->GetTabFeatures()->tab_ui_helper();
   ASSERT_NE(nullptr, helper);
   helper->set_created_by_session_restore(true);
   TabRendererData data =
diff --git a/chrome/browser/ui/tabs/tab_strip_model.cc b/chrome/browser/ui/tabs/tab_strip_model.cc
index 2d467e9..db4d689 100644
--- a/chrome/browser/ui/tabs/tab_strip_model.cc
+++ b/chrome/browser/ui/tabs/tab_strip_model.cc
@@ -790,12 +790,9 @@
   if (create_historical_tab) {
     id = delegate_->CreateHistoricalTab(tab->GetContents());
   }
-  if (tab_detach_reason == tabs::TabInterface::DetachReason::kDelete) {
-    tab->DestroyTabFeatures();
-  }
 
   std::unique_ptr<tabs::TabModel> old_tab_model =
-      RemoveTabFromIndexImpl(index_at_time_of_removal);
+      RemoveTabFromIndexImpl(index_at_time_of_removal, tab_detach_reason);
 
   old_tab_model->OnRemovedFromModel();
   return std::make_unique<DetachedTab>(
@@ -3840,18 +3837,21 @@
 }
 
 std::unique_ptr<tabs::TabModel> TabStripModel::RemoveTabFromIndexImpl(
-    int index) {
-  tabs::TabInterface* tab = GetTabAtIndex(index);
+    int index,
+    tabs::TabInterface::DetachReason tab_detach_reason) {
+  tabs::TabModel* const tab = GetTabModelAtIndex(index);
   const std::optional<tab_groups::TabGroupId> old_group = tab->GetGroup();
-
-  std::optional<int> next_selected_index =
-      DetermineNewSelectedIndex(GetTabAtIndex(index));
+  std::optional<int> next_selected_index = DetermineNewSelectedIndex(tab);
   const bool removed_tab_is_split = tab->IsSplit();
   if (removed_tab_is_split) {
     RemoveSplitImpl(tab->GetSplit().value(),
                     SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
   }
 
+  if (tab_detach_reason == tabs::TabInterface::DetachReason::kDelete) {
+    tab->DestroyTabFeatures();
+  }
+
   // Remove the tab.
   std::unique_ptr<tabs::TabModel> old_data =
       base::WrapUnique(static_cast<tabs::TabModel*>(
diff --git a/chrome/browser/ui/tabs/tab_strip_model.h b/chrome/browser/ui/tabs/tab_strip_model.h
index 5b5bd54b..e560f05c 100644
--- a/chrome/browser/ui/tabs/tab_strip_model.h
+++ b/chrome/browser/ui/tabs/tab_strip_model.h
@@ -1150,7 +1150,9 @@
 
   // Updates the `contents_data_` and sends out observer notifications for
   // removing an existing tab in  the tabstrip.
-  std::unique_ptr<tabs::TabModel> RemoveTabFromIndexImpl(int index);
+  std::unique_ptr<tabs::TabModel> RemoveTabFromIndexImpl(
+      int index,
+      tabs::TabInterface::DetachReason tab_detach_reason);
 
   // Updates the `contents_data_` and sends out observer notifications for
   // updating the index, pinned state or group property.
diff --git a/chrome/browser/ui/views/frame/browser_view.cc b/chrome/browser/ui/views/frame/browser_view.cc
index 1d15a64..ffa5ec18 100644
--- a/chrome/browser/ui/views/frame/browser_view.cc
+++ b/chrome/browser/ui/views/frame/browser_view.cc
@@ -3103,10 +3103,6 @@
     return true;
   }
 
-  if (GetLocationBarView()->ActivateFirstInactiveBubbleForAccessibility()) {
-    return true;
-  }
-
   // TODO: this fixes https://crbug.com/40668249 and https://crbug.com/40674460,
   // but a more general solution should be desirable to find any bubbles
   // anchored in the views hierarchy.
diff --git a/chrome/browser/ui/views/frame/browser_view_browsertest.cc b/chrome/browser/ui/views/frame/browser_view_browsertest.cc
index 3c0677b..c3a8f1f 100644
--- a/chrome/browser/ui/views/frame/browser_view_browsertest.cc
+++ b/chrome/browser/ui/views/frame/browser_view_browsertest.cc
@@ -380,13 +380,14 @@
 // Verifies a tab should show its favicon.
 IN_PROC_BROWSER_TEST_F(BrowserViewTest, ShowFaviconInTab) {
   // Opens "chrome://version/" page, which uses default favicon.
-  GURL version_url(chrome::kChromeUIVersionURL);
+  const GURL version_url(chrome::kChromeUIVersionURL);
   ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), version_url));
-  auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
-  auto* helper = TabUIHelper::FromWebContents(contents);
+  auto* const tab_features =
+      browser()->tab_strip_model()->GetActiveTab()->GetTabFeatures();
+  auto* const helper = tab_features->tab_ui_helper();
   ASSERT_TRUE(helper);
 
-  auto favicon = helper->GetFavicon();
+  const auto favicon = helper->GetFavicon();
   ASSERT_FALSE(favicon.IsEmpty());
 }
 
diff --git a/chrome/browser/ui/views/frame/browser_view_interactive_uitest.cc b/chrome/browser/ui/views/frame/browser_view_interactive_uitest.cc
index 6bf76e0..2375a3427ef 100644
--- a/chrome/browser/ui/views/frame/browser_view_interactive_uitest.cc
+++ b/chrome/browser/ui/views/frame/browser_view_interactive_uitest.cc
@@ -13,6 +13,7 @@
 #include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/ui/views/frame/browser_view.h"
 #include "chrome/browser/ui/views/location_bar/location_bar_view.h"
+#include "chrome/browser/ui/views/page_action/page_action_view.h"
 #include "chrome/browser/ui/views/tabs/tab_strip.h"
 #include "chrome/common/chrome_features.h"
 #include "chrome/common/pref_names.h"
@@ -28,8 +29,11 @@
 #include "ui/base/ozone_buildflags.h"
 #include "ui/base/ui_base_features.h"
 #include "ui/ozone/public/ozone_platform.h"
+#include "ui/views/bubble/bubble_dialog_delegate_view.h"
+#include "ui/views/bubble/bubble_dialog_model_host.h"
 #include "ui/views/buildflags.h"
 #include "ui/views/test/ax_event_counter.h"
+#include "ui/views/test/widget_activation_waiter.h"
 #include "ui/views/widget/widget_interactive_uitest_utils.h"
 
 #if BUILDFLAG(IS_MAC)
@@ -344,6 +348,46 @@
 }
 #endif
 
+#if BUILDFLAG(IS_MAC)
+// TODO(crbug.com/40568702) NativeWidgetMac::Deactivate is not implemented on
+// Mac.
+#define MAYBE_FocusInactivePopupForAccessibility \
+  DISABLED_FocusInactivePopupForAccessibility
+#else
+#define MAYBE_FocusInactivePopupForAccessibility \
+  FocusInactivePopupForAccessibility
+#endif
+IN_PROC_BROWSER_TEST_F(BrowserViewTest,
+                       MAYBE_FocusInactivePopupForAccessibility) {
+  std::unique_ptr<ui::DialogModel> dialog_model =
+      ui::DialogModel::Builder()
+          .SetTitle(u"test")
+          .SetIsAlertDialog()
+          .AddOkButton(base::DoNothing())
+          .Build();
+  views::View* anchor = browser_view()->GetLocationBarView();
+  auto bubble = std::make_unique<views::BubbleDialogModelHost>(
+      std::move(dialog_model), anchor, views::BubbleBorder::TOP_RIGHT);
+  bubble->set_close_on_deactivate(false);
+  views::Widget* widget =
+      views::BubbleDialogDelegate::CreateBubble(std::move(bubble));
+
+  widget->Show();
+  views::test::WaitForWidgetActive(widget, true);
+
+  widget->Deactivate();
+  widget->ShowInactive();
+  EXPECT_TRUE(widget->IsVisible());
+  EXPECT_FALSE(widget->IsActive());
+
+  browser_view()->FocusInactivePopupForAccessibility();
+  views::test::WaitForWidgetActive(widget, true);
+
+  // Ensure the bubble's widget refreshed appropriately.
+  EXPECT_TRUE(widget->IsVisible());
+  EXPECT_TRUE(widget->IsActive());
+}
+
 class BrowserViewFullscreenTest : public BrowserViewTest {
  public:
   BrowserViewFullscreenTest() {
diff --git a/chrome/browser/ui/views/frame/top_container_loading_bar.cc b/chrome/browser/ui/views/frame/top_container_loading_bar.cc
index 276fefb3..146f793 100644
--- a/chrome/browser/ui/views/frame/top_container_loading_bar.cc
+++ b/chrome/browser/ui/views/frame/top_container_loading_bar.cc
@@ -8,6 +8,8 @@
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/color/chrome_color_id.h"
 #include "chrome/browser/ui/tab_ui_helper.h"
+#include "chrome/browser/ui/tabs/public/tab_features.h"
+#include "components/tabs/public/tab_interface.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
 #include "ui/color/color_provider.h"
 #include "ui/compositor/layer.h"
@@ -129,8 +131,10 @@
     return;
   }
 
+  tabs::TabInterface* const tab_interface =
+      tabs::TabInterface::GetFromContents(web_contents());
   TabUIHelper* const tab_ui_helper =
-      TabUIHelper::FromWebContents(web_contents());
+      tab_interface->GetTabFeatures()->tab_ui_helper();
   if (tab_ui_helper->ShouldHideThrobber()) {
     HideImmediately();
     return;
diff --git a/chrome/browser/ui/views/location_bar/lens_overlay_homework_page_action_icon_view.cc b/chrome/browser/ui/views/location_bar/lens_overlay_homework_page_action_icon_view.cc
new file mode 100644
index 0000000..c93890f
--- /dev/null
+++ b/chrome/browser/ui/views/location_bar/lens_overlay_homework_page_action_icon_view.cc
@@ -0,0 +1,183 @@
+// 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/views/location_bar/lens_overlay_homework_page_action_icon_view.h"
+
+#include "chrome/browser/lens/region_search/lens_region_search_controller.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/search/search.h"
+#include "chrome/browser/ui/browser_element_identifiers.h"
+#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
+#include "chrome/browser/ui/color/chrome_color_id.h"
+#include "chrome/browser/ui/lens/lens_overlay_entry_point_controller.h"
+#include "chrome/browser/ui/lens/lens_search_controller.h"
+#include "chrome/browser/ui/views/frame/browser_view.h"
+#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
+#include "chrome/browser/ui/views/omnibox/omnibox_view_views.h"
+#include "chrome/browser/ui/views/page_action/page_action_icon_view.h"
+#include "chrome/browser/user_education/user_education_service.h"
+#include "chrome/grit/branded_strings.h"
+#include "components/lens/lens_features.h"
+#include "components/lens/lens_metrics.h"
+#include "components/omnibox/browser/omnibox_prefs.h"
+#include "components/vector_icons/vector_icons.h"
+#include "content/public/browser/navigation_controller.h"
+#include "content/public/browser/navigation_entry.h"
+#include "ui/base/metadata/metadata_impl_macros.h"
+#include "ui/views/accessibility/view_accessibility.h"
+#include "ui/views/interaction/element_tracker_views.h"
+#include "ui/views/view_class_properties.h"
+
+LensOverlayHomeworkPageActionIconView::LensOverlayHomeworkPageActionIconView(
+    IconLabelBubbleView::Delegate* parent_delegate,
+    Delegate* delegate,
+    BrowserWindowInterface* browser)
+    : PageActionIconView(nullptr,
+                         0,
+                         parent_delegate,
+                         delegate,
+                         "LensOverlayHomework"),
+      browser_(browser) {
+  CHECK(browser_);
+  image_container_view()->SetFlipCanvasOnPaintForRTLUI(false);
+
+  SetProperty(views::kElementIdentifierKey,
+              kLensOverlayHomeworkPageActionIconElementId);
+
+  SetLabel(l10n_util::GetStringUTF16(
+      IDS_CONTENT_LENS_OVERLAY_HOMEWORK_ENTRYPOINT_LABEL));
+  SetUseTonalColorsWhenExpanded(true);
+  SetBackgroundVisibility(BackgroundVisibility::kWithLabel);
+}
+
+LensOverlayHomeworkPageActionIconView::
+    ~LensOverlayHomeworkPageActionIconView() = default;
+
+void LensOverlayHomeworkPageActionIconView::UpdateImpl() {
+  const bool should_show = ShouldShow();
+
+  if (should_show) {
+    // UpdateImpl() can be called multiple times, so make sure we don't call
+    // ShowCallToAction() more than once while the chip is showing.
+    if (!scoped_window_call_to_action_ptr_) {
+      scoped_window_call_to_action_ptr_ = browser_->ShowCallToAction();
+    }
+  } else {
+    scoped_window_call_to_action_ptr_.reset();
+  }
+
+  SetVisible(should_show);
+  ResetSlideAnimation(true);
+}
+
+bool LensOverlayHomeworkPageActionIconView::ShouldShow() {
+  if (!lens::features::IsLensOverlayEduActionChipEnabled()) {
+    return false;
+  }
+
+  if (browser_->GetProfile()->IsOffTheRecord()) {
+    return false;
+  }
+
+  if (!browser_->GetProfile()->GetPrefs()->GetBoolean(
+          omnibox::kShowGoogleLensShortcut)) {
+    return false;
+  }
+
+  // Hide the homework chip if the broader lens feature is disabled.
+  const auto* controller =
+      browser_->GetFeatures().lens_overlay_entry_point_controller();
+  if (!controller || !controller->AreVisible()) {
+    return false;
+  }
+
+  auto* web_contents = GetWebContents();
+  if (!web_contents) {
+    return false;
+  }
+
+  // Don't show the chip if the location bar isn't visible yet.
+  View* location_bar_view =
+      views::ElementTrackerViews::GetInstance()->GetUniqueView(
+          kLocationBarElementId,
+          views::ElementTrackerViews::GetContextForView(this));
+  if (!location_bar_view) {
+    return false;
+  }
+
+  // Hide the homework chip if the location bar is focused.
+  const views::FocusManager* const focus_manager = GetFocusManager();
+  if (!focus_manager ||
+      location_bar_view->Contains(focus_manager->GetFocusedView())) {
+    return false;
+  }
+
+  // Treat the chip as a window-level call to action UI; only one such UI is
+  // allowed to show at a time. Check if scoped_window_call_to_action_ptr_ is
+  // already set (we are already showing the chip) before checking
+  // CanShowCallToAction().
+  if (!scoped_window_call_to_action_ptr_ && !browser_->CanShowCallToAction()) {
+    return false;
+  }
+
+  // Use the committed entry (or the visible entry, if the committed entry is
+  // the initial NavigationEntry) so the bookmarks bar disappears at the same
+  // time the page does.
+  CHECK(web_contents);
+  content::NavigationEntry* entry =
+      web_contents->GetController().GetLastCommittedEntry();
+  if (entry->IsInitialEntry()) {
+    entry = web_contents->GetController().GetVisibleEntry();
+  }
+  const GURL& url = entry->GetURL();
+
+  return controller->IsUrlEduEligible(url);
+}
+
+void LensOverlayHomeworkPageActionIconView::OnExecuting(
+    PageActionIconView::ExecuteSource source) {
+  // If the user entered Lens through the keyboard, we want to open Lens Web
+  // in a new tab.
+  // TODO(crbug.com/404640455): Clean up after a11y updates.
+  if (source == PageActionIconView::EXECUTE_SOURCE_KEYBOARD) {
+    browser_->GetFeatures().lens_region_search_controller()->Start(
+        GetWebContents(), /*use_fullscreen_capture=*/true,
+        /*is_google_default_search_provider=*/true,
+        lens::AmbientSearchEntryPoint::
+            LENS_OVERLAY_LOCATION_BAR_ACCESSIBILITY_FALLBACK);
+    return;
+  }
+
+  LensSearchController* const controller =
+      LensSearchController::FromTabWebContents(GetWebContents());
+  CHECK(controller);
+
+  controller->OpenLensOverlay(
+      lens::LensOverlayInvocationSource::kHomeworkActionChip);
+  UserEducationService::MaybeNotifyNewBadgeFeatureUsed(
+      GetWebContents()->GetBrowserContext(), lens::features::kLensOverlay);
+}
+
+views::BubbleDialogDelegate* LensOverlayHomeworkPageActionIconView::GetBubble()
+    const {
+  return nullptr;
+}
+
+const gfx::VectorIcon& LensOverlayHomeworkPageActionIconView::GetVectorIcon()
+    const {
+#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
+  return vector_icons::kGoogleLensMonochromeLogoIcon;
+#else
+  return vector_icons::kSearchChromeRefreshIcon;
+#endif
+}
+
+void LensOverlayHomeworkPageActionIconView::
+    ExecuteWithKeyboardSourceForTesting() {
+  CHECK(GetVisible());
+  OnExecuting(EXECUTE_SOURCE_KEYBOARD);
+}
+
+BEGIN_METADATA(LensOverlayHomeworkPageActionIconView)
+END_METADATA
diff --git a/chrome/browser/ui/views/location_bar/lens_overlay_homework_page_action_icon_view.h b/chrome/browser/ui/views/location_bar/lens_overlay_homework_page_action_icon_view.h
new file mode 100644
index 0000000..e8afadc
--- /dev/null
+++ b/chrome/browser/ui/views/location_bar/lens_overlay_homework_page_action_icon_view.h
@@ -0,0 +1,46 @@
+// 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.
+
+#ifndef CHROME_BROWSER_UI_VIEWS_LOCATION_BAR_LENS_OVERLAY_HOMEWORK_PAGE_ACTION_ICON_VIEW_H_
+#define CHROME_BROWSER_UI_VIEWS_LOCATION_BAR_LENS_OVERLAY_HOMEWORK_PAGE_ACTION_ICON_VIEW_H_
+
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/lens/region_search/lens_region_search_controller.h"
+#include "chrome/browser/ui/views/page_action/page_action_icon_view.h"
+#include "ui/base/metadata/metadata_header_macros.h"
+
+class BrowserWindowInterface;
+class ScopedWindowCallToAction;
+
+class LensOverlayHomeworkPageActionIconView : public PageActionIconView {
+  METADATA_HEADER(LensOverlayHomeworkPageActionIconView, PageActionIconView)
+
+ public:
+  LensOverlayHomeworkPageActionIconView(
+      IconLabelBubbleView::Delegate* parent_delegate,
+      Delegate* delegate,
+      BrowserWindowInterface* browser);
+  ~LensOverlayHomeworkPageActionIconView() override;
+
+  // PageActionIconView:
+  views::BubbleDialogDelegate* GetBubble() const override;
+  void OnExecuting(PageActionIconView::ExecuteSource execute_source) override;
+  const gfx::VectorIcon& GetVectorIcon() const override;
+
+  void ExecuteWithKeyboardSourceForTesting();
+
+ protected:
+  // PageActionIconView:
+  void UpdateImpl() override;
+
+ private:
+  bool ShouldShow();
+  void ShowCallToAction();
+
+  const raw_ptr<BrowserWindowInterface> browser_;
+
+  std::unique_ptr<ScopedWindowCallToAction> scoped_window_call_to_action_ptr_;
+};
+
+#endif  // CHROME_BROWSER_UI_VIEWS_LOCATION_BAR_LENS_OVERLAY_HOMEWORK_PAGE_ACTION_ICON_VIEW_H_
diff --git a/chrome/browser/ui/views/location_bar/lens_overlay_homework_page_action_icon_view_interactive_uitest.cc b/chrome/browser/ui/views/location_bar/lens_overlay_homework_page_action_icon_view_interactive_uitest.cc
new file mode 100644
index 0000000..dfc9428c
--- /dev/null
+++ b/chrome/browser/ui/views/location_bar/lens_overlay_homework_page_action_icon_view_interactive_uitest.cc
@@ -0,0 +1,274 @@
+// 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.
+
+// TODO(crbug.com/376283383): This file should be moved closer to the
+// `LensOverlayEntryPointController` once the page actions migration is
+// complete.
+
+#include <memory>
+
+#include "base/functional/callback_forward.h"
+#include "base/test/run_until.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/actions/chrome_action_id.h"
+#include "chrome/browser/ui/browser_element_identifiers.h"
+#include "chrome/browser/ui/tabs/public/tab_features.h"
+#include "chrome/browser/ui/ui_features.h"
+#include "chrome/browser/ui/views/frame/browser_view.h"
+#include "chrome/browser/ui/views/location_bar/lens_overlay_homework_page_action_icon_view.h"
+#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
+#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
+#include "chrome/common/webui_url_constants.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/lens/lens_features.h"
+#include "components/omnibox/browser/omnibox_prefs.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "ui/events/test/test_event.h"
+#include "ui/views/interaction/element_tracker_views.h"
+#include "ui/views/test/widget_test.h"
+#include "url/url_constants.h"
+
+using ::testing::MatchesRegex;
+
+namespace {
+
+constexpr char kDocumentWithNamedElement[] = "/select.html";
+constexpr char kDocument2[] = "/title1.html";
+
+class ViewVisibilityWaiter : public views::ViewObserver {
+ public:
+  explicit ViewVisibilityWaiter(views::View* observed_view,
+                                bool expected_visible)
+      : view_(observed_view), expected_visible_(expected_visible) {
+    observation_.Observe(view_.get());
+  }
+  ViewVisibilityWaiter(const ViewVisibilityWaiter&) = delete;
+  ViewVisibilityWaiter& operator=(const ViewVisibilityWaiter&) = delete;
+
+  ~ViewVisibilityWaiter() override = default;
+
+  // Wait for changes to occur, or return immediately if view already has
+  // expected visibility.
+  void Wait() {
+    if (expected_visible_ != view_->GetVisible()) {
+      run_loop_.Run();
+    }
+  }
+
+ private:
+  // views::ViewObserver:
+  void OnViewVisibilityChanged(views::View* observed_view,
+                               views::View* starting_view) override {
+    if (expected_visible_ == observed_view->GetVisible()) {
+      run_loop_.Quit();
+    }
+  }
+
+  raw_ptr<views::View> view_;
+  const bool expected_visible_;
+  base::RunLoop run_loop_;
+  base::ScopedObservation<views::View, views::ViewObserver> observation_{this};
+};
+
+class LensOverlayHomeworkPageActionIconViewTestBase
+    : public InProcessBrowserTest {
+ public:
+  LensOverlayHomeworkPageActionIconViewTestBase() = default;
+  LensOverlayHomeworkPageActionIconViewTestBase(
+      const LensOverlayHomeworkPageActionIconViewTestBase&) = delete;
+  LensOverlayHomeworkPageActionIconViewTestBase& operator=(
+      const LensOverlayHomeworkPageActionIconViewTestBase&) = delete;
+  ~LensOverlayHomeworkPageActionIconViewTestBase() override = default;
+
+  void SetUp() override {
+    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
+    InProcessBrowserTest::SetUp();
+  }
+
+  void SetUpOnMainThread() override {
+    InProcessBrowserTest::SetUpOnMainThread();
+    embedded_test_server()->StartAcceptingConnections();
+  }
+
+  void TearDownOnMainThread() override {
+    EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
+    InProcessBrowserTest::TearDownOnMainThread();
+  }
+
+  LensOverlayHomeworkPageActionIconView* lens_overlay_homework_icon_view() {
+    views::View* const icon_view =
+        views::ElementTrackerViews::GetInstance()->GetFirstMatchingView(
+            kLensOverlayHomeworkPageActionIconElementId,
+            browser()->window()->GetElementContext());
+    return icon_view
+               ? views::AsViewClass<LensOverlayHomeworkPageActionIconView>(
+                     icon_view)
+               : nullptr;
+  }
+
+  LocationBarView* location_bar_view() {
+    views::View* const location_bar_view =
+        views::ElementTrackerViews::GetInstance()->GetUniqueView(
+            kLocationBarElementId, browser()->window()->GetElementContext());
+    return location_bar_view
+               ? views::AsViewClass<LocationBarView>(location_bar_view)
+               : nullptr;
+  }
+
+ protected:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+class LensOverlayHomeworkPageActionIconViewTest
+    : public LensOverlayHomeworkPageActionIconViewTestBase {
+ public:
+  LensOverlayHomeworkPageActionIconViewTest() {
+    scoped_feature_list_.InitWithFeaturesAndParameters(
+        {base::test::FeatureRefAndParams(lens::features::kLensOverlay, {}),
+         base::test::FeatureRefAndParams(
+             lens::features::kLensOverlayEduActionChip,
+             {{"url-allow-filters", "[\"*\"]"},
+              {"url-path-match-allow-filters", "[\"select\"]"}})},
+        {});
+  }
+};
+
+IN_PROC_BROWSER_TEST_F(LensOverlayHomeworkPageActionIconViewTest,
+                       ShowsOnMatchingPage) {
+  // Navigate to a matching page.
+  const GURL url = embedded_test_server()->GetURL(kDocumentWithNamedElement);
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(url)));
+
+  LensOverlayHomeworkPageActionIconView* icon_view =
+      lens_overlay_homework_icon_view();
+  views::FocusManager* focus_manager = icon_view->GetFocusManager();
+  focus_manager->ClearFocus();
+  EXPECT_FALSE(focus_manager->GetFocusedView());
+  EXPECT_TRUE(icon_view->GetVisible());
+
+  // Focus in the location bar should hide the icon.
+  location_bar_view()->FocusLocation(false);
+  ViewVisibilityWaiter(icon_view, false).Wait();
+
+  EXPECT_TRUE(focus_manager->GetFocusedView());
+  EXPECT_FALSE(icon_view->GetVisible());
+}
+
+IN_PROC_BROWSER_TEST_F(LensOverlayHomeworkPageActionIconViewTest,
+                       HidesOnNonMatchingPage) {
+  // Navigate to a non-matching page.
+  const GURL url = embedded_test_server()->GetURL(kDocument2);
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(url)));
+
+  LensOverlayHomeworkPageActionIconView* icon_view =
+      lens_overlay_homework_icon_view();
+  views::FocusManager* focus_manager = icon_view->GetFocusManager();
+  focus_manager->ClearFocus();
+  EXPECT_FALSE(focus_manager->GetFocusedView());
+  EXPECT_FALSE(icon_view->GetVisible());
+
+  // Focus in the location bar should not show the icon.
+  location_bar_view()->FocusLocation(false);
+  ViewVisibilityWaiter(icon_view, false).Wait();
+
+  EXPECT_TRUE(focus_manager->GetFocusedView());
+  EXPECT_FALSE(icon_view->GetVisible());
+}
+
+#if BUILDFLAG(IS_WIN)
+#define MAYBE_OpensNewTabWhenEnteredThroughKeyboard \
+  DISABLED_OpensNewTabWhenEnteredThroughKeyboard
+#else
+#define MAYBE_OpensNewTabWhenEnteredThroughKeyboard \
+  OpensNewTabWhenEnteredThroughKeyboard
+#endif
+// Flaky failures on Windows; see https://crbug.com/419308044.
+IN_PROC_BROWSER_TEST_F(LensOverlayHomeworkPageActionIconViewTest,
+                       MAYBE_OpensNewTabWhenEnteredThroughKeyboard) {
+  const GURL url = embedded_test_server()->GetURL(kDocumentWithNamedElement);
+  // Navigate to a matching page.
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(url)));
+  // We need to wait for paint in order to take a screenshot of the page.
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    return browser()
+        ->tab_strip_model()
+        ->GetActiveTab()
+        ->GetContents()
+        ->CompletedFirstVisuallyNonEmptyPaint();
+  }));
+
+  LensOverlayHomeworkPageActionIconView* icon_view =
+      lens_overlay_homework_icon_view();
+  views::FocusManager* focus_manager = icon_view->GetFocusManager();
+  focus_manager->ClearFocus();
+  EXPECT_FALSE(focus_manager->GetFocusedView());
+  EXPECT_TRUE(icon_view->GetVisible());
+
+  // Executing the lens overlay icon view with keyboard source should open a new
+  // tab.
+  ui_test_utils::TabAddedWaiter tab_add(browser());
+  lens_overlay_homework_icon_view()->ExecuteWithKeyboardSourceForTesting();
+  auto* new_tab_contents = tab_add.Wait();
+
+  EXPECT_TRUE(new_tab_contents);
+  content::WaitForLoadStop(new_tab_contents);
+  EXPECT_THAT(new_tab_contents->GetLastCommittedURL().query(),
+              MatchesRegex("ep=crmntob&re=df&s=4&st=\\d+&lm=.+"));
+}
+
+IN_PROC_BROWSER_TEST_F(LensOverlayHomeworkPageActionIconViewTest,
+                       DoesNotShowWhenSettingDisabled) {
+  // Disable the setting.
+  browser()->profile()->GetPrefs()->SetBoolean(omnibox::kShowGoogleLensShortcut,
+                                               false);
+  const GURL url = embedded_test_server()->GetURL(kDocumentWithNamedElement);
+  // Navigate to a matching page.
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(url)));
+
+  LensOverlayHomeworkPageActionIconView* icon_view =
+      lens_overlay_homework_icon_view();
+  views::FocusManager* focus_manager = icon_view->GetFocusManager();
+  focus_manager->ClearFocus();
+  EXPECT_FALSE(focus_manager->GetFocusedView());
+  EXPECT_FALSE(icon_view->GetVisible());
+
+  // Focus in the location bar should not show the icon.
+  location_bar_view()->FocusLocation(false);
+  ViewVisibilityWaiter(icon_view, false).Wait();
+
+  EXPECT_TRUE(focus_manager->GetFocusedView());
+  EXPECT_FALSE(icon_view->GetVisible());
+}
+
+IN_PROC_BROWSER_TEST_F(LensOverlayHomeworkPageActionIconViewTest,
+                       RespectsShowShortcutPreference) {
+  // Ensure the shortcut pref starts enabled.
+  browser()->profile()->GetPrefs()->SetBoolean(omnibox::kShowGoogleLensShortcut,
+                                               true);
+
+  const GURL url = embedded_test_server()->GetURL(kDocumentWithNamedElement);
+  // Navigate to a matching page.
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(url)));
+
+  views::View* icon_view = lens_overlay_homework_icon_view();
+  views::FocusManager* focus_manager = icon_view->GetFocusManager();
+  focus_manager->ClearFocus();
+  EXPECT_FALSE(focus_manager->GetFocusedView());
+  EXPECT_TRUE(icon_view->GetVisible());
+
+  // Disable the preference, the entrypoint should immediately disappear.
+  browser()->profile()->GetPrefs()->SetBoolean(omnibox::kShowGoogleLensShortcut,
+                                               false);
+  EXPECT_FALSE(icon_view->GetVisible());
+
+  // Re-enable the preference, the entrypoint should immediately become
+  // visible.
+  browser()->profile()->GetPrefs()->SetBoolean(omnibox::kShowGoogleLensShortcut,
+                                               true);
+  EXPECT_TRUE(icon_view->GetVisible());
+}
+
+}  // namespace
diff --git a/chrome/browser/ui/views/location_bar/location_bar_view.cc b/chrome/browser/ui/views/location_bar/location_bar_view.cc
index baf90f1..55e2295 100644
--- a/chrome/browser/ui/views/location_bar/location_bar_view.cc
+++ b/chrome/browser/ui/views/location_bar/location_bar_view.cc
@@ -40,6 +40,7 @@
 #include "chrome/browser/ui/autofill/payments/save_card_bubble_controller_impl.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_actions.h"
+#include "chrome/browser/ui/browser_element_identifiers.h"
 #include "chrome/browser/ui/browser_finder.h"
 #include "chrome/browser/ui/browser_window.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_features.h"
@@ -166,6 +167,7 @@
 #include "ui/views/style/typography.h"
 #include "ui/views/style/typography_provider.h"
 #include "ui/views/view.h"
+#include "ui/views/view_class_properties.h"
 #include "ui/views/view_utils.h"
 #include "ui/views/widget/widget.h"
 
@@ -233,6 +235,7 @@
           &LocationBarView::OnAppShimChanged, base::Unretained(this)));
 #endif
   GetViewAccessibility().SetRole(ax::mojom::Role::kGroup);
+  SetProperty(views::kElementIdentifierKey, kLocationBarElementId);
 }
 
 LocationBarView::~LocationBarView() = default;
@@ -436,6 +439,14 @@
     }
   }
 
+  if (browser_ && lens::features::IsLensOverlayEduActionChipEnabled()) {
+    // Position in the leading position, like the expanding entrypoint for
+    // kLensOverlay above. While both chips may be enabled, they will not appear
+    // at the same time due to different focus behavior.
+    params.types_enabled.insert(params.types_enabled.begin(),
+                                PageActionIconType::kLensOverlayHomework);
+  }
+
   if (browser_ && tab_groups::SavedTabGroupUtils::SupportsSharedTabGroups()) {
     params.types_enabled.push_back(PageActionIconType::kCollaborationMessaging);
   }
@@ -1019,11 +1030,6 @@
   omnibox_view_->ResetTabState(contents);
 }
 
-bool LocationBarView::ActivateFirstInactiveBubbleForAccessibility() {
-  return page_action_icon_controller_
-      ->ActivateFirstInactiveBubbleForAccessibility();
-}
-
 ChipController* LocationBarView::GetChipController() {
   if (base::FeatureList::IsEnabled(
           content_settings::features::kLeftHandSideActivityIndicators)) {
diff --git a/chrome/browser/ui/views/location_bar/location_bar_view.h b/chrome/browser/ui/views/location_bar/location_bar_view.h
index 9d98ffa..87ebe76 100644
--- a/chrome/browser/ui/views/location_bar/location_bar_view.h
+++ b/chrome/browser/ui/views/location_bar/location_bar_view.h
@@ -196,10 +196,6 @@
   // Clears the location bar's state for |contents|.
   void ResetTabState(content::WebContents* contents);
 
-  // Activates the first visible but inactive PageActionIconView for
-  // accessibility.
-  bool ActivateFirstInactiveBubbleForAccessibility();
-
   // Controls the chip in the LocationBarView.
   ChipController* GetChipController();
 
diff --git a/chrome/browser/ui/views/location_bar/location_icon_view_interactive_uitest.cc b/chrome/browser/ui/views/location_bar/location_icon_view_interactive_uitest.cc
index 9b54387..f09e7af 100644
--- a/chrome/browser/ui/views/location_bar/location_icon_view_interactive_uitest.cc
+++ b/chrome/browser/ui/views/location_bar/location_icon_view_interactive_uitest.cc
@@ -63,59 +63,4 @@
   EXPECT_EQ(PageInfoBubbleView::BUBBLE_NONE,
             PageInfoBubbleView::GetShownBubbleType());
 }
-
-#if BUILDFLAG(IS_MAC)
-// TODO(jongkwon.lee): https://crbug.com/825834 NativeWidgetMac::Deactivate is
-// not implemented on Mac.
-#define MAYBE_ActivateFirstInactiveBubbleForAccessibility \
-  DISABLED_ActivateFirstInactiveBubbleForAccessibility
-#else
-#define MAYBE_ActivateFirstInactiveBubbleForAccessibility \
-  ActivateFirstInactiveBubbleForAccessibility
-#endif
-IN_PROC_BROWSER_TEST_F(LocationIconViewTest,
-                       MAYBE_ActivateFirstInactiveBubbleForAccessibility) {
-  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
-  LocationBarView* location_bar_view = browser_view->GetLocationBarView();
-  EXPECT_FALSE(
-      location_bar_view->ActivateFirstInactiveBubbleForAccessibility());
-
-  content::WebContents* web_contents =
-      browser()->tab_strip_model()->GetActiveWebContents();
-  browser_view->ShowTranslateBubble(
-      web_contents, translate::TRANSLATE_STEP_AFTER_TRANSLATE, "en", "fr",
-      translate::TranslateErrors::NONE, true);
-
-  views::View* icon_view;
-  if (IsPageActionMigrated(PageActionIconType::kTranslate)) {
-    icon_view = browser_view->toolbar_button_provider()->GetPageActionView(
-        kActionShowTranslate);
-  } else {
-    icon_view = browser_view->toolbar_button_provider()->GetPageActionIconView(
-        PageActionIconType::kTranslate);
-  }
-
-  ASSERT_TRUE(icon_view);
-  EXPECT_TRUE(icon_view->GetVisible());
-
-  // Ensure the bubble's widget is visible, but inactive. Active widgets are
-  // focused by accessibility, so not of concern.
-  views::Widget* widget = browser()
-                              ->GetFeatures()
-                              .translate_bubble_controller()
-                              ->GetTranslateBubble()
-                              ->GetWidget();
-  widget->Deactivate();
-  widget->ShowInactive();
-  EXPECT_TRUE(widget->IsVisible());
-  EXPECT_FALSE(widget->IsActive());
-
-  EXPECT_TRUE(location_bar_view->ActivateFirstInactiveBubbleForAccessibility());
-
-  // Ensure the bubble's widget refreshed appropriately.
-  EXPECT_TRUE(icon_view->GetVisible());
-  EXPECT_TRUE(widget->IsVisible());
-  EXPECT_TRUE(widget->IsActive());
-}
-
 }  // namespace
diff --git a/chrome/browser/ui/views/media_router/cast_dialog_coordinator_unittest.cc b/chrome/browser/ui/views/media_router/cast_dialog_coordinator_browsertest.cc
similarity index 87%
rename from chrome/browser/ui/views/media_router/cast_dialog_coordinator_unittest.cc
rename to chrome/browser/ui/views/media_router/cast_dialog_coordinator_browsertest.cc
index 72dbffd..eebf2e8e 100644
--- a/chrome/browser/ui/views/media_router/cast_dialog_coordinator_unittest.cc
+++ b/chrome/browser/ui/views/media_router/cast_dialog_coordinator_browsertest.cc
@@ -8,15 +8,17 @@
 
 #include "base/test/scoped_feature_list.h"
 #include "base/time/time.h"
+#include "chrome/browser/ui/browser_tabstrip.h"
 #include "chrome/browser/ui/media_router/cast_dialog_controller.h"
 #include "chrome/browser/ui/media_router/media_cast_mode.h"
 #include "chrome/browser/ui/media_router/media_route_starter.h"
 #include "chrome/browser/ui/views/frame/browser_view.h"
-#include "chrome/browser/ui/views/frame/test_with_browser_view.h"
 #include "chrome/browser/ui/views/frame/top_container_view.h"
 #include "chrome/browser/ui/views/media_router/cast_dialog_view.h"
 #include "chrome/browser/ui/views/toolbar/toolbar_view.h"
+#include "chrome/test/base/in_process_browser_test.h"
 #include "content/public/browser/web_contents.h"
+#include "content/public/test/browser_test.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/views/test/widget_test.h"
@@ -45,9 +47,9 @@
   MOCK_METHOD(void, RegisterDestructor, (base::OnceClosure));
 };
 
-class CastDialogCoordinatorTest : public TestWithBrowserView {
+class CastDialogCoordinatorTest : public InProcessBrowserTest {
  public:
-  void SetUp() override { TestWithBrowserView::SetUp(); }
+  void SetUp() override { InProcessBrowserTest::SetUp(); }
 
   NiceMock<MockCastDialogController> controller_;
   CastDialogCoordinator cast_dialog_coordinator_;
@@ -56,10 +58,10 @@
 // Tests show and hide for ShowDialogCenteredForBrowserWindow. Defers
 // ShowDialogWithToolbarAction to Media Router tests (already covered) since
 // additional Media Router services setup is required.
-TEST_F(CastDialogCoordinatorTest, ShowAndHideDialog) {
+IN_PROC_BROWSER_TEST_F(CastDialogCoordinatorTest, ShowAndHideDialog) {
   EXPECT_CALL(controller_, AddObserver(_));
   cast_dialog_coordinator_.ShowDialogCenteredForBrowserWindow(
-      &controller_, browser_view()->browser(), base::Time::Now(),
+      &controller_, browser(), base::Time::Now(),
       MediaRouterDialogActivationLocation::PAGE);
   EXPECT_TRUE(cast_dialog_coordinator_.IsShowing());
   EXPECT_NE(nullptr, cast_dialog_coordinator_.GetCastDialogWidget());
diff --git a/chrome/browser/ui/views/new_tab_footer/BUILD.gn b/chrome/browser/ui/views/new_tab_footer/BUILD.gn
index 7cbb750..4e7ffbc1 100644
--- a/chrome/browser/ui/views/new_tab_footer/BUILD.gn
+++ b/chrome/browser/ui/views/new_tab_footer/BUILD.gn
@@ -15,6 +15,7 @@
     "//base:base",
     "//chrome/browser/profiles:profile",
     "//chrome/browser/resources/new_tab_footer:resources_grit",
+    "//chrome/browser/search",
     "//chrome/browser/ui:ui_features",
     "//chrome/browser/ui/browser_window",
     "//chrome/browser/ui/webui:webui_util",
@@ -27,15 +28,19 @@
     "//ui/views",
     "//ui/views/controls/webview",
   ]
+  public_deps = [ "//chrome/browser:browser_public_dependencies" ]
 }
 
-source_set("unit_tests") {
+source_set("browser_tests") {
   testonly = true
-  sources = [ "footer_controller_unittest.cc" ]
+  defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]
+  sources = [ "footer_controller_browsertest.cc" ]
   deps = [
     ":new_tab_footer",
     "//base/test:test_support",
+    "//chrome/browser/search_engines",
     "//chrome/browser/ui:ui_features",
+    "//chrome/browser/ui/browser_window",
     "//chrome/test:test_support",
   ]
 }
diff --git a/chrome/browser/ui/views/new_tab_footer/footer_controller.cc b/chrome/browser/ui/views/new_tab_footer/footer_controller.cc
index 111c192..b6b92d3b 100644
--- a/chrome/browser/ui/views/new_tab_footer/footer_controller.cc
+++ b/chrome/browser/ui/views/new_tab_footer/footer_controller.cc
@@ -4,82 +4,111 @@
 
 #include "chrome/browser/ui/views/new_tab_footer/footer_controller.h"
 
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/enterprise/util/managed_browser_utils.h"
+#include "chrome/browser/search/search.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
 #include "chrome/browser/ui/ui_features.h"
+#include "chrome/browser/ui/views/frame/browser_view.h"
 #include "chrome/browser/ui/views/new_tab_footer/footer_web_view.h"
 #include "chrome/browser/ui/webui/new_tab_footer/new_tab_footer_helper.h"
+#include "chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.h"
+#include "chrome/browser/ui/webui/new_tab_page_third_party/new_tab_page_third_party_ui.h"
+#include "chrome/browser/ui/webui/ntp/new_tab_ui.h"
 #include "chrome/common/pref_names.h"
 #include "components/prefs/pref_service.h"
+#include "components/tabs/public/tab_interface.h"
 #include "content/public/browser/navigation_entry.h"
 
 namespace new_tab_footer {
 
-// TODO (crbug.com/415116344) add unittest coverage.
-NewTabFooterController::NewTabFooterController(tabs::TabInterface* tab)
-    : tab_(tab) {
-  // TODO(crbug.com/4438803): Support SideBySide.
-  if (features::IsNtpFooterEnabledWithoutSideBySide()) {
-    footer_web_view_ = tab_->GetBrowserWindowInterface()->NewTabFooterWebView();
+namespace {
+bool IsNtp(const GURL& url,
+           content::WebContents* web_contents,
+           Profile* profile) {
+  content::NavigationEntry* entry =
+      web_contents->GetController().GetLastCommittedEntry();
+  if (entry->IsInitialEntry()) {
+    entry = web_contents->GetController().GetVisibleEntry();
   }
+  return NewTabUI::IsNewTab(url) || NewTabPageUI::IsNewTabPageOrigin(url) ||
+         NewTabPageThirdPartyUI::IsNewTabPageOrigin(url) ||
+         search::NavEntryIsInstantNTP(web_contents, entry) ||
+         ntp_footer::IsExtensionNtp(url, profile);
+}
+}  // namespace
 
-  profile_ = tab_->GetBrowserWindowInterface()->GetProfile();
+NewTabFooterController::NewTabFooterController(BrowserWindowInterface* browser,
+                                               NewTabFooterWebView* footer)
+    : browser_(browser), footer_(footer) {
+  profile_ = browser_->GetProfile();
   pref_change_registrar_.Init(profile_->GetPrefs());
   pref_change_registrar_.Add(
       prefs::kNtpFooterVisible,
       base::BindRepeating(&NewTabFooterController::UpdateFooterVisibility,
                           weak_factory_.GetWeakPtr()));
-
-  content::WebContentsObserver::Observe(tab_->GetContents());
-  tab_did_activate_callback_subscription_ = tab_->RegisterDidActivate(
-      base::BindRepeating(&NewTabFooterController::TabForegrounded,
+  pref_change_registrar_.Add(
+      prefs::kNTPFooterExtensionAttributionEnabled,
+      base::BindRepeating(&NewTabFooterController::UpdateFooterVisibility,
                           weak_factory_.GetWeakPtr()));
+#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
+  local_state_pref_change_registrar_.Init(g_browser_process->local_state());
+  local_state_pref_change_registrar_.Add(
+      prefs::kNTPFooterManagementNoticeEnabled,
+      base::BindRepeating(&NewTabFooterController::UpdateFooterVisibility,
+                          weak_factory_.GetWeakPtr()));
+#endif
+
+  tab_activation_subscription_subscription_ =
+      browser_->RegisterActiveTabDidChange(
+          base::BindRepeating(&NewTabFooterController::OnActiveTabChanged,
+                              weak_factory_.GetWeakPtr()));
 }
 
 NewTabFooterController::~NewTabFooterController() = default;
 
+void NewTabFooterController::TearDown() {
+  pref_change_registrar_.Reset();
+  tab_activation_subscription_subscription_ = base::CallbackListSubscription();
+  footer_ = nullptr;
+  browser_ = nullptr;
+  profile_ = nullptr;
+}
+
 void NewTabFooterController::DidFinishNavigation(
     content::NavigationHandle* navigation_handle) {
-  if (tab_->IsActivated()) {
     UpdateFooterVisibility();
-  }
 }
 
 void NewTabFooterController::UpdateFooterVisibility() {
-  if (!footer_web_view_) {
+  // TODO(crbug.com/4438803): Support SideBySide. Currently, when it is enabled,
+  // footer_ will have no value.
+  if (!footer_) {
     return;
   }
 
-  GURL url =
-      tab_->GetContents()->GetController().GetLastCommittedEntry()->GetURL();
+  GURL url = web_contents()->GetController().GetLastCommittedEntry()->GetURL();
   if (url.is_empty()) {
-    url = tab_->GetContents()->GetController().GetVisibleEntry()->GetURL();
+    url = web_contents()->GetController().GetVisibleEntry()->GetURL();
   }
 
-  bool is_footer_visible_pref =
-      profile_->GetPrefs()->GetBoolean(prefs::kNtpFooterVisible);
-  bool can_show_footer =
-      is_footer_visible_pref && ntp_footer::IsExtensionNtp(url, profile_);
-  if (can_show_footer) {
-    ShowUI();
+  bool managed_ntp =
+      IsNtp(url, web_contents(), profile_) &&
+      enterprise_util::CanShowEnterpriseBadgingForNTPFooter(profile_);
+  bool show = managed_ntp ||
+              (profile_->GetPrefs()->GetBoolean(prefs::kNtpFooterVisible) &&
+               ntp_footer::CanShowExtensionFooter(url, profile_));
+  if (show) {
+    footer_->ShowUI();
   } else {
-    CloseUI();
+    footer_->CloseUI();
   }
 }
 
-void NewTabFooterController::TabForegrounded(tabs::TabInterface* tab) {
+void NewTabFooterController::OnActiveTabChanged(
+    BrowserWindowInterface* browser) {
+  Observe(browser->GetActiveTabInterface()->GetContents());
   UpdateFooterVisibility();
 }
 
-void NewTabFooterController::ShowUI() {
-  if (footer_web_view_) {
-    footer_web_view_->ShowUI();
-  }
-}
-
-void NewTabFooterController::CloseUI() {
-  if (footer_web_view_) {
-    footer_web_view_->CloseUI();
-  }
-}
-
 }  // namespace new_tab_footer
diff --git a/chrome/browser/ui/views/new_tab_footer/footer_controller.h b/chrome/browser/ui/views/new_tab_footer/footer_controller.h
index ce32123..2f650be3 100644
--- a/chrome/browser/ui/views/new_tab_footer/footer_controller.h
+++ b/chrome/browser/ui/views/new_tab_footer/footer_controller.h
@@ -7,34 +7,39 @@
 
 #include "chrome/browser/ui/views/new_tab_footer/footer_web_view.h"
 #include "components/prefs/pref_change_registrar.h"
-#include "components/tabs/public/tab_interface.h"
 #include "content/public/browser/web_contents_observer.h"
 
+class BrowserWindowInterface;
+
 namespace new_tab_footer {
 
 // Class used to manage the state of the new tab footer.
 class NewTabFooterController : public content::WebContentsObserver {
  public:
-  explicit NewTabFooterController(tabs::TabInterface* tab);
+  explicit NewTabFooterController(BrowserWindowInterface* browser,
+                                  NewTabFooterWebView* footer);
   NewTabFooterController(const NewTabFooterController&) = delete;
   NewTabFooterController& operator=(const NewTabFooterController&) = delete;
   ~NewTabFooterController() override;
 
+  void TearDown();
+
  private:
   // content::WebContentsObserver:
   void DidFinishNavigation(
       content::NavigationHandle* navigation_handle) override;
 
   void UpdateFooterVisibility();
-  // Called when the associated tab enters the foreground.
-  void TabForegrounded(tabs::TabInterface* tab);
+  // Callback for active tab changes from BrowserWindowInterface.
+  void OnActiveTabChanged(BrowserWindowInterface* browser);
   void ShowUI();
   void CloseUI();
 
-  const raw_ptr<tabs::TabInterface> tab_;
-  raw_ptr<new_tab_footer::NewTabFooterWebView> footer_web_view_;
-  base::CallbackListSubscription tab_did_activate_callback_subscription_;
+  raw_ptr<BrowserWindowInterface> browser_;
+  raw_ptr<new_tab_footer::NewTabFooterWebView> footer_;
+  base::CallbackListSubscription tab_activation_subscription_subscription_;
   PrefChangeRegistrar pref_change_registrar_;
+  PrefChangeRegistrar local_state_pref_change_registrar_;
   raw_ptr<Profile> profile_;
 
   base::WeakPtrFactory<NewTabFooterController> weak_factory_{this};
diff --git a/chrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.cc b/chrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.cc
new file mode 100644
index 0000000..c0c4aeb1
--- /dev/null
+++ b/chrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.cc
@@ -0,0 +1,259 @@
+// 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 <memory>
+
+#include "base/test/scoped_feature_list.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/enterprise/browser_management/management_service_factory.h"
+#include "chrome/browser/extensions/extension_browsertest.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
+#include "chrome/browser/ui/ui_features.h"
+#include "chrome/browser/ui/views/frame/browser_view.h"
+#include "chrome/browser/ui/views/new_tab_footer/footer_web_view.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/chrome_test_utils.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/policy/core/common/management/scoped_management_service_override_for_testing.h"
+#include "components/prefs/pref_service.h"
+#include "components/search/ntp_features.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "extensions/common/extension.h"
+#include "extensions/test/test_extension_dir.h"
+#include "net/base/url_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/base/window_open_disposition.h"
+#include "url/gurl.h"
+
+namespace {
+const char kNonNtpUrl[] = "https://www.google.com";
+}
+
+class FooterControllerExtensionTestBase
+    : public extensions::ExtensionBrowserTest {
+ public:
+  void SetUpOnMainThread() override {
+    extensions::ExtensionBrowserTest::SetUpOnMainThread();
+    profile()->GetPrefs()->SetBoolean(prefs::kNtpFooterVisible, true);
+  }
+
+  scoped_refptr<const extensions::Extension> LoadNtpExtension() {
+    extensions::TestExtensionDir extension_dir;
+    constexpr char kManifest[] = R"(
+                            {
+                              "chrome_url_overrides": {
+                                  "newtab": "ext.html"
+                              },
+                              "name": "Extension-overridden NTP",
+                              "manifest_version": 3,
+                              "version": "0.1"
+                            })";
+    extension_dir.WriteManifest(kManifest);
+    extension_dir.WriteFile(FILE_PATH_LITERAL("ext.html"),
+                            "<body>Extension-overridden NTP</body>");
+    scoped_refptr<const extensions::Extension> extension =
+        LoadExtension(extension_dir.Pack());
+    return extension;
+  }
+
+  void NavigateCurrentTab(const GURL& url) {
+    ui_test_utils::NavigateToURLWithDisposition(
+        browser(), url, WindowOpenDisposition::CURRENT_TAB,
+        ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
+  }
+
+  void OpenNewTab(const GURL& url) {
+    ui_test_utils::NavigateToURLWithDisposition(
+        browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
+        ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
+  }
+
+  new_tab_footer::NewTabFooterWebView* footer() {
+    return BrowserView::GetBrowserViewForBrowser(browser())
+        ->new_tab_footer_web_view();
+  }
+
+ protected:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+class FooterControllerExtensionTest : public FooterControllerExtensionTestBase {
+ public:
+  FooterControllerExtensionTest() {
+    feature_list_.InitWithFeatures(
+        /*enabled_features=*/{ntp_features::kNtpFooter},
+        /*disabled_features=*/{features::kSideBySide,
+                               features::kEnterpriseBadgingForNtpFooter});
+  }
+  ~FooterControllerExtensionTest() override = default;
+};
+
+IN_PROC_BROWSER_TEST_F(FooterControllerExtensionTest,
+                       FooterShown_ExtensionNTP) {
+  auto extension = LoadNtpExtension();
+  ASSERT_FALSE(footer()->GetVisible());
+
+  OpenNewTab(GURL(extension->url()));
+  EXPECT_TRUE(footer()->GetVisible());
+
+  NavigateCurrentTab(extension->url());
+  EXPECT_TRUE(footer()->GetVisible());
+}
+
+IN_PROC_BROWSER_TEST_F(FooterControllerExtensionTest,
+                       FooterHidden_NonExtensionNTP) {
+  auto extension = LoadNtpExtension();
+  ASSERT_FALSE(footer()->GetVisible());
+  NavigateCurrentTab(extension->url());
+  EXPECT_TRUE(footer()->GetVisible());
+
+  NavigateCurrentTab(GURL(kNonNtpUrl));
+  EXPECT_FALSE(footer()->GetVisible());
+
+  OpenNewTab(GURL(chrome::kChromeUINewTabPageURL));
+  EXPECT_FALSE(footer()->GetVisible());
+}
+
+IN_PROC_BROWSER_TEST_F(FooterControllerExtensionTest, UserPrefChanged) {
+  profile()->GetPrefs()->SetBoolean(prefs::kNtpFooterVisible, false);
+  auto extension = LoadNtpExtension();
+  NavigateCurrentTab(extension->url());
+  ASSERT_FALSE(footer()->GetVisible());
+
+  profile()->GetPrefs()->SetBoolean(prefs::kNtpFooterVisible, true);
+  EXPECT_TRUE(footer()->GetVisible());
+
+  profile()->GetPrefs()->SetBoolean(prefs::kNtpFooterVisible, false);
+  EXPECT_FALSE(footer()->GetVisible());
+}
+
+IN_PROC_BROWSER_TEST_F(FooterControllerExtensionTest,
+                       AttributionPolicyChanged) {
+  auto extension = LoadNtpExtension();
+  ASSERT_FALSE(footer()->GetVisible());
+
+  NavigateCurrentTab(extension->url());
+  EXPECT_TRUE(footer()->GetVisible());
+
+  profile()->GetPrefs()->SetBoolean(
+      prefs::kNTPFooterExtensionAttributionEnabled, false);
+  EXPECT_FALSE(footer()->GetVisible());
+
+  profile()->GetPrefs()->SetBoolean(
+      prefs::kNTPFooterExtensionAttributionEnabled, true);
+  EXPECT_TRUE(footer()->GetVisible());
+}
+
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
+class FooterControllerEnterpriseTest
+    : public FooterControllerExtensionTestBase,
+      public testing::WithParamInterface<bool> {
+ public:
+  FooterControllerEnterpriseTest() {
+    feature_list_.InitWithFeatures(
+        /*enabled_features=*/{ntp_features::kNtpFooter,
+                              features::kEnterpriseBadgingForNtpFooter},
+        /*disabled_features=*/{features::kSideBySide});
+  }
+  ~FooterControllerEnterpriseTest() override = default;
+
+  bool managed() { return GetParam(); }
+  PrefService* local_state() { return g_browser_process->local_state(); }
+};
+
+INSTANTIATE_TEST_SUITE_P(, FooterControllerEnterpriseTest, testing::Bool());
+
+IN_PROC_BROWSER_TEST_P(FooterControllerEnterpriseTest,
+                       FooterShown_NoticeEnabled) {
+  policy::ScopedManagementServiceOverrideForTesting
+      profile_supervised_management(
+          policy::ManagementServiceFactory::GetForProfile(profile()),
+          managed() ? policy::EnterpriseManagementAuthority::DOMAIN_LOCAL
+                    : policy::EnterpriseManagementAuthority::NONE);
+
+  // Non-NTP
+  ASSERT_FALSE(footer()->GetVisible());
+
+  // Default NTP
+  NavigateCurrentTab(GURL(chrome::kChromeUINewTabURL));
+  EXPECT_EQ(managed(), footer()->GetVisible());
+
+  // 1P NTP
+  NavigateCurrentTab(GURL(chrome::kChromeUINewTabPageURL));
+  EXPECT_EQ(managed(), footer()->GetVisible());
+
+  // 3P NTP
+  NavigateCurrentTab(GURL(chrome::kChromeUINewTabPageThirdPartyURL));
+  EXPECT_EQ(managed(), footer()->GetVisible());
+
+  // Extension NTP
+  auto extension = LoadNtpExtension();
+  NavigateCurrentTab(extension->url());
+  EXPECT_TRUE(footer()->GetVisible());
+
+  // Non-NTP
+  NavigateCurrentTab(GURL(kNonNtpUrl));
+  EXPECT_FALSE(footer()->GetVisible());
+}
+
+IN_PROC_BROWSER_TEST_P(FooterControllerEnterpriseTest,
+                       FooterHidden_NoticeDisabled) {
+  policy::ScopedManagementServiceOverrideForTesting
+      profile_supervised_management(
+          policy::ManagementServiceFactory::GetForProfile(profile()),
+          managed() ? policy::EnterpriseManagementAuthority::DOMAIN_LOCAL
+                    : policy::EnterpriseManagementAuthority::NONE);
+  local_state()->SetBoolean(prefs::kNTPFooterManagementNoticeEnabled, false);
+
+  NavigateCurrentTab(GURL(chrome::kChromeUINewTabPageURL));
+  EXPECT_FALSE(footer()->GetVisible());
+}
+
+IN_PROC_BROWSER_TEST_P(FooterControllerEnterpriseTest,
+                       FooterHidden_NoticePolicyChanged) {
+  policy::ScopedManagementServiceOverrideForTesting
+      profile_supervised_management(
+          policy::ManagementServiceFactory::GetForProfile(profile()),
+          managed() ? policy::EnterpriseManagementAuthority::DOMAIN_LOCAL
+                    : policy::EnterpriseManagementAuthority::NONE);
+  ASSERT_FALSE(footer()->GetVisible());
+
+  NavigateCurrentTab(GURL(chrome::kChromeUINewTabURL));
+  EXPECT_EQ(managed(), footer()->GetVisible());
+
+  local_state()->SetBoolean(prefs::kNTPFooterManagementNoticeEnabled, false);
+  EXPECT_FALSE(footer()->GetVisible());
+
+  local_state()->SetBoolean(prefs::kNTPFooterManagementNoticeEnabled, true);
+  EXPECT_EQ(managed(), footer()->GetVisible());
+}
+#endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
+
+// TODO(crbug.com/4438803): Once the controller supports SideBySide enablement,
+// refactor `FooterControllerExtensionTest` into a value-parameterized test,
+// making `FooterControllerSideBySideTest` and
+// `FooterControllerExtensionTestBase` redundant.
+class FooterControllerSideBySideTest : public InProcessBrowserTest {
+ public:
+  FooterControllerSideBySideTest() {
+    feature_list_.InitWithFeatures(
+        /*enabled_features=*/{ntp_features::kNtpFooter, features::kSideBySide,
+                              features::kEnterpriseBadgingForNtpFooter},
+        /*disabled_features=*/{});
+  }
+  ~FooterControllerSideBySideTest() override = default;
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(FooterControllerSideBySideTest, FooterNotCreated) {
+  auto* footer = BrowserView::GetBrowserViewForBrowser(browser())
+                     ->new_tab_footer_web_view();
+  EXPECT_FALSE(footer);
+}
diff --git a/chrome/browser/ui/views/new_tab_footer/footer_controller_unittest.cc b/chrome/browser/ui/views/new_tab_footer/footer_controller_unittest.cc
deleted file mode 100644
index 8e90ca5..0000000
--- a/chrome/browser/ui/views/new_tab_footer/footer_controller_unittest.cc
+++ /dev/null
@@ -1,235 +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/views/new_tab_footer/footer_controller.h"
-
-#include "base/test/scoped_feature_list.h"
-#include "chrome/browser/extensions/chrome_test_extension_loader.h"
-#include "chrome/browser/extensions/extension_service_test_base.h"
-#include "chrome/browser/extensions/extension_web_ui.h"
-#include "chrome/browser/ui/browser_window/test/mock_browser_window_interface.h"
-#include "chrome/browser/ui/tabs/test/mock_tab_interface.h"
-#include "chrome/browser/ui/ui_features.h"
-#include "chrome/browser/ui/views/new_tab_footer/footer_web_view.h"
-#include "chrome/common/pref_names.h"
-#include "chrome/test/base/testing_profile.h"
-#include "components/search/ntp_features.h"
-#include "content/public/test/web_contents_tester.h"
-#include "extensions/browser/extension_registry.h"
-#include "extensions/common/extension_builder.h"
-#include "extensions/test/test_extension_dir.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace {
-const char kNonExtensionNtpUrl[] = "https://www.google.com";
-}
-
-class FakeNewTabFooterWebView : public new_tab_footer::NewTabFooterWebView {
- public:
-  explicit FakeNewTabFooterWebView(BrowserWindowInterface* browser_window)
-      : NewTabFooterWebView(browser_window) {}
-  ~FakeNewTabFooterWebView() override {}
-
-  bool did_show_ui() { return did_show_ui_; }
-  bool did_close_ui() { return did_close_ui_; }
-
-  void Clear() {
-    did_show_ui_ = false;
-    did_close_ui_ = false;
-  }
-
- protected:
-  // WebUIContentsWrapper::Host:
-  void ShowUI() override { did_show_ui_ = true; }
-  void CloseUI() override { did_close_ui_ = true; }
-
-  bool did_show_ui_ = false;
-  bool did_close_ui_ = false;
-};
-
-class FakeBrowserWindowInterface : public MockBrowserWindowInterface {
- public:
-  explicit FakeBrowserWindowInterface(Profile* profile) { profile_ = profile; }
-  ~FakeBrowserWindowInterface() override = default;
-
-  // MockBrowserWindowInterface:
-  FakeNewTabFooterWebView* NewTabFooterWebView() override {
-    return footer_web_view_;
-  }
-  Profile* GetProfile() override { return profile_.get(); }
-
-  void SetFooterWebView(FakeNewTabFooterWebView* footer_web_view) {
-    footer_web_view_ = footer_web_view;
-  }
-
- protected:
-  raw_ptr<Profile> profile_;
-  raw_ptr<FakeNewTabFooterWebView> footer_web_view_;
-};
-
-class FakeTabInterface : public tabs::MockTabInterface {
- public:
-  FakeTabInterface(FakeBrowserWindowInterface* browser_window,
-                   content::BrowserContext* browser_context) {
-    browser_window_ = browser_window;
-    web_contents_ = content::WebContentsTester::CreateTestWebContents(
-        browser_context, nullptr);
-    footer_web_view_ =
-        std::make_unique<FakeNewTabFooterWebView>(browser_window);
-    browser_window_->SetFooterWebView(footer_web_view_.get());
-  }
-  ~FakeTabInterface() override {
-    browser_window_->SetFooterWebView(nullptr);
-    footer_web_view_.reset();
-  }
-
-  // tabs::MockTabInterface:
-  FakeBrowserWindowInterface* GetBrowserWindowInterface() override {
-    return browser_window_;
-  }
-  content::WebContents* GetContents() const override {
-    return web_contents_.get();
-  }
-  bool IsActivated() const override { return true; }
-
-  void NavigateTo(const GURL url) {
-    EXPECT_TRUE(web_contents_.get());
-    content::WebContentsTester* web_contents_tester =
-        content::WebContentsTester::For(web_contents_.get());
-    web_contents_tester->NavigateAndCommit(url);
-  }
-
- protected:
-  std::unique_ptr<content::WebContents> web_contents_;
-  std::unique_ptr<FakeNewTabFooterWebView> footer_web_view_;
-  raw_ptr<FakeBrowserWindowInterface> browser_window_;
-};
-
-class FooterControllerExtensionTest
-    : public extensions::ExtensionServiceTestBase {
- public:
-  void SetUp() override {
-    feature_list_.InitWithFeatures(
-        /*enabled_features=*/{ntp_features::kNtpFooter},
-        /*disabled_features=*/{features::kSideBySide});
-    ExtensionServiceTestBase::SetUp();
-    InitializeEmptyExtensionService();
-
-    browser_window_ = std::make_unique<FakeBrowserWindowInterface>(profile());
-    tab_ = std::make_unique<FakeTabInterface>(browser_window_.get(),
-                                              browser_context());
-    controller_ =
-        std::make_unique<new_tab_footer::NewTabFooterController>(tab_.get());
-  }
-
-  void TearDown() override {
-    controller_.reset();
-    tab_.reset();
-    browser_window_.reset();
-    ExtensionServiceTestBase::TearDown();
-  }
-
-  scoped_refptr<const extensions::Extension> LoadNtpExtension() {
-    extensions::TestExtensionDir extension_dir;
-    const std::string kManifest = R"(
-      {
-        "chrome_url_overrides": {
-            "newtab": "ext.html"
-        },
-        "name": "Extension-overridden NTP",
-          "manifest_version": 3,
-          "version": "0.1"
-      })";
-    extension_dir.WriteManifest(kManifest);
-    extension_dir.WriteFile(FILE_PATH_LITERAL("ext.html"),
-                            "<body>Extension-overridden NTP</body>");
-    extensions::ChromeTestExtensionLoader extension_loader(profile());
-    scoped_refptr<const extensions::Extension> extension =
-        extension_loader.LoadExtension(extension_dir.Pack());
-    return extension;
-  }
-
-  FakeBrowserWindowInterface* browser_window() { return browser_window_.get(); }
-
-  FakeTabInterface* tab_interface() { return tab_.get(); }
-
- protected:
-  base::test::ScopedFeatureList feature_list_;
-  std::unique_ptr<FakeBrowserWindowInterface> browser_window_;
-  std::unique_ptr<FakeTabInterface> tab_;
-  std::unique_ptr<new_tab_footer::NewTabFooterController> controller_;
-};
-
-TEST_F(FooterControllerExtensionTest, FooterShown_ExtensionNTP) {
-  profile()->GetPrefs()->SetBoolean(prefs::kNtpFooterVisible, true);
-  auto extension = LoadNtpExtension();
-  ASSERT_TRUE(extension);
-  // Force activation of the URL override. The usual observer for
-  // extension load isn't created in the unit test.
-  ExtensionWebUI::RegisterOrActivateChromeURLOverrides(
-      profile_.get(),
-      extensions::URLOverrides::GetChromeURLOverrides(extension.get()));
-  EXPECT_FALSE(browser_window()->NewTabFooterWebView()->did_show_ui());
-  tab_interface()->NavigateTo(extension->url());
-
-  EXPECT_TRUE(browser_window()->NewTabFooterWebView()->did_show_ui());
-}
-
-TEST_F(FooterControllerExtensionTest, FooterHidden_NonExtensionNTP) {
-  profile()->GetPrefs()->SetBoolean(prefs::kNtpFooterVisible, true);
-  // After a pref change, there's an attempt to show the footer.
-  EXPECT_TRUE(browser_window()->NewTabFooterWebView()->did_close_ui());
-  // Clear the value of `FakeNewTabFooterWebView::did_close_ui()` to
-  // check that the navigation also results in a hidden footer.
-  browser_window()->NewTabFooterWebView()->Clear();
-
-  EXPECT_FALSE(browser_window()->NewTabFooterWebView()->did_close_ui());
-  tab_interface()->NavigateTo(GURL(kNonExtensionNtpUrl));
-
-  EXPECT_TRUE(browser_window()->NewTabFooterWebView()->did_close_ui());
-}
-
-// Ensures footer is shown on extension NTPs when
-// `prefs::kNtpFooterVisible` is set to true.
-TEST_F(FooterControllerExtensionTest, FooterShown_UserPref) {
-  profile()->GetPrefs()->SetBoolean(prefs::kNtpFooterVisible, false);
-  auto extension = LoadNtpExtension();
-  ASSERT_TRUE(extension);
-  // Force activation of the URL override. The usual observer for
-  // extension load isn't created in the unit test.
-  ExtensionWebUI::RegisterOrActivateChromeURLOverrides(
-      profile_.get(),
-      extensions::URLOverrides::GetChromeURLOverrides(extension.get()));
-
-  tab_interface()->NavigateTo(extension->url());
-  EXPECT_FALSE(browser_window()->NewTabFooterWebView()->did_show_ui());
-
-  profile()->GetPrefs()->SetBoolean(prefs::kNtpFooterVisible, true);
-  EXPECT_TRUE(browser_window()->NewTabFooterWebView()->did_show_ui());
-}
-
-// Ensures footer is hidden on extension NTPs when
-// `prefs::kNtpFooterVisible` is set to false.
-TEST_F(FooterControllerExtensionTest, FooterHidden_UserPref) {
-  profile()->GetPrefs()->SetBoolean(prefs::kNtpFooterVisible, true);
-  // After a pref change, there's an attempt to show the footer.
-  EXPECT_TRUE(browser_window()->NewTabFooterWebView()->did_close_ui());
-  // Clear the value of `FakeNewTabFooterWebView::did_close_ui()` to check
-  // the effect of pref changes on an extension NTP.
-  browser_window()->NewTabFooterWebView()->Clear();
-  EXPECT_FALSE(browser_window()->NewTabFooterWebView()->did_close_ui());
-
-  auto extension = LoadNtpExtension();
-  ASSERT_TRUE(extension);
-  // Force activation of the URL override. The usual observer for
-  // extension load isn't created in the unit test.
-  ExtensionWebUI::RegisterOrActivateChromeURLOverrides(
-      profile_.get(),
-      extensions::URLOverrides::GetChromeURLOverrides(extension.get()));
-
-  tab_interface()->NavigateTo(extension->url());
-  EXPECT_FALSE(browser_window()->NewTabFooterWebView()->did_close_ui());
-
-  profile()->GetPrefs()->SetBoolean(prefs::kNtpFooterVisible, false);
-  EXPECT_TRUE(browser_window()->NewTabFooterWebView()->did_close_ui());
-}
diff --git a/chrome/browser/ui/views/new_tab_footer/footer_interactive_uitest.cc b/chrome/browser/ui/views/new_tab_footer/footer_interactive_uitest.cc
index b329e865..85ffbca 100644
--- a/chrome/browser/ui/views/new_tab_footer/footer_interactive_uitest.cc
+++ b/chrome/browser/ui/views/new_tab_footer/footer_interactive_uitest.cc
@@ -3,30 +3,33 @@
 // found in the LICENSE file.
 
 #include "base/test/scoped_feature_list.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/enterprise/browser_management/management_service_factory.h"
 #include "chrome/browser/extensions/chrome_test_extension_loader.h"
 #include "chrome/browser/extensions/install_verifier.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser_commands.h"
+#include "chrome/browser/ui/browser_element_identifiers.h"
 #include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/ui/views/new_tab_footer/footer_web_view.h"
 #include "chrome/browser/ui/webui/test_support/webui_interactive_test_mixin.h"
+#include "chrome/common/pref_names.h"
 #include "chrome/test/base/ui_test_utils.h"
 #include "chrome/test/interaction/interactive_browser_test.h"
+#include "components/policy/core/common/management/scoped_management_service_override_for_testing.h"
 #include "components/search/ntp_features.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/test_navigation_observer.h"
 #include "extensions/test/test_extension_dir.h"
 
-namespace {
-constexpr char kFooterViewName[] = "footer_view";
-}  // namespace
-
 class FooterInteractiveTest
     : public WebUiInteractiveTestMixin<InteractiveBrowserTest> {
  public:
   FooterInteractiveTest() {
     scoped_feature_list_.InitWithFeatures(
-        /*enabled_features=*/{ntp_features::kNtpFooter},
+        /*enabled_features=*/{ntp_features::kNtpFooter,
+                              features::kEnterpriseBadgingForNtpFooter},
         /*disabled_features=*/{features::kSideBySide});
   }
 
@@ -71,6 +74,17 @@
     nav_observer.Wait();
   }
 
+  InteractiveTestApi::MultiStep OpenCustomizeChromeSidePanel(
+      const ui::ElementIdentifier& contents_id) {
+    return Steps(Do(base::BindLambdaForTesting([=, this]() {
+                   chrome::ExecuteCommand(browser(),
+                                          IDC_SHOW_CUSTOMIZE_CHROME_SIDE_PANEL);
+                 })),
+                 WaitForShow(kCustomizeChromeSidePanelWebViewElementId),
+                 InstrumentNonTabWebView(
+                     contents_id, kCustomizeChromeSidePanelWebViewElementId));
+  }
+
   new_tab_footer::NewTabFooterWebView* GetFooterView() {
     return browser()->GetBrowserView().new_tab_footer_web_view();
   }
@@ -80,33 +94,142 @@
   extensions::ScopedInstallVerifierBypassForTest install_verifier_bypass_;
 };
 
-IN_PROC_BROWSER_TEST_F(FooterInteractiveTest, FooterVisibleOnExtensionNtp) {
+IN_PROC_BROWSER_TEST_F(FooterInteractiveTest,
+                       ConsumerExtensionNtp_FooterVisible) {
   LoadNtpOverridingExtension(browser()->profile());
   RunTestSequence(
       // Open extension NTP.
       Do(base::BindLambdaForTesting([&, this]() { OpenNewTabPage(); })),
-      Steps(NameView(kFooterViewName, GetFooterView()),
-            // Ensure footer is visible.
-            CheckView(kFooterViewName,
-                      [](new_tab_footer::NewTabFooterWebView* footer) {
-                        return footer->GetVisible();
-                      })));
+      // Ensure footer is visible.
+      Steps(WaitForShow(kNtpFooterId)));
 }
 
 IN_PROC_BROWSER_TEST_F(FooterInteractiveTest,
-                       FooterNotVisibleOnNonExtensionNtp) {
+                       ConsumerNonExtensionNtp_FooterNotVisible) {
   LoadNtpOverridingExtension(browser()->profile());
   RunTestSequence(
       // Open extension NTP.
       Do(base::BindLambdaForTesting([&, this]() { OpenNewTabPage(); })),
-      Steps(NameView(kFooterViewName, GetFooterView()),
-            // Ensure footer is visible.
-            CheckView(kFooterViewName,
-                      [](new_tab_footer::NewTabFooterWebView* footer) {
-                        return footer->GetVisible();
-                      })),
+      // Ensure footer is visible.
+      Steps(WaitForShow(kNtpFooterId)),
       // Navigate to non-extension NTP and check that the footer isn't visible.
       Do(base::BindLambdaForTesting(
           [&, this]() { NavigateTo(GURL("https://google.com")); })),
-      WaitForHide(kFooterViewName));
+      WaitForHide(kNtpFooterId));
 }
+
+IN_PROC_BROWSER_TEST_F(FooterInteractiveTest,
+                       CustomizeChrome_ToggleHidesFooter) {
+  browser()->GetProfile()->GetPrefs()->SetBoolean(prefs::kNtpFooterVisible,
+                                                  true);
+
+  DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kLocalCustomizeChromeElementId);
+  const DeepQuery kFooterSection = {"customize-chrome-app", "#footer",
+                                    "customize-chrome-footer",
+                                    "#showToggleContainer", "#showToggle"};
+
+  LoadNtpOverridingExtension(browser()->profile());
+  RunTestSequence(
+      // Open extension NTP.
+      Do(base::BindLambdaForTesting([&, this]() { OpenNewTabPage(); })),
+      // Ensure footer is visible.
+      WaitForShow(kNtpFooterId),
+      OpenCustomizeChromeSidePanel(kLocalCustomizeChromeElementId),
+      Steps(
+          // Click the footer section toggle.
+          ScrollIntoView(kLocalCustomizeChromeElementId, kFooterSection),
+          EnsurePresent(kLocalCustomizeChromeElementId, kFooterSection),
+          ExecuteJsAt(kLocalCustomizeChromeElementId, kFooterSection,
+                      "(toggle) => toggle.click()"),
+          // Ensure the footer is no longer visible.
+          WaitForHide(kNtpFooterId)));
+}
+
+IN_PROC_BROWSER_TEST_F(FooterInteractiveTest,
+                       CustomizeChrome_ToggleShowsFooter) {
+  browser()->GetProfile()->GetPrefs()->SetBoolean(prefs::kNtpFooterVisible,
+                                                  false);
+
+  DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kLocalCustomizeChromeElementId);
+  const DeepQuery kFooterSection = {"customize-chrome-app", "#footer",
+                                    "customize-chrome-footer",
+                                    "#showToggleContainer", "#showToggle"};
+  LoadNtpOverridingExtension(browser()->profile()),
+      RunTestSequence(
+          Do(base::BindLambdaForTesting([&, this]() { OpenNewTabPage(); })),
+          // Ensure footer is not visible.
+          EnsureNotPresent(kNtpFooterId),
+          OpenCustomizeChromeSidePanel(kLocalCustomizeChromeElementId),
+          Steps(
+              // Click the footer section toggle.
+              ScrollIntoView(kLocalCustomizeChromeElementId, kFooterSection),
+              EnsurePresent(kLocalCustomizeChromeElementId, kFooterSection),
+              ExecuteJsAt(kLocalCustomizeChromeElementId, kFooterSection,
+                          "(toggle) => toggle.click()"),
+              // Ensure footer is visible.
+              WaitForShow(kNtpFooterId)));
+}
+
+#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
+IN_PROC_BROWSER_TEST_F(FooterInteractiveTest,
+                       EnterpriseNonExtensionNtp_FooterVisible) {
+  policy::ScopedManagementServiceOverrideForTesting browser_management(
+      policy::ManagementServiceFactory::GetForProfile(browser()->profile()),
+      policy::EnterpriseManagementAuthority::DOMAIN_LOCAL);
+  RunTestSequence(
+      // Open NTP.
+      Do(base::BindLambdaForTesting([&, this]() { OpenNewTabPage(); })),
+      // Ensure footer is visible.
+      Steps(WaitForShow(kNtpFooterId)));
+}
+
+IN_PROC_BROWSER_TEST_F(FooterInteractiveTest,
+                       EnterpriseExtensionNtp_FooterVisible) {
+  policy::ScopedManagementServiceOverrideForTesting browser_management(
+      policy::ManagementServiceFactory::GetForProfile(browser()->profile()),
+      policy::EnterpriseManagementAuthority::DOMAIN_LOCAL);
+  LoadNtpOverridingExtension(browser()->profile());
+  RunTestSequence(
+      // Open extension NTP.
+      Do(base::BindLambdaForTesting([&, this]() { OpenNewTabPage(); })),
+      // Ensure footer is visible.
+      Steps(WaitForShow(kNtpFooterId)));
+}
+
+IN_PROC_BROWSER_TEST_F(FooterInteractiveTest,
+                       EnterpriseNonNtp_FooterNotVisible) {
+  policy::ScopedManagementServiceOverrideForTesting browser_management(
+      policy::ManagementServiceFactory::GetForProfile(browser()->profile()),
+      policy::EnterpriseManagementAuthority::DOMAIN_LOCAL);
+  RunTestSequence(
+      // Open NTP.
+      Do(base::BindLambdaForTesting([&, this]() { OpenNewTabPage(); })),
+      Steps(
+          // Ensure footer is visible.
+          Steps(WaitForShow(kNtpFooterId))),
+      // Navigate to non-extension NTP and check that the footer isn't visible.
+      Do(base::BindLambdaForTesting(
+          [&, this]() { NavigateTo(GURL("https://google.com")); })),
+      WaitForHide(kNtpFooterId));
+}
+
+IN_PROC_BROWSER_TEST_F(FooterInteractiveTest,
+                       ManagementNoticeDisabledByPolicy_FooterNotVisible) {
+  g_browser_process->local_state()->SetBoolean(
+      prefs::kNTPFooterManagementNoticeEnabled, false);
+  policy::ScopedManagementServiceOverrideForTesting browser_management(
+      policy::ManagementServiceFactory::GetForProfile(browser()->profile()),
+      policy::EnterpriseManagementAuthority::DOMAIN_LOCAL);
+  OpenNewTabPage();
+  EXPECT_FALSE(GetFooterView()->GetVisible());
+}
+
+IN_PROC_BROWSER_TEST_F(FooterInteractiveTest,
+                       ExtensionAttributionDisabledByPolicy_FooterNotVisible) {
+  browser()->profile()->GetPrefs()->SetBoolean(
+      prefs::kNTPFooterExtensionAttributionEnabled, false);
+  LoadNtpOverridingExtension(browser()->profile());
+  OpenNewTabPage();
+  EXPECT_FALSE(GetFooterView()->GetVisible());
+}
+#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
diff --git a/chrome/browser/ui/views/new_tab_footer/footer_web_view.cc b/chrome/browser/ui/views/new_tab_footer/footer_web_view.cc
index 27ac67e..1da7454 100644
--- a/chrome/browser/ui/views/new_tab_footer/footer_web_view.cc
+++ b/chrome/browser/ui/views/new_tab_footer/footer_web_view.cc
@@ -12,6 +12,9 @@
 #include "chrome/common/webui_url_constants.h"
 #include "chrome/grit/generated_resources.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
+#include "ui/views/view_class_properties.h"
+
+DEFINE_ELEMENT_IDENTIFIER_VALUE(kNtpFooterId);
 
 namespace new_tab_footer {
 
@@ -25,6 +28,7 @@
   SetWebContents(contents_wrapper_->web_contents());
   webui::SetBrowserWindowInterface(contents_wrapper_->web_contents(),
                                    browser_window);
+  SetProperty(views::kElementIdentifierKey, kNtpFooterId);
 }
 
 NewTabFooterWebView::~NewTabFooterWebView() {
diff --git a/chrome/browser/ui/views/new_tab_footer/footer_web_view.h b/chrome/browser/ui/views/new_tab_footer/footer_web_view.h
index 5b012ea..3ee8f3a 100644
--- a/chrome/browser/ui/views/new_tab_footer/footer_web_view.h
+++ b/chrome/browser/ui/views/new_tab_footer/footer_web_view.h
@@ -13,6 +13,8 @@
 class BrowserWindowInterface;
 class WebUIContentsWrapper;
 
+DECLARE_ELEMENT_IDENTIFIER_VALUE(kNtpFooterId);
+
 namespace new_tab_footer {
 
 // NewTabFooterWebView is used to present the WebContents of the New Tab Footer.
diff --git a/chrome/browser/ui/views/page_action/page_action_icon_controller.cc b/chrome/browser/ui/views/page_action/page_action_icon_controller.cc
index e31f3ff..5732e0f 100644
--- a/chrome/browser/ui/views/page_action/page_action_icon_controller.cc
+++ b/chrome/browser/ui/views/page_action/page_action_icon_controller.cc
@@ -34,6 +34,7 @@
 #include "chrome/browser/ui/views/location_bar/cookie_controls/cookie_controls_icon_view.h"
 #include "chrome/browser/ui/views/location_bar/find_bar_icon.h"
 #include "chrome/browser/ui/views/location_bar/intent_picker_view.h"
+#include "chrome/browser/ui/views/location_bar/lens_overlay_homework_page_action_icon_view.h"
 #include "chrome/browser/ui/views/location_bar/lens_overlay_page_action_icon_view.h"
 #include "chrome/browser/ui/views/location_bar/star_view.h"
 #include "chrome/browser/ui/views/location_bar/zoom_bubble_view.h"
@@ -283,6 +284,12 @@
                       params.browser, params.icon_label_bubble_delegate,
                       params.page_action_icon_delegate));
         break;
+      case PageActionIconType::kLensOverlayHomework:
+        add_page_action_icon(
+            type, std::make_unique<LensOverlayHomeworkPageActionIconView>(
+                      params.icon_label_bubble_delegate,
+                      params.page_action_icon_delegate, params.browser));
+        break;
       case PageActionIconType::kOptimizationGuide:
         add_page_action_icon(
             type, std::make_unique<OptimizationGuideIconView>(
@@ -348,22 +355,6 @@
   });
 }
 
-bool PageActionIconController::ActivateFirstInactiveBubbleForAccessibility() {
-  for (auto icon_item : page_action_icon_views_) {
-    auto* icon = icon_item.second.get();
-    if (!icon->GetVisible() || !icon->GetBubble()) {
-      continue;
-    }
-
-    views::Widget* widget = icon->GetBubble()->GetWidget();
-    if (widget && widget->IsVisible() && !widget->IsActive()) {
-      widget->Show();
-      return true;
-    }
-  }
-  return false;
-}
-
 void PageActionIconController::SetIconColor(SkColor icon_color) {
   for (auto icon_item : page_action_icon_views_) {
     icon_item.second->SetIconColor(icon_color);
diff --git a/chrome/browser/ui/views/page_action/page_action_icon_controller.h b/chrome/browser/ui/views/page_action/page_action_icon_controller.h
index 2052a681..549fba3 100644
--- a/chrome/browser/ui/views/page_action/page_action_icon_controller.h
+++ b/chrome/browser/ui/views/page_action/page_action_icon_controller.h
@@ -46,10 +46,6 @@
 
   bool IsAnyIconVisible() const;
 
-  // Activates the first visible but inactive icon for accessibility. Returns
-  // whether any icons were activated.
-  bool ActivateFirstInactiveBubbleForAccessibility();
-
   // Update the icons' color.
   void SetIconColor(SkColor icon_color);
 
diff --git a/chrome/browser/ui/views/performance_controls/tab_list_row_view.cc b/chrome/browser/ui/views/performance_controls/tab_list_row_view.cc
index f4f1f67..c60715c 100644
--- a/chrome/browser/ui/views/performance_controls/tab_list_row_view.cc
+++ b/chrome/browser/ui/views/performance_controls/tab_list_row_view.cc
@@ -14,10 +14,12 @@
 #include "base/time/time.h"
 #include "chrome/browser/ui/performance_controls/tab_list_model.h"
 #include "chrome/browser/ui/tab_ui_helper.h"
+#include "chrome/browser/ui/tabs/public/tab_features.h"
 #include "chrome/browser/ui/views/chrome_layout_provider.h"
 #include "chrome/grit/generated_resources.h"
 #include "components/performance_manager/public/resource_attribution/page_context.h"
 #include "components/strings/grit/components_strings.h"
+#include "components/tabs/public/tab_interface.h"
 #include "components/url_formatter/url_formatter.h"
 #include "components/vector_icons/vector_icons.h"
 #include "content/public/browser/web_contents.h"
@@ -155,7 +157,10 @@
   content::WebContents* const web_contents = tab.GetWebContents();
   CHECK(web_contents);
 
-  TabUIHelper* const tab_ui_helper = TabUIHelper::FromWebContents(web_contents);
+  tabs::TabInterface* const tab_interface =
+      tabs::TabInterface::GetFromContents(web_contents);
+  TabUIHelper* const tab_ui_helper =
+      tab_interface->GetTabFeatures()->tab_ui_helper();
   CHECK(tab_ui_helper);
 
   // The container adds all contents of the row as child views to ensure that we
diff --git a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
index 81b9314..60ad9eb 100644
--- a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
+++ b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
@@ -32,6 +32,7 @@
 #include "chrome/browser/ui/browser_tabstrip.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_features.h"
 #include "chrome/browser/ui/tab_ui_helper.h"
+#include "chrome/browser/ui/tabs/public/tab_features.h"
 #include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.h"
 #include "chrome/browser/ui/tabs/split_tab_util.h"
 #include "chrome/browser/ui/tabs/tab_enums.h"
@@ -706,8 +707,7 @@
       IDS_TAB_CXMENU_PLACEHOLDER_GROUP_TITLE, tab_group->tab_count() - 1);
   std::u16string short_title;
   gfx::ElideString(
-      TabUIHelper::FromWebContents(tab_group->GetFirstTab()->GetContents())
-          ->GetTitle(),
+      tab_group->GetFirstTab()->GetTabFeatures()->tab_ui_helper()->GetTitle(),
       kContextMenuTabTitleMaxLength, &short_title);
   return base::ReplaceStringPlaceholders(format_string, short_title, nullptr);
 }
@@ -856,10 +856,13 @@
   if (selection.active_tab_changed()) {
     // It's possible for `new_contents` to be null when the final tab in a tab
     // strip is closed.
-    content::WebContents* new_contents = selection.new_contents;
+    content::WebContents* const new_contents = selection.new_contents;
+    tabs::TabInterface* const new_tab_interface = selection.new_tab;
     std::optional<size_t> index = selection.new_model.active();
-    if (new_contents && index.has_value()) {
-      TabUIHelper::FromWebContents(new_contents)->SetWasActiveAtLeastOnce();
+    if (new_contents && new_tab_interface && index.has_value()) {
+      new_tab_interface->GetTabFeatures()
+          ->tab_ui_helper()
+          ->SetWasActiveAtLeastOnce();
       SetTabDataAt(new_contents, index.value());
     }
   }
diff --git a/chrome/browser/ui/views/tabs/compound_tab_container.cc b/chrome/browser/ui/views/tabs/compound_tab_container.cc
index 3068beb..e655cb3c 100644
--- a/chrome/browser/ui/views/tabs/compound_tab_container.cc
+++ b/chrome/browser/ui/views/tabs/compound_tab_container.cc
@@ -671,11 +671,6 @@
   return unpinned_tab_container_->get_group_views_for_testing();  // IN-TEST
 }
 
-int CompoundTabContainer::GetActiveTabWidth() const {
-  // Only the unpinned container has variable-width tabs.
-  return unpinned_tab_container_->GetActiveTabWidth();
-}
-
 gfx::Rect CompoundTabContainer::GetIdealBounds(int model_index) const {
   // Ideal bounds for pinned tabs are fine as-is.
   if (model_index < NumPinnedTabs()) {
diff --git a/chrome/browser/ui/views/tabs/compound_tab_container.h b/chrome/browser/ui/views/tabs/compound_tab_container.h
index 9bb50d65..924df23 100644
--- a/chrome/browser/ui/views/tabs/compound_tab_container.h
+++ b/chrome/browser/ui/views/tabs/compound_tab_container.h
@@ -96,7 +96,6 @@
   TabGroupViews* GetGroupViews(tab_groups::TabGroupId group_id) const override;
   const std::map<tab_groups::TabGroupId, std::unique_ptr<TabGroupViews>>&
   get_group_views_for_testing() const override;
-  int GetActiveTabWidth() const override;
   gfx::Rect GetIdealBounds(int model_index) const override;
   gfx::Rect GetIdealBounds(tab_groups::TabGroupId group) const override;
 
diff --git a/chrome/browser/ui/views/tabs/dragging/dragging_tabs_session.cc b/chrome/browser/ui/views/tabs/dragging/dragging_tabs_session.cc
index 45e4c4db..2c9860c 100644
--- a/chrome/browser/ui/views/tabs/dragging/dragging_tabs_session.cc
+++ b/chrome/browser/ui/views/tabs/dragging/dragging_tabs_session.cc
@@ -236,7 +236,7 @@
   }
 
   return gfx::Rect(tab_strip_point.x(), tab_strip_point.y(),
-                   attached_context_->GetActiveTabWidth(),
+                   TabStyle::Get()->GetStandardWidth(/*is_split=*/false),
                    GetLayoutConstant(TAB_HEIGHT));
 }
 
diff --git a/chrome/browser/ui/views/tabs/dragging/tab_drag_context.h b/chrome/browser/ui/views/tabs/dragging/tab_drag_context.h
index 2c8a6c5a..c4e8352 100644
--- a/chrome/browser/ui/views/tabs/dragging/tab_drag_context.h
+++ b/chrome/browser/ui/views/tabs/dragging/tab_drag_context.h
@@ -96,9 +96,6 @@
   // Returns true if a tab is being dragged into this tab strip.
   virtual bool IsActiveDropTarget() const = 0;
 
-  // Returns the width of the active tab.
-  virtual int GetActiveTabWidth() const = 0;
-
   // Returns where the drag region begins and ends; tabs dragged beyond these
   // points should detach.
   virtual int TabDragAreaEndX() const = 0;
diff --git a/chrome/browser/ui/views/tabs/tab_container.h b/chrome/browser/ui/views/tabs/tab_container.h
index ed2983d..b621711 100644
--- a/chrome/browser/ui/views/tabs/tab_container.h
+++ b/chrome/browser/ui/views/tabs/tab_container.h
@@ -184,9 +184,6 @@
                          std::unique_ptr<TabGroupViews>>&
   get_group_views_for_testing() const = 0;
 
-  // Returns the current width of the active tab.
-  virtual int GetActiveTabWidth() const = 0;
-
   // Returns ideal bounds for the tab at `model_index` in this TabContainer's
   // coordinate space.
   virtual gfx::Rect GetIdealBounds(int model_index) const = 0;
diff --git a/chrome/browser/ui/views/tabs/tab_container_impl.cc b/chrome/browser/ui/views/tabs/tab_container_impl.cc
index 75379edb..ee8890d 100644
--- a/chrome/browser/ui/views/tabs/tab_container_impl.cc
+++ b/chrome/browser/ui/views/tabs/tab_container_impl.cc
@@ -822,10 +822,6 @@
   return group_views_;
 }
 
-int TabContainerImpl::GetActiveTabWidth() const {
-  return layout_helper_->active_tab_width();
-}
-
 gfx::Rect TabContainerImpl::GetIdealBounds(int model_index) const {
   return tabs_view_model_.ideal_bounds(model_index);
 }
diff --git a/chrome/browser/ui/views/tabs/tab_container_impl.h b/chrome/browser/ui/views/tabs/tab_container_impl.h
index b1af740..2513dc0c 100644
--- a/chrome/browser/ui/views/tabs/tab_container_impl.h
+++ b/chrome/browser/ui/views/tabs/tab_container_impl.h
@@ -130,8 +130,6 @@
   const std::map<tab_groups::TabGroupId, std::unique_ptr<TabGroupViews>>&
   get_group_views_for_testing() const override;
 
-  int GetActiveTabWidth() const override;
-
   gfx::Rect GetIdealBounds(int model_index) const override;
   gfx::Rect GetIdealBounds(tab_groups::TabGroupId group) const override;
 
diff --git a/chrome/browser/ui/views/tabs/tab_strip.cc b/chrome/browser/ui/views/tabs/tab_strip.cc
index e1ddab79..d4a7ae4 100644
--- a/chrome/browser/ui/views/tabs/tab_strip.cc
+++ b/chrome/browser/ui/views/tabs/tab_strip.cc
@@ -478,10 +478,6 @@
     return false;
   }
 
-  int GetActiveTabWidth() const override {
-    return tab_strip_->GetActiveTabWidth();
-  }
-
   int GetTabDragAreaWidth() const override {
     // There are two cases here (with tab scrolling enabled):
     // 1) If the tab strip is not wider than the tab strip region (and thus
@@ -2239,10 +2235,6 @@
 #endif  // BUILDFLAG(IS_WIN)
 }
 
-int TabStrip::GetActiveTabWidth() const {
-  return tab_container_->GetActiveTabWidth();
-}
-
 const Tab* TabStrip::GetLastVisibleTab() const {
   for (int i = GetTabCount() - 1; i >= 0; --i) {
     const Tab* tab = tab_at(i);
@@ -2556,5 +2548,4 @@
                                TabSeparatorColor,
                                ui::metadata::SkColorConverter)
 ADD_READONLY_PROPERTY_METADATA(float, HoverOpacityForRadialHighlight)
-ADD_READONLY_PROPERTY_METADATA(int, ActiveTabWidth)
 END_METADATA
diff --git a/chrome/browser/ui/views/tabs/tab_strip.h b/chrome/browser/ui/views/tabs/tab_strip.h
index e6d84f5..3f389e4 100644
--- a/chrome/browser/ui/views/tabs/tab_strip.h
+++ b/chrome/browser/ui/views/tabs/tab_strip.h
@@ -402,9 +402,6 @@
   // Returns whether the window background behind the tabstrip is transparent.
   bool TitlebarBackgroundIsTransparent() const;
 
-  // Returns the current width of the active tab.
-  int GetActiveTabWidth() const;
-
   // Returns the last tab in the strip that's actually visible.  This will be
   // the actual last tab unless the strip is in the overflow node_data.
   const Tab* GetLastVisibleTab() const;
diff --git a/chrome/browser/ui/views/tabs/tab_strip_layout_helper.cc b/chrome/browser/ui/views/tabs/tab_strip_layout_helper.cc
index 6ed59f6..61c8b89b 100644
--- a/chrome/browser/ui/views/tabs/tab_strip_layout_helper.cc
+++ b/chrome/browser/ui/views/tabs/tab_strip_layout_helper.cc
@@ -47,7 +47,6 @@
     GetTabsCallback get_tabs_callback)
     : controller_(controller),
       get_tabs_callback_(get_tabs_callback),
-      active_tab_width_(TabStyle::Get()->GetStandardWidth(/*is_split=*/false)),
       tab_strip_layout_domain_(LayoutDomain::kInactiveWidthEqualsActiveWidth) {}
 
 TabStripLayoutHelper::~TabStripLayoutHelper() = default;
@@ -229,14 +228,6 @@
       case TabSlotView::ViewType::kTab:
         if (!slot.state.IsClosed()) {
           tabs->set_ideal_bounds(current_tab_model_index, bounds[i]);
-          bool is_active = i == active_tab_slot_index;
-
-          if (active_split_id.has_value() &&
-              slot.view->split() == active_split_id) {
-            is_active = true;
-          }
-
-          UpdateCachedTabWidth(i, bounds[i].width(), is_active);
           ++current_tab_model_index;
         }
         break;
@@ -424,19 +415,6 @@
   return std::nullopt;
 }
 
-void TabStripLayoutHelper::UpdateCachedTabWidth(int tab_index,
-                                                int tab_width,
-                                                bool active) {
-  // If the slot is collapsed, its width should never be reported as the
-  // current active or inactive tab width - it's not even visible.
-  if (SlotIsCollapsedTab(tab_index)) {
-    return;
-  }
-  if (active) {
-    active_tab_width_ = tab_width;
-  }
-}
-
 bool TabStripLayoutHelper::SlotIsCollapsedTab(int i) const {
   // The slot can only be collapsed if it is a tab and in a collapsed group.
   // If the slot is indeed a tab and in a group, check the collapsed state of
diff --git a/chrome/browser/ui/views/tabs/tab_strip_layout_helper.h b/chrome/browser/ui/views/tabs/tab_strip_layout_helper.h
index 35a9b06..462eccb 100644
--- a/chrome/browser/ui/views/tabs/tab_strip_layout_helper.h
+++ b/chrome/browser/ui/views/tabs/tab_strip_layout_helper.h
@@ -49,7 +49,6 @@
   // GetTabs() and all tab group headers.
   std::vector<TabSlotView*> GetTabSlotViews() const;
 
-  int active_tab_width() { return active_tab_width_; }
   LayoutDomain layout_domain() { return tab_strip_layout_domain_; }
 
   // Returns the number of pinned tabs in the tabstrip.
@@ -102,8 +101,7 @@
   int CalculatePreferredWidth();
 
   // Generates and sets the ideal bounds for the views in `tabs` and
-  // `group_headers`. Updates the cached width in `active_tab_width_`. Returns
-  // the total width occupied by the new ideal bounds.
+  // `group_headers`. Returns the total width occupied by the new ideal bounds.
   int UpdateIdealBounds(int available_width);
 
  private:
@@ -147,9 +145,6 @@
   // tab. Otherwise returns `std::nullopt`.
   std::optional<int> GetAdjacentSplitTab(int index) const;
 
-  // Updates the value of either `active_tab_width_`.
-  void UpdateCachedTabWidth(int tab_index, int tab_width, bool active);
-
   // True iff the slot at index `i` is a tab that is in a collapsed group.
   bool SlotIsCollapsedTab(int i) const;
 
@@ -166,10 +161,6 @@
   // Contains the ideal bounds of tab group headers.
   std::map<tab_groups::TabGroupId, gfx::Rect> group_header_ideal_bounds_;
 
-  // The current widths of tabs. If the space for tabs is not evenly divisible
-  // into these widths, the initial tabs in the strip will be 1 px larger.
-  int active_tab_width_;
-
   LayoutDomain tab_strip_layout_domain_;
 };
 
diff --git a/chrome/browser/ui/views/tabs/tab_strip_unittest.cc b/chrome/browser/ui/views/tabs/tab_strip_unittest.cc
index 917db15..bdf7b24b 100644
--- a/chrome/browser/ui/views/tabs/tab_strip_unittest.cc
+++ b/chrome/browser/ui/views/tabs/tab_strip_unittest.cc
@@ -185,8 +185,6 @@
     views::test::RunScheduledLayout(tab_strip_parent_.get());
   }
 
-  int GetActiveTabWidth() { return tab_strip_->GetActiveTabWidth(); }
-
   // End any outstanding drag and animate tabs back to their ideal bounds.
   void StopDragging() { tab_strip_->GetDragContext()->StoppedDragging(); }
 
@@ -463,30 +461,6 @@
 }
 #endif
 
-// The cached widths are private, but if they give incorrect results it can
-// cause subtle errors in other tests. Therefore it's prudent to test them.
-TEST_P(TabStripTest, CachedWidthsReportCorrectSize) {
-  controller_->AddTab(0, TabActive::kInactive);
-  controller_->AddTab(1, TabActive::kActive);
-  controller_->AddTab(2, TabActive::kInactive);
-
-  const int standard_width =
-      TabStyle::Get()->GetStandardWidth(/*is_split*/ false);
-
-  SetMaxTabStripWidth(1000);
-
-  EXPECT_EQ(standard_width, GetActiveTabWidth());
-
-  SetMaxTabStripWidth(240);
-
-  EXPECT_LT(GetActiveTabWidth(), standard_width);
-
-  SetMaxTabStripWidth(50);
-
-  EXPECT_EQ(TabStyle::Get()->GetMinimumActiveWidth(/*is_split*/ false),
-            GetActiveTabWidth());
-}
-
 // The active tab should always be at least as wide as its minimum width.
 // http://crbug.com/587688
 TEST_P(TabStripTest, ActiveTabWidthWhenTabsAreTiny) {
diff --git a/chrome/browser/ui/web_applications/sub_apps_service_impl_browsertest.cc b/chrome/browser/ui/web_applications/sub_apps_service_impl_browsertest.cc
index 15839ab..6c3c16e 100644
--- a/chrome/browser/ui/web_applications/sub_apps_service_impl_browsertest.cc
+++ b/chrome/browser/ui/web_applications/sub_apps_service_impl_browsertest.cc
@@ -9,6 +9,7 @@
 #include <utility>
 #include <vector>
 
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/test/gmock_expected_support.h"
 #include "base/test/scoped_feature_list.h"
diff --git a/chrome/browser/ui/web_applications/web_app_browsertest_base.cc b/chrome/browser/ui/web_applications/web_app_browsertest_base.cc
index 5edbe6f4..a6ec0c4 100644
--- a/chrome/browser/ui/web_applications/web_app_browsertest_base.cc
+++ b/chrome/browser/ui/web_applications/web_app_browsertest_base.cc
@@ -7,6 +7,7 @@
 #include <string>
 #include <vector>
 
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/bind.h"
 #include "base/time/time.h"
diff --git a/chrome/browser/ui/webui/ash/settings/pages/device/inputs_section.cc b/chrome/browser/ui/webui/ash/settings/pages/device/inputs_section.cc
index 4bc9aea..8d7d21c 100644
--- a/chrome/browser/ui/webui/ash/settings/pages/device/inputs_section.cc
+++ b/chrome/browser/ui/webui/ash/settings/pages/device/inputs_section.cc
@@ -261,8 +261,6 @@
        IDS_SETTINGS_INPUT_METHOD_OPTIONS_JAPANESE_SECTION_SHORTCUT_ASDFGHJKL},
       {"inputMethodOptionsJapaneseKeymapStyle",
        IDS_SETTINGS_INPUT_METHOD_OPTIONS_JAPANESE_KEYMAP_STYLE},
-      {"inputMethodOptionsJapaneseKeymapStyleCustom",
-       IDS_SETTINGS_INPUT_METHOD_OPTIONS_JAPANESE_KEYMAP_STYLE_CUSTOM},
       {"inputMethodOptionsJapaneseKeymapStyleAtok",
        IDS_SETTINGS_INPUT_METHOD_OPTIONS_JAPANESE_KEYMAP_STYLE_ATOK},
       {"inputMethodOptionsJapaneseKeymapStyleMsIme",
diff --git a/chrome/browser/ui/webui/new_tab_footer/BUILD.gn b/chrome/browser/ui/webui/new_tab_footer/BUILD.gn
index 0c51a8b0..2f5132fa 100644
--- a/chrome/browser/ui/webui/new_tab_footer/BUILD.gn
+++ b/chrome/browser/ui/webui/new_tab_footer/BUILD.gn
@@ -34,6 +34,7 @@
     "//chrome/browser/extensions",
     "//chrome/browser/profiles",
     "//chrome/browser/resources/new_tab_footer:resources_grit",
+    "//chrome/browser/search",
     "//chrome/browser/ui/browser_window",
     "//chrome/browser/ui/webui:webui_util",
     "//chrome/common",
diff --git a/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_helper.cc b/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_helper.cc
index e9b6320..ee29535 100644
--- a/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_helper.cc
+++ b/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_helper.cc
@@ -4,7 +4,11 @@
 
 #include "chrome/browser/ui/webui/new_tab_footer/new_tab_footer_helper.h"
 
+#include "chrome/browser/enterprise/util/managed_browser_utils.h"
 #include "chrome/browser/extensions/settings_api_helpers.h"
+#include "chrome/common/pref_names.h"
+#include "components/prefs/pref_service.h"
+#include "components/search/ntp_features.h"
 #include "extensions/common/constants.h"
 
 namespace ntp_footer {
@@ -24,4 +28,12 @@
   return extension_managing_ntp->id() == url.host();
 }
 
+bool CanShowExtensionFooter(const GURL& url, Profile* profile) {
+  if (!IsExtensionNtp(url, profile)) {
+    return false;
+  }
+
+  return profile->GetPrefs()->GetBoolean(
+      prefs::kNTPFooterExtensionAttributionEnabled);
+}
 }  // namespace ntp_footer
diff --git a/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_helper.h b/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_helper.h
index 2a16ae7..1aa328c 100644
--- a/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_helper.h
+++ b/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_helper.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_UI_WEBUI_NEW_TAB_FOOTER_NEW_TAB_FOOTER_HELPER_H_
 
 #include "chrome/browser/profiles/profile.h"
+#include "content/public/browser/web_contents.h"
 #include "url/gurl.h"
 
 class Profile;
@@ -13,6 +14,8 @@
 namespace ntp_footer {
 // Returns whether `url` belongs to an extension NTP.
 bool IsExtensionNtp(const GURL& url, Profile* profile);
+// Returns whether the extension attribution can be shown.
+bool CanShowExtensionFooter(const GURL& url, Profile* profile);
 }  // namespace ntp_footer
 
 #endif  // CHROME_BROWSER_UI_WEBUI_NEW_TAB_FOOTER_NEW_TAB_FOOTER_HELPER_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 2a14191..00f81e7 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
@@ -22,8 +22,10 @@
 #include "chrome/browser/ui/browser_list.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/tab_ui_helper.h"
 #include "chrome/browser/ui/tabs/alert/tab_alert.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/tab_utils.h"
 #include "chrome/browser/ui/tabs/test_tab_strip_model_delegate.h"
 #include "chrome/browser/ui/tabs/test_util.h"
@@ -969,6 +971,20 @@
   TabStripModel* fake_tab_strip_model() { return tab_strip_model_.get(); }
   Profile* testing_profile() { return testing_profile_.get(); }
 
+  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();
+    tab_features->SetTabUIHelperForTesting(
+        std::make_unique<TabUIHelper>(*tab_interface));
+    fake_tab_strip_model()->AppendTab(std::move(tab_model), false);
+    return tab_interface;
+  }
+
  private:
   std::unique_ptr<TestingProfile> testing_profile_;
   base::test::ScopedFeatureList feature_list_;
@@ -985,26 +1001,14 @@
   // Create stale tabs.
   std::vector<tabs::TabInterface*> stale_tabs_raw_ptr;
   for (int i = 0; i < 4; ++i) {
-    std::unique_ptr<tabs::TabModel> tab_model =
-        std::make_unique<tabs::TabModel>(
-            content::WebContents::Create(
-                content::WebContents::CreateParams(testing_profile())),
-            fake_tab_strip_model());
-    stale_tabs_raw_ptr.push_back(tab_model.get());
-    fake_tab_strip_model()->AppendTab(std::move(tab_model), false);
+    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) {
-    std::unique_ptr<tabs::TabModel> tab_model =
-        std::make_unique<tabs::TabModel>(
-            content::WebContents::Create(
-                content::WebContents::CreateParams(testing_profile())),
-            fake_tab_strip_model());
-    duplicate_tabs[duplicate_tabs_url].push_back(tab_model.get());
-    fake_tab_strip_model()->AppendTab(std::move(tab_model), false);
+    duplicate_tabs[duplicate_tabs_url].push_back(AppendBackgroundTab());
   }
 
   EXPECT_CALL(*tab_declutter_controller(), GetStaleTabs())
@@ -1035,30 +1039,19 @@
   // Create stale tabs.
   std::vector<tabs::TabInterface*> stale_tabs_raw_ptr;
   for (int i = 0; i < 4; ++i) {
-    std::unique_ptr<tabs::TabModel> tab_model =
-        std::make_unique<tabs::TabModel>(
-            content::WebContents::Create(
-                content::WebContents::CreateParams(testing_profile())),
-            fake_tab_strip_model());
-    stale_tabs_raw_ptr.push_back(tab_model.get());
-    fake_tab_strip_model()->AppendTab(std::move(tab_model), false);
+    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) {
-    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::TabInterface* const tab_interface = AppendBackgroundTab();
     auto navigation_simulator =
         content::NavigationSimulator::CreateBrowserInitiated(
-            duplicate_tabs_url, tab_model->GetContents());
+            duplicate_tabs_url, tab_interface->GetContents());
     navigation_simulator->Commit();
-    duplicate_tabs[duplicate_tabs_url].push_back(tab_model.get());
-    fake_tab_strip_model()->AppendTab(std::move(tab_model), false);
+    duplicate_tabs[duplicate_tabs_url].push_back(tab_interface);
   }
 
   EXPECT_CALL(*tab_declutter_controller(), GetStaleTabs())
@@ -1091,25 +1084,13 @@
   std::vector<tabs::TabInterface*> stale_tabs_raw_ptr;
 
   for (int i = 0; i < 4; ++i) {
-    std::unique_ptr<tabs::TabModel> tab_model =
-        std::make_unique<tabs::TabModel>(
-            content::WebContents::Create(
-                content::WebContents::CreateParams(testing_profile())),
-            fake_tab_strip_model());
-    stale_tabs_raw_ptr.push_back(tab_model.get());
-    fake_tab_strip_model()->AppendTab(std::move(tab_model), false);
+    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) {
-    std::unique_ptr<tabs::TabModel> tab_model =
-        std::make_unique<tabs::TabModel>(
-            content::WebContents::Create(
-                content::WebContents::CreateParams(testing_profile())),
-            fake_tab_strip_model());
-    duplicate_tabs[duplicate_tabs_url].push_back(tab_model.get());
-    fake_tab_strip_model()->AppendTab(std::move(tab_model), false);
+    duplicate_tabs[duplicate_tabs_url].push_back(AppendBackgroundTab());
   }
 
   EXPECT_CALL(*tab_declutter_controller(), GetStaleTabs())
@@ -1133,31 +1114,19 @@
 
   // Create 10 stale tabs.
   for (int i = 0; i < 10; ++i) {
-    std::unique_ptr<tabs::TabModel> tab_model =
-        std::make_unique<tabs::TabModel>(
-            content::WebContents::Create(
-                content::WebContents::CreateParams(testing_profile())),
-            fake_tab_strip_model());
-    stale_tabs_raw_ptr.push_back(tab_model.get());
-    fake_tab_strip_model()->AppendTab(std::move(tab_model), false);
+    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) {
-    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::TabInterface* const tab_interface = AppendBackgroundTab();
     auto navigation_simulator =
         content::NavigationSimulator::CreateBrowserInitiated(
-            duplicate_tabs_url, tab_model->GetContents());
+            duplicate_tabs_url, tab_interface->GetContents());
     navigation_simulator->Commit();
-    duplicate_tabs[duplicate_tabs_url].push_back(tab_model.get());
-    fake_tab_strip_model()->AppendTab(std::move(tab_model), false);
+    duplicate_tabs[duplicate_tabs_url].push_back(tab_interface);
   }
 
   EXPECT_CALL(*tab_declutter_controller(), GetStaleTabs())
@@ -1221,31 +1190,20 @@
 
   // Create 10 stale tabs.
   for (int i = 0; i < 10; ++i) {
-    std::unique_ptr<tabs::TabModel> tab_model =
-        std::make_unique<tabs::TabModel>(
-            content::WebContents::Create(
-                content::WebContents::CreateParams(testing_profile())),
-            fake_tab_strip_model());
-    stale_tabs_raw_ptr.push_back(tab_model.get());
-    fake_tab_strip_model()->AppendTab(std::move(tab_model), false);
+    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) {
-    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::TabInterface* const tab_interface = AppendBackgroundTab();
 
     auto navigation_simulator =
         content::NavigationSimulator::CreateBrowserInitiated(
-            duplicate_tabs_url, tab_model->GetContents());
+            duplicate_tabs_url, tab_interface->GetContents());
     navigation_simulator->Commit();
-    duplicate_tabs[duplicate_tabs_url].push_back(tab_model.get());
-    fake_tab_strip_model()->AppendTab(std::move(tab_model), false);
+    duplicate_tabs[duplicate_tabs_url].push_back(tab_interface);
   }
 
   EXPECT_CALL(*tab_declutter_controller(), GetStaleTabs())
diff --git a/chrome/build/android-arm32.pgo.txt b/chrome/build/android-arm32.pgo.txt
index 72c9568..6c77e39 100644
--- a/chrome/build/android-arm32.pgo.txt
+++ b/chrome/build/android-arm32.pgo.txt
@@ -1 +1 @@
-chrome-android32-main-1747936607-13cfbcc65a4f112c07e4996fe9d478fe4d2bf491-69fb47b82a6b54185ef4d8fb55a4d5986f147002.profdata
+chrome-android32-main-1747958116-71d90e08d2fa9a822583b99f4c96ec0d81aced54-27634fa381c6426758aa5ee8e41e07eb20de6241.profdata
diff --git a/chrome/build/android-arm64.pgo.txt b/chrome/build/android-arm64.pgo.txt
index aab9c892..22fc2e0 100644
--- a/chrome/build/android-arm64.pgo.txt
+++ b/chrome/build/android-arm64.pgo.txt
@@ -1 +1 @@
-chrome-android64-main-1747939698-5ed854c0d3de178bffe4287d730129210f18d97e-81491c26d7e19d0b6fd34afdd80ed104d6fca73a.profdata
+chrome-android64-main-1747953933-2c9850db71463d0ee7218eac1d206dc2de1c3eeb-f016a0d14d0e312bd1b35da74149506ea9d3c849.profdata
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 07e7e99..afb6534 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1747943845-4eca17c6187b57e881e4c677c7783458e390e7ae-0bf5f94cccb26bba96f5a138a90554069b08e752.profdata
+chrome-mac-arm-main-1747958116-56b29ed03fabeb23985e90cf4cbb8c7dff663295-27634fa381c6426758aa5ee8e41e07eb20de6241.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 8b13c22..51abc2bc 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1747914995-1a7d603e542c97b9fb3ee8057ff4e1f9c0f97cca-cdc6b2f711718e875f7f4dd35ce9bbf52c3704d5.profdata
+chrome-win32-main-1747936607-0f2275ac7ccea9479fe0d01329add34967ac4d9d-69fb47b82a6b54185ef4d8fb55a4d5986f147002.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index d2ec0f5..4991595cd 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1747925918-75b78bae67e7e33a17c7c18ad1ea8f11cde84c1e-13567e7795a503b19f15caf7456ab3235f01670c.profdata
+chrome-win64-main-1747936607-5a6137eb5450ca5d3616b870007034a682bdd0d1-69fb47b82a6b54185ef4d8fb55a4d5986f147002.profdata
diff --git a/chrome/common/extensions/api/experimental_actor.idl b/chrome/common/extensions/api/experimental_actor.idl
index 1e42bc9..629ea273 100644
--- a/chrome/common/extensions/api/experimental_actor.idl
+++ b/chrome/common/extensions/api/experimental_actor.idl
@@ -9,8 +9,10 @@
   callback ClosureCallback = void();
 
   interface Functions {
-    // Creates and starts a new task.  By default, opens a new tab to
-    // about:blank that is ready for actions.
+    // Creates and starts a new task. By default, opens a new tab to
+    // about:blank that is ready for actions. The startTaskProto can
+    // optionally specify a tab_id to use an existing tab instead of creating
+    // a new one.
     // startTaskProto: encoded optimization_guide.proto.BrowserStartTask
     // startTaskcallback:
     //   encoded optimization_guide.proto.BrowserStartTaskResult
diff --git a/chrome/renderer/chrome_content_renderer_client.cc b/chrome/renderer/chrome_content_renderer_client.cc
index 3b762714..a051c685 100644
--- a/chrome/renderer/chrome_content_renderer_client.cc
+++ b/chrome/renderer/chrome_content_renderer_client.cc
@@ -1704,11 +1704,16 @@
     blink::WebRuntimeFeatures::EnableAdTagging(true);
 
   if (IsStandaloneContentExtensionProcess()) {
-    // These Web APIs are only exposed to workers in extensions.
+    // These Web API features are exposed in extensions.
     blink::WebRuntimeFeatures::EnableWebUSBOnServiceWorkers(true);
 #if !BUILDFLAG(IS_ANDROID)
     blink::WebRuntimeFeatures::EnableWebHIDOnServiceWorkers(true);
 #endif  // !BUILDFLAG(IS_ANDROID)
+    if (blink::WebRuntimeFeatures::IsAIPromptAPIForExtensionEnabled() &&
+        base::FeatureList::IsEnabled(
+            blink::features::kAIPromptAPIForExtension)) {
+      blink::WebRuntimeFeatures::EnableAIPromptAPI(true);
+    }
     blink::WebRuntimeFeatures::EnableAIPromptAPIForWorkers(true);
     blink::WebRuntimeFeatures::EnableAIRewriterAPIForWorkers(true);
     blink::WebRuntimeFeatures::EnableAISummarizationAPIForWorkers(true);
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 7c56986..bd5d2923 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -2353,6 +2353,7 @@
       "//chrome/browser/ui/user_education",
       "//chrome/browser/ui/views/bubble",
       "//chrome/browser/ui/views/download",
+      "//chrome/browser/ui/views/new_tab_footer:browser_tests",
       "//chrome/browser/ui/views/page_action:page_action",
       "//chrome/browser/ui/views/privacy_sandbox",
       "//chrome/browser/ui/views/side_panel",
@@ -3484,6 +3485,7 @@
       "../browser/ui/views/location_bar/intent_chip_button_browsertest.cc",
       "../browser/ui/views/location_bar/intent_chip_button_test_base.cc",
       "../browser/ui/views/location_bar/intent_chip_button_test_base.h",
+      "../browser/ui/views/media_router/cast_dialog_coordinator_browsertest.cc",
       "../browser/ui/views/passwords/password_change/password_change_ui_browsertest.cc",
       "../browser/ui/views/privacy_sandbox/privacy_sandbox_dialog_view_browsertest.cc",
       "../browser/ui/views/profiles/avatar_toolbar_button_browsertest.cc",
@@ -8100,7 +8102,6 @@
       "//chrome/browser/ui/tabs:tab_strip",
       "//chrome/browser/ui/tabs:test_support",
       "//chrome/browser/ui/tabs:unit_tests",
-      "//chrome/browser/ui/views/new_tab_footer:unit_tests",
       "//chrome/browser/ui/views/page_action:unit_tests",
       "//chrome/browser/ui/views/webid:test_support",
       "//chrome/browser/ui/webui:webui_util",
@@ -10199,7 +10200,6 @@
       "../browser/ui/views/location_bar/icon_label_bubble_view_unittest.cc",
       "../browser/ui/views/location_bar/location_icon_view_unittest.cc",
       "../browser/ui/views/media_router/cast_dialog_access_code_cast_button_unittest.cc",
-      "../browser/ui/views/media_router/cast_dialog_coordinator_unittest.cc",
       "../browser/ui/views/media_router/cast_dialog_metrics_unittest.cc",
       "../browser/ui/views/media_router/cast_dialog_no_sinks_view_unittest.cc",
       "../browser/ui/views/media_router/cast_dialog_sink_button_unittest.cc",
@@ -11386,6 +11386,7 @@
         "../browser/ui/views/fullscreen_control/fullscreen_control_view_interactive_uitest.cc",
         "../browser/ui/views/keyboard_access_interactive_uitest.cc",
         "../browser/ui/views/location_bar/cookie_controls/cookie_controls_interactive_uitest.cc",
+        "../browser/ui/views/location_bar/lens_overlay_homework_page_action_icon_view_interactive_uitest.cc",
         "../browser/ui/views/location_bar/lens_overlay_page_action_icon_view_interactive_uitest.cc",
         "../browser/ui/views/location_bar/location_icon_view_interactive_uitest.cc",
         "../browser/ui/views/location_bar/merchant_trust_chip_button_interactive_uitest.cc",
diff --git a/chrome/test/data/extensions/api_test/runtime/uninstall_url/manifest.json b/chrome/test/data/extensions/api_test/runtime/uninstall_url/manifest.json
index ecab261..0012e15 100644
--- a/chrome/test/data/extensions/api_test/runtime/uninstall_url/manifest.json
+++ b/chrome/test/data/extensions/api_test/runtime/uninstall_url/manifest.json
@@ -5,8 +5,7 @@
   "description": "Browser test for chrome.runtime.setUninstallURL",
   "permissions": [
     "management",
-    "tabs",
-    "<all_urls>"
+    "tabs"
   ],
   "background": {
     "scripts": ["test.js"],
diff --git a/chrome/test/data/webui/print_preview/destination_dialog_interactive_test.ts b/chrome/test/data/webui/print_preview/destination_dialog_interactive_test.ts
index d325475..806af17 100644
--- a/chrome/test/data/webui/print_preview/destination_dialog_interactive_test.ts
+++ b/chrome/test/data/webui/print_preview/destination_dialog_interactive_test.ts
@@ -53,7 +53,7 @@
     const searchInput = dialog.$.searchBox.getSearchInput();
     assertTrue(!!searchInput);
     const whenFocusDone = eventToPromise('focus', searchInput);
-    dialog.destinationStore.startLoadAllDestinations();
+    dialog.destinationStore!.startLoadAllDestinations();
     dialog.show();
     return whenFocusDone;
   });
@@ -66,7 +66,7 @@
         const searchInput = searchBox.getSearchInput();
         assertTrue(!!searchInput);
         const whenFocusDone = eventToPromise('focus', searchInput);
-        dialog.destinationStore.startLoadAllDestinations();
+        dialog.destinationStore!.startLoadAllDestinations();
         dialog.show();
         return whenFocusDone
             .then(() => {
diff --git a/chrome/test/data/webui/print_preview/model_settings_availability_test.ts b/chrome/test/data/webui/print_preview/model_settings_availability_test.ts
index 079cf6b..693479f 100644
--- a/chrome/test/data/webui/print_preview/model_settings_availability_test.ts
+++ b/chrome/test/data/webui/print_preview/model_settings_availability_test.ts
@@ -15,6 +15,7 @@
   let model: PrintPreviewModelElement;
 
   function simulateCapabilitiesChange(capabilities: Cdd) {
+    assertTrue(!!model.destination);
     model.destination.capabilities = capabilities;
     // In prod code, capabilities changes are detected by print-preview-app
     // which then calls updateSettingsFromDestination().
@@ -45,6 +46,7 @@
   // These tests verify that the model correctly updates the settings
   // availability based on the destination and document info.
   test('copies', function() {
+    assertTrue(!!model.destination);
     assertTrue(model.getSetting('copies').available);
 
     // Set max copies to 1.
@@ -75,6 +77,7 @@
   });
 
   test('collate', function() {
+    assertTrue(!!model.destination);
     assertTrue(model.getSetting('collate').available);
 
     // Remove collate capability.
@@ -93,6 +96,8 @@
   });
 
   test('layout', async function() {
+    assertTrue(!!model.destination);
+
     // Layout is available since the printer has the capability and the
     // document is set to modifiable.
     assertTrue(model.getSetting('layout').available);
@@ -102,7 +107,7 @@
      {option: [{type: 'PORTRAIT', is_default: true}]},
      {option: [{type: 'LANDSCAPE', is_default: true}]},
     ].forEach(layoutCap => {
-      const capabilities = getCddTemplate(model.destination.id).capabilities!;
+      const capabilities = getCddTemplate(model.destination!.id).capabilities!;
       capabilities.printer.page_orientation = layoutCap;
       // Layout section should now be hidden.
       simulateCapabilitiesChange(capabilities);
@@ -129,6 +134,7 @@
   });
 
   test('color', function() {
+    assertTrue(!!model.destination);
     // Color is available since the printer has the capability.
     assertTrue(model.getSetting('color').available);
 
@@ -172,7 +178,7 @@
        colorCap: {option: [{type: 'CUSTOM_COLOR', vendor_id: '42'}]},
        expectedValue: true,
      }].forEach(capabilityAndValue => {
-      const capabilities = getCddTemplate(model.destination.id).capabilities!;
+      const capabilities = getCddTemplate(model.destination!.id).capabilities!;
       capabilities.printer.color = capabilityAndValue.colorCap;
       simulateCapabilitiesChange(capabilities);
       assertFalse(model.getSetting('color').available);
@@ -210,7 +216,7 @@
        },
        expectedValue: true,
      }].forEach(capabilityAndValue => {
-      const capabilities = getCddTemplate(model.destination.id).capabilities!;
+      const capabilities = getCddTemplate(model.destination!.id).capabilities!;
       capabilities.printer.color = capabilityAndValue.colorCap;
       simulateCapabilitiesChange(capabilities);
       assertEquals(
@@ -221,12 +227,13 @@
 
   function setSaveAsPdfDestination(): Promise<void> {
     const saveAsPdf = getSaveAsPdfDestination();
-    saveAsPdf.capabilities = getCddTemplate(model.destination.id).capabilities;
+    saveAsPdf.capabilities = getCddTemplate(saveAsPdf.id).capabilities;
     model.destination = saveAsPdf;
     return microtasksFinished();
   }
 
   test('media size', async function() {
+    assertTrue(!!model.destination);
     // Media size is available since the printer has the capability.
     assertTrue(model.getSetting('mediaSize').available);
 
@@ -287,6 +294,7 @@
   });
 
   test('dpi', function() {
+    assertTrue(!!model.destination);
     // The settings are available since the printer has multiple DPI options.
     assertTrue(model.getSetting('dpi').available);
 
@@ -424,6 +432,7 @@
     assertTrue(model.getSetting('headerFooter').available);
 
     // Small paper sizes
+    assertTrue(!!model.destination);
     const capabilities = getCddTemplate(model.destination.id).capabilities!;
     capabilities.printer.media_size = {
       'option': [
@@ -478,6 +487,7 @@
   });
 
   test('duplex', function() {
+    assertTrue(!!model.destination);
     assertTrue(model.getSetting('duplex').available);
     assertTrue(model.getSetting('duplexShortEdge').available);
 
diff --git a/chrome/test/data/webui/side_panel/customize_chrome/appearance_test.ts b/chrome/test/data/webui/side_panel/customize_chrome/appearance_test.ts
index b2dc801b..685d8a18 100644
--- a/chrome/test/data/webui/side_panel/customize_chrome/appearance_test.ts
+++ b/chrome/test/data/webui/side_panel/customize_chrome/appearance_test.ts
@@ -19,6 +19,16 @@
 
 import {$$, assertNotStyle, assertStyle, createBackgroundImage, createTheme, createThirdPartyThemeInfo, installMock} from './test_support.js';
 
+const newTabPageTypes = [
+  NewTabPageType.kFirstPartyWebUI,
+  NewTabPageType.kThirdPartyWebUI,
+  NewTabPageType.kThirdPartyRemote,
+  NewTabPageType.kExtension,
+  NewTabPageType.kIncognito,
+  NewTabPageType.kGuestMode,
+  NewTabPageType.kNone,
+];
+
 suite('AppearanceTest', () => {
   let appearanceElement: AppearanceElement;
   let callbackRouterRemote: CustomizeChromePageRemote;
@@ -619,6 +629,31 @@
     });
   });
 
+  suite('NtpFooterEnabled', () => {
+    suiteSetup(() => {
+      loadTimeData.overrideValues({
+        footerEnabled: true,
+      });
+    });
+
+    newTabPageTypes.forEach((t) => {
+      test(`classic chrome button NTP type ${t}`, async () => {
+        // Arrange.
+        const theme = createTheme();
+        theme.backgroundImage = createBackgroundImage('chrome://theme/foo');
+        callbackRouterRemote.setTheme(theme);
+        callbackRouterRemote.attachedTabStateUpdated(t);
+        await microtasksFinished();
+
+        // Assert.
+        assertEquals(
+            t === NewTabPageType.kFirstPartyWebUI ||
+                t === NewTabPageType.kThirdPartyWebUI,
+            !appearanceElement.$.setClassicChromeButton.hidden);
+      });
+    });
+  });
+
   test('source tab type should update the content', async () => {
     const idsControlledByIsSourceTabFirstPartyNtp = [
       '#editButtonsContainer',
@@ -632,21 +667,10 @@
       '#chromeColors',
       '#followThemeToggle',
       '#followThemeToggleControl',
-      '#setClassicChromeButton',
       '#editThemeButton',
       '#editThemeIcon',
     ];
 
-    const newTabPageTypes = [
-      NewTabPageType.kFirstPartyWebUI,
-      NewTabPageType.kThirdPartyWebUI,
-      NewTabPageType.kThirdPartyRemote,
-      NewTabPageType.kExtension,
-      NewTabPageType.kIncognito,
-      NewTabPageType.kGuestMode,
-      NewTabPageType.kNone,
-    ];
-
     const checkIdsVisibility = (sourceTabType: NewTabPageType) => {
       idsControlledByIsSourceTabFirstPartyNtp.forEach(
           id => assertEquals(
diff --git a/chromeos/ash/components/boca/babelorca/fakes/fake_caption_controller_delegate.cc b/chromeos/ash/components/boca/babelorca/fakes/fake_caption_controller_delegate.cc
index 154d564e..6a3c7c7 100644
--- a/chromeos/ash/components/boca/babelorca/fakes/fake_caption_controller_delegate.cc
+++ b/chromeos/ash/components/boca/babelorca/fakes/fake_caption_controller_delegate.cc
@@ -14,6 +14,7 @@
 #include "components/live_caption/caption_bubble_controller.h"
 #include "components/live_caption/caption_bubble_settings.h"
 #include "components/live_caption/views/caption_bubble_model.h"
+#include "components/live_caption/views/translation_view_wrapper_base.h"
 #include "components/prefs/pref_service.h"
 #include "media/mojo/mojom/speech_recognition.mojom.h"
 #include "media/mojo/mojom/speech_recognition_result.h"
@@ -73,7 +74,8 @@
 std::unique_ptr<captions::CaptionBubbleController>
 FakeCaptionControllerDelegate::CreateCaptionBubbleController(
     captions::CaptionBubbleSettings*,
-    const std::string&) {
+    const std::string&,
+    std::unique_ptr<captions::TranslationViewWrapperBase>) {
   caption_bubble_alive_ = true;
   ++create_bubble_controller_count_;
   return std::make_unique<FakeCaptionBubbleController>(this);
diff --git a/chromeos/ash/components/boca/babelorca/fakes/fake_caption_controller_delegate.h b/chromeos/ash/components/boca/babelorca/fakes/fake_caption_controller_delegate.h
index eece7a6..183fb92 100644
--- a/chromeos/ash/components/boca/babelorca/fakes/fake_caption_controller_delegate.h
+++ b/chromeos/ash/components/boca/babelorca/fakes/fake_caption_controller_delegate.h
@@ -17,6 +17,7 @@
 namespace captions {
 class CaptionBubbleController;
 class CaptionBubbleSettings;
+class TranslationViewWrapperBase;
 }  // namespace captions
 
 namespace media {
@@ -38,8 +39,10 @@
   ~FakeCaptionControllerDelegate() override;
 
   std::unique_ptr<captions::CaptionBubbleController>
-  CreateCaptionBubbleController(captions::CaptionBubbleSettings*,
-                                const std::string&) override;
+  CreateCaptionBubbleController(
+      captions::CaptionBubbleSettings*,
+      const std::string&,
+      std::unique_ptr<captions::TranslationViewWrapperBase>) override;
 
   void AddCaptionStyleObserver(ui::NativeThemeObserver* observer) override;
 
diff --git a/chromeos/ash/resources/internal b/chromeos/ash/resources/internal
index f6bfe79..7986129 160000
--- a/chromeos/ash/resources/internal
+++ b/chromeos/ash/resources/internal
@@ -1 +1 @@
-Subproject commit f6bfe79494ac7266c15b9139f25377ce1e59b22c
+Subproject commit 79861294509d9038645cd39ca828edd93abaf37b
diff --git a/chromeos/tast_control.gni b/chromeos/tast_control.gni
index 4f786b2..f8c6598 100644
--- a/chromeos/tast_control.gni
+++ b/chromeos/tast_control.gni
@@ -105,6 +105,9 @@
   # b/409349162
   "inputs.PhysicalKeyboardKoreanTyping@jacuzzi",
 
+  # b/419598394
+  "dlp.DataLeakPreventionRulesListFilesUSB.ash_blocked@brya",
+
   # READ COMMENT AT TOP BEFORE ADDING NEW TESTS HERE.
 ]
 
diff --git a/clank b/clank
index 890fb2ea..c2910b6 160000
--- a/clank
+++ b/clank
@@ -1 +1 @@
-Subproject commit 890fb2eabfdafcda1830d1aaf97ba4bf0c52866e
+Subproject commit c2910b6472c3849f25d534e1a00fe9691acf9f8d
diff --git a/components/autofill/core/common/autofill_payments_features.cc b/components/autofill/core/common/autofill_payments_features.cc
index 39270fad..de4f002 100644
--- a/components/autofill/core/common/autofill_payments_features.cc
+++ b/components/autofill/core/common/autofill_payments_features.cc
@@ -75,7 +75,7 @@
 // When enabled, card benefit source will be synced to Chrome clients.
 BASE_FEATURE(kAutofillEnableCardBenefitsSourceSync,
              "AutofillEnableCardBenefitsSourceSync",
-             base::FEATURE_DISABLED_BY_DEFAULT);
+             base::FEATURE_ENABLED_BY_DEFAULT);
 
 // When enabled, Chrome will show metadata along with other card information
 // when the virtual card is presented to users.
@@ -162,6 +162,12 @@
              "AutofillEnableLogFormEventsToAllParsedFormTypes",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+// When enabled, virtual card downstream enrollment will support multiple
+// requests at a time.
+BASE_FEATURE(kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollment,
+             "AutofillEnableMultipleRequestInVirtualCardDownstreamEnrollment",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 // When enabled, the card benefits toggle in settings will show updated text.
 BASE_FEATURE(kAutofillEnableNewCardBenefitsToggleText,
              "AutofillEnableNewCardBenefitsToggleText",
diff --git a/components/autofill/core/common/autofill_payments_features.h b/components/autofill/core/common/autofill_payments_features.h
index 3017c61..07fff5ce 100644
--- a/components/autofill/core/common/autofill_payments_features.h
+++ b/components/autofill/core/common/autofill_payments_features.h
@@ -58,6 +58,9 @@
 COMPONENT_EXPORT(AUTOFILL)
 BASE_DECLARE_FEATURE(kAutofillEnableLogFormEventsToAllParsedFormTypes);
 COMPONENT_EXPORT(AUTOFILL)
+BASE_DECLARE_FEATURE(
+    kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollment);
+COMPONENT_EXPORT(AUTOFILL)
 BASE_DECLARE_FEATURE(kAutofillEnableNewCardBenefitsToggleText);
 COMPONENT_EXPORT(AUTOFILL)
 BASE_DECLARE_FEATURE(kAutofillEnableNewFopDisplayDesktop);
diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/JavascriptOptimizerCategory.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/JavascriptOptimizerCategory.java
index b00bdcd..997b6c0 100644
--- a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/JavascriptOptimizerCategory.java
+++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/JavascriptOptimizerCategory.java
@@ -5,9 +5,13 @@
 package org.chromium.components.browser_ui.site_settings;
 
 import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
 
+import org.chromium.base.ApiCompatibilityUtils;
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.build.annotations.Nullable;
+import org.chromium.components.browser_ui.styles.SemanticColorUtils;
 import org.chromium.components.content_settings.ContentSettingsType;
 import org.chromium.components.permissions.OsAdditionalSecurityPermissionUtil;
 import org.chromium.content_public.browser.BrowserContextHandle;
@@ -36,4 +40,15 @@
         var provider = OsAdditionalSecurityPermissionUtil.getProviderInstance();
         return (provider == null) ? null : provider.getJavascriptOptimizerMessage(context);
     }
+
+    @Override
+    Drawable getDisabledInAndroidIcon(Context context) {
+        Drawable icon =
+                ApiCompatibilityUtils.getDrawable(
+                        context.getResources(), R.drawable.secured_by_brand_shield_24);
+        icon.mutate();
+        int disabledColor = SemanticColorUtils.getDefaultControlColorActive(context);
+        icon.setColorFilter(disabledColor, PorterDuff.Mode.SRC_IN);
+        return icon;
+    }
 }
diff --git a/components/enterprise/connectors/core/realtime_reporting_client_base.cc b/components/enterprise/connectors/core/realtime_reporting_client_base.cc
index 8dba204..47820e42 100644
--- a/components/enterprise/connectors/core/realtime_reporting_client_base.cc
+++ b/components/enterprise/connectors/core/realtime_reporting_client_base.cc
@@ -4,6 +4,8 @@
 
 #include "components/enterprise/connectors/core/realtime_reporting_client_base.h"
 
+#include <ctime>
+
 #include "base/containers/contains.h"
 #include "base/containers/to_value_list.h"
 #include "base/i18n/time_formatting.h"
@@ -186,9 +188,7 @@
 
   // If the timestamp is not set, it's a realtime event so use current time.
   if (!event.has_time()) {
-    int64_t timestamp_millis = base::Time::Now().InMillisecondsSinceUnixEpoch();
-    event.mutable_time()->set_seconds(timestamp_millis / 1000);
-    event.mutable_time()->set_nanos((timestamp_millis % 1000) * 1000000);
+    *event.mutable_time() = ToProtoTimestamp(base::Time::Now());
   }
 
 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
diff --git a/components/enterprise/connectors/core/reporting_utils.cc b/components/enterprise/connectors/core/reporting_utils.cc
index 71e3985..1122f58 100644
--- a/components/enterprise/connectors/core/reporting_utils.cc
+++ b/components/enterprise/connectors/core/reporting_utils.cc
@@ -92,6 +92,14 @@
       {kMaskedUsername, base::UTF16ToUTF8(username.substr(pos))});
 }
 
+::google3_protos::Timestamp ToProtoTimestamp(base::Time time) {
+  int64_t millis = time.InMillisecondsFSinceUnixEpoch();
+  ::google3_protos::Timestamp timestamp;
+  timestamp.set_seconds(millis / 1000);
+  timestamp.set_nanos((millis % 1000) * 1000000);
+  return timestamp;
+}
+
 std::unique_ptr<url_matcher::URLMatcher> CreateURLMatcherForOptInEvent(
     const enterprise_connectors::ReportingSettings& settings,
     const char* event_type) {
diff --git a/components/enterprise/connectors/core/reporting_utils.h b/components/enterprise/connectors/core/reporting_utils.h
index 748c95f1..c2e0b9cc 100644
--- a/components/enterprise/connectors/core/reporting_utils.h
+++ b/components/enterprise/connectors/core/reporting_utils.h
@@ -25,6 +25,9 @@
 // username should be masked.
 std::string MaskUsername(const std::u16string& username);
 
+// Convert base::Time to Timestamp proto.
+::google3_protos::Timestamp ToProtoTimestamp(base::Time);
+
 // Verify if the given `matcher` matches the `url`.
 bool IsUrlMatched(url_matcher::URLMatcher* matcher, const GURL& url);
 
diff --git a/components/js_injection/browser/navigation_listener_browsertest.cc b/components/js_injection/browser/navigation_listener_browsertest.cc
index 3dbc4f1..5c673d2 100644
--- a/components/js_injection/browser/navigation_listener_browsertest.cc
+++ b/components/js_injection/browser/navigation_listener_browsertest.cc
@@ -5,6 +5,7 @@
 #include <string>
 
 #include "base/json/json_writer.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "components/js_injection/browser/js_communication_host.h"
 #include "components/js_injection/browser/navigation_web_message_sender.h"
diff --git a/components/lens/lens_features.cc b/components/lens/lens_features.cc
index 51ea0191..5cd4968 100644
--- a/components/lens/lens_features.cc
+++ b/components/lens/lens_features.cc
@@ -484,6 +484,11 @@
         true};
 
 constexpr base::FeatureParam<bool>
+    kLensOverlayVisualSelectionUpdatesEnableRegionSelectedGlow{
+        &kLensOverlayVisualSelectionUpdates, "enable-region-selected-glow",
+        true};
+
+constexpr base::FeatureParam<bool>
     kLensOverlayVisualSelectionUpdatesCsbThumbnail{
         &kLensOverlayVisualSelectionUpdates, "enable-csb-thumbnail", true};
 
@@ -1037,6 +1042,11 @@
          kLensOverlayVisualSelectionUpdatesEnableGradientRegionStroke.Get();
 }
 
+bool GetVisualSelectionUpdatesEnableRegionSelectedGlow() {
+  return base::FeatureList::IsEnabled(kLensOverlayVisualSelectionUpdates) &&
+         kLensOverlayVisualSelectionUpdatesEnableRegionSelectedGlow.Get();
+}
+
 bool GetVisualSelectionUpdatesEnableCsbThumbnail() {
   return base::FeatureList::IsEnabled(kLensOverlayVisualSelectionUpdates) &&
          kLensOverlayVisualSelectionUpdatesCsbThumbnail.Get();
diff --git a/components/lens/lens_features.h b/components/lens/lens_features.h
index 1084c8e..9189254 100644
--- a/components/lens/lens_features.h
+++ b/components/lens/lens_features.h
@@ -773,6 +773,10 @@
 COMPONENT_EXPORT(LENS_FEATURES)
 extern bool GetVisualSelectionUpdatesEnableGradientRegionStroke();
 
+// Whether to enable the region selected glow for the visual selection updates.
+COMPONENT_EXPORT(LENS_FEATURES)
+extern bool GetVisualSelectionUpdatesEnableRegionSelectedGlow();
+
 // Whether to enable the thumbnail in the contextual searchbox.
 COMPONENT_EXPORT(LENS_FEATURES)
 extern bool GetVisualSelectionUpdatesEnableCsbThumbnail();
diff --git a/components/lens/lens_overlay_invocation_source.h b/components/lens/lens_overlay_invocation_source.h
index 0b532e2..57f6c1e5 100644
--- a/components/lens/lens_overlay_invocation_source.h
+++ b/components/lens/lens_overlay_invocation_source.h
@@ -45,7 +45,17 @@
   // The context menu when long pressing a web image.
   kContextMenu = 8,
 
-  kMaxValue = kContextMenu
+  // The Lens suggestion in the omnibox.
+  kOmniboxPageAction = 9,
+
+  // The contextual suggestions in the omnibox that take you directly to
+  // contextual answers in the side panel.
+  kOmniboxContextualSuggestion = 10,
+
+  // The Lens homework action chip in the omnibox.
+  kHomeworkActionChip = 11,
+
+  kMaxValue = kHomeworkActionChip
 };
 // LINT.ThenChange(//tools/metrics/histograms/metadata/lens/enums.xml:LensOverlayInvocationSource)
 // When adding a value here, also update:
diff --git a/components/lens/lens_overlay_metrics.cc b/components/lens/lens_overlay_metrics.cc
index 3f10ea9..a2a1da28 100644
--- a/components/lens/lens_overlay_metrics.cc
+++ b/components/lens/lens_overlay_metrics.cc
@@ -36,6 +36,12 @@
       return "LVFGallery";
     case LensOverlayInvocationSource::kContextMenu:
       return "ContextMenu";
+    case LensOverlayInvocationSource::kOmniboxPageAction:
+      return "OmniboxPageAction";
+    case LensOverlayInvocationSource::kOmniboxContextualSuggestion:
+      return "OmniboxContextualSuggestion";
+    case LensOverlayInvocationSource::kHomeworkActionChip:
+      return "HomeworkActionChip";
   }
 }
 
@@ -388,8 +394,17 @@
       event.SetFindInPage(time_to_first_interaction.InMilliseconds());
       break;
     case lens::LensOverlayInvocationSource::kOmnibox:
+    // TODO(crbug.com/419051875): Add separate UKM for homework action chip.
+    case lens::LensOverlayInvocationSource::kHomeworkActionChip:
       event.SetOmnibox(time_to_first_interaction.InMilliseconds());
       break;
+    case lens::LensOverlayInvocationSource::kOmniboxPageAction:
+      event.SetOmniboxPageAction(time_to_first_interaction.InMilliseconds());
+      break;
+    case lens::LensOverlayInvocationSource::kOmniboxContextualSuggestion:
+      event.SetOmniboxContextualSuggestion(
+          time_to_first_interaction.InMilliseconds());
+      break;
   }
   event.SetFirstInteractionType(static_cast<int64_t>(first_interaction_type))
       .Record(ukm::UkmRecorder::Get());
diff --git a/components/live_caption/caption_bubble_controller.h b/components/live_caption/caption_bubble_controller.h
index 909c28e0..6a1b482 100644
--- a/components/live_caption/caption_bubble_controller.h
+++ b/components/live_caption/caption_bubble_controller.h
@@ -22,6 +22,7 @@
 
 class CaptionBubbleContext;
 class CaptionBubbleSettings;
+class TranslationViewWrapperBase;
 
 ///////////////////////////////////////////////////////////////////////////////
 // Caption Bubble Controller
@@ -41,7 +42,8 @@
 
   static std::unique_ptr<CaptionBubbleController> Create(
       CaptionBubbleSettings* caption_bubble_settings,
-      const std::string& application_locale);
+      const std::string& application_locale,
+      std::unique_ptr<TranslationViewWrapperBase> translation_view_wrapper);
 
   // Called when the speech service has an error.  This should be part of
   // `CaptionControllerBase::Listener`, but the callbacks make this tricky.
diff --git a/components/live_caption/caption_controller_base.cc b/components/live_caption/caption_controller_base.cc
index 1cb7c6a..f08fb44 100644
--- a/components/live_caption/caption_controller_base.cc
+++ b/components/live_caption/caption_controller_base.cc
@@ -12,6 +12,8 @@
 #include "components/live_caption/caption_bubble_controller.h"
 #include "components/live_caption/caption_util.h"
 #include "components/live_caption/pref_names.h"
+#include "components/live_caption/views/translation_view_wrapper.h"
+#include "components/live_caption/views/translation_view_wrapper_base.h"
 #include "components/prefs/pref_change_registrar.h"
 #include "components/prefs/pref_service.h"
 #include "ui/native_theme/native_theme.h"
@@ -36,9 +38,12 @@
 
   std::unique_ptr<CaptionBubbleController> CreateCaptionBubbleController(
       CaptionBubbleSettings* caption_bubble_settings,
-      const std::string& application_locale) override {
+      const std::string& application_locale,
+      std::unique_ptr<TranslationViewWrapperBase> translation_view_wrapper)
+      override {
     return CaptionBubbleController::Create(caption_bubble_settings,
-                                           application_locale);
+                                           application_locale,
+                                           std::move(translation_view_wrapper));
   }
 
   void AddCaptionStyleObserver(ui::NativeThemeObserver* observer) override {
@@ -84,7 +89,8 @@
   is_ui_constructed_ = true;
 
   auto controller = delegate_->CreateCaptionBubbleController(
-      caption_bubble_settings(), application_locale_);
+      caption_bubble_settings(), application_locale_,
+      CreateTranslationViewWrapper());
   caption_bubble_controller_ = controller.get();
   AddListener(std::move(controller));
 
@@ -138,6 +144,11 @@
   return caption_bubble_controller_.get();
 }
 
+std::unique_ptr<TranslationViewWrapperBase>
+CaptionControllerBase::CreateTranslationViewWrapper() {
+  return std::make_unique<TranslationViewWrapper>(caption_bubble_settings());
+}
+
 void CaptionControllerBase::OnCaptionStyleUpdated() {
   if (!caption_bubble_controller_) {
     return;
diff --git a/components/live_caption/caption_controller_base.h b/components/live_caption/caption_controller_base.h
index a4575958..96f2f907 100644
--- a/components/live_caption/caption_controller_base.h
+++ b/components/live_caption/caption_controller_base.h
@@ -27,6 +27,7 @@
 class CaptionBubbleContext;
 class CaptionBubbleController;
 class CaptionBubbleSettings;
+class TranslationViewWrapperBase;
 
 class CaptionControllerBase : public ui::NativeThemeObserver {
  public:
@@ -40,7 +41,9 @@
     virtual std::unique_ptr<CaptionBubbleController>
     CreateCaptionBubbleController(
         CaptionBubbleSettings* caption_bubble_settings,
-        const std::string& application_locale) = 0;
+        const std::string& application_locale,
+        std::unique_ptr<TranslationViewWrapperBase>
+            translation_view_wrapper) = 0;
 
     virtual void AddCaptionStyleObserver(ui::NativeThemeObserver* observer) = 0;
 
@@ -133,6 +136,9 @@
   PrefChangeRegistrar* pref_change_registrar() const;
   CaptionBubbleController* caption_bubble_controller() const;
 
+  virtual std::unique_ptr<TranslationViewWrapperBase>
+  CreateTranslationViewWrapper();
+
  private:
   virtual CaptionBubbleSettings* caption_bubble_settings() = 0;
 
diff --git a/components/live_caption/caption_controller_base_unittest.cc b/components/live_caption/caption_controller_base_unittest.cc
index b2a66b017..560afc1 100644
--- a/components/live_caption/caption_controller_base_unittest.cc
+++ b/components/live_caption/caption_controller_base_unittest.cc
@@ -10,6 +10,7 @@
 #include "components/live_caption/caption_bubble_context.h"
 #include "components/live_caption/caption_bubble_controller.h"
 #include "components/live_caption/pref_names.h"
+#include "components/live_caption/views/translation_view_wrapper_base.h"
 #include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/pref_service.h"
 #include "components/prefs/testing_pref_service.h"
@@ -136,14 +137,16 @@
  public:
   explicit MockCaptionControllerDelegate(
       std::unique_ptr<CaptionBubbleController> bubble_controller) {
-    EXPECT_CALL(*this, CreateCaptionBubbleController(_, _))
+    EXPECT_CALL(*this, CreateCaptionBubbleController(_, _, _))
         .WillOnce(Return(std::move(bubble_controller)));
   }
   ~MockCaptionControllerDelegate() override = default;
 
   MOCK_METHOD(std::unique_ptr<CaptionBubbleController>,
               CreateCaptionBubbleController,
-              (CaptionBubbleSettings*, const std::string&),
+              (CaptionBubbleSettings*,
+               const std::string&,
+               std::unique_ptr<TranslationViewWrapperBase>),
               (override));
 
   void AddCaptionStyleObserver(ui::NativeThemeObserver*) override {}
diff --git a/components/live_caption/live_caption_controller_unittest.cc b/components/live_caption/live_caption_controller_unittest.cc
index ea16d2b..b746717 100644
--- a/components/live_caption/live_caption_controller_unittest.cc
+++ b/components/live_caption/live_caption_controller_unittest.cc
@@ -15,6 +15,7 @@
 #include "components/live_caption/caption_bubble_context.h"
 #include "components/live_caption/caption_bubble_controller.h"
 #include "components/live_caption/pref_names.h"
+#include "components/live_caption/views/translation_view_wrapper_base.h"
 #include "components/pref_registry/pref_registry_syncable.h"
 #include "components/prefs/pref_service.h"
 #include "components/prefs/testing_pref_service.h"
@@ -37,7 +38,9 @@
 
   MOCK_METHOD(std::unique_ptr<CaptionBubbleController>,
               CreateCaptionBubbleController,
-              (CaptionBubbleSettings*, const std::string&),
+              (CaptionBubbleSettings*,
+               const std::string&,
+               std::unique_ptr<TranslationViewWrapperBase>),
               (override));
 
   void AddCaptionStyleObserver(ui::NativeThemeObserver*) override {}
diff --git a/components/live_caption/views/caption_bubble_controller_views.cc b/components/live_caption/views/caption_bubble_controller_views.cc
index 5cf15f0..b7216b8 100644
--- a/components/live_caption/views/caption_bubble_controller_views.cc
+++ b/components/live_caption/views/caption_bubble_controller_views.cc
@@ -29,10 +29,11 @@
 // Static
 std::unique_ptr<CaptionBubbleController> CaptionBubbleController::Create(
     CaptionBubbleSettings* caption_bubble_settings,
-    const std::string& application_locale) {
+    const std::string& application_locale,
+    std::unique_ptr<TranslationViewWrapperBase> translation_view_wrapper) {
   return std::make_unique<CaptionBubbleControllerViews>(
       caption_bubble_settings, application_locale,
-      std::make_unique<TranslationViewWrapper>(caption_bubble_settings));
+      std::move(translation_view_wrapper));
 }
 
 CaptionBubbleControllerViews::CaptionBubbleControllerViews(
diff --git a/components/media_effects/OWNERS b/components/media_effects/OWNERS
index d4987c9..58c950b 100644
--- a/components/media_effects/OWNERS
+++ b/components/media_effects/OWNERS
@@ -1,4 +1,3 @@
 bryantchandler@chromium.org
 mfoltz@chromium.org
-bialpio@chromium.org
 ahmedmoussa@google.com
diff --git a/components/messages/android/DEPS b/components/messages/android/DEPS
index 3c1bde2..32a168c 100644
--- a/components/messages/android/DEPS
+++ b/components/messages/android/DEPS
@@ -8,7 +8,7 @@
   "+components/browser_ui/widget/android",
   "+components/browser_ui/styles/android",
   "+third_party/skia/include",
-  "+ui/accessibility",
+  "+ui/accessibility/android",
   "+ui/android",
   "+ui/gfx/android",
 ]
diff --git a/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerCoordinator.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerCoordinator.java
index 2427d78..b0ed817f 100644
--- a/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerCoordinator.java
+++ b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerCoordinator.java
@@ -5,15 +5,20 @@
 package org.chromium.components.messages;
 
 import android.animation.Animator;
+import android.content.Context;
 import android.content.res.Resources;
 import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.VisibleForTesting;
 import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityEventCompat;
 
 import org.chromium.base.supplier.Supplier;
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.build.annotations.Nullable;
+import org.chromium.components.messages.MessageFeatureMap.AccessibilityEventInvestigationGroup;
 import org.chromium.components.messages.MessageStateHandler.Position;
 import org.chromium.ui.accessibility.AccessibilityState;
 import org.chromium.ui.listmenu.ListMenuHost.PopupMenuShownListener;
@@ -193,6 +198,7 @@
                 () -> {
                     setOnTouchRunnable(null);
                     setOnTitleChanged(null);
+                    sendPaneChangeAccessibilityEvent(/* isShowing= */ false);
                     messageHidden.run();
                 });
     }
@@ -220,6 +226,82 @@
             msg = mView.getResources().getString(R.string.message_new_actions_available);
         }
         ViewCompat.setAccessibilityPaneTitle(mParentView, msg);
+        sendPaneChangeAccessibilityEvent(/* isShowing= */ true);
+    }
+
+    /**
+     * Sends accessibility events for pane appearance/disappearance when the message is shown/hidden
+     * respectively. This should ideally move accessibility focus automatically to/out of the
+     * message view as applicable.
+     *
+     * @param isShowing Whether the message is visible. {@code true} if shown, {@code false} if
+     *     hidden.
+     */
+    @SuppressWarnings("WrongConstant")
+    private void sendPaneChangeAccessibilityEvent(boolean isShowing) {
+        // We will only send an AccessibilityEvent when running the
+        // MessagesAccessibilityEventInvestigations experiment since this is known to crash in some
+        // cases. The experiment will provide more hints to crash root cause and possible solutions.
+        if (!MessageFeatureList.isMessagesAccessibilityEventInvestigationsEnabled()) {
+            return;
+        }
+
+        AccessibilityEvent event =
+                AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+        if (isShowing) {
+            event.setContentChangeTypes(AccessibilityEventCompat.CONTENT_CHANGE_TYPE_PANE_APPEARED);
+        } else {
+            event.setContentChangeTypes(
+                    AccessibilityEventCompat.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED);
+        }
+
+        // The MessagesAccessibilityEventInvestigations has various arms to try potential solutions.
+        int approach = MessageFeatureList.getMessagesAccessibilityEventInvestigationsParam();
+        switch (approach) {
+            case AccessibilityEventInvestigationGroup.ENABLED_BASELINE:
+                // Case 1 = "EnabledBaseline" group. This is our baseline group, which acts similar
+                // to a control group for the experiment. This is the ideal implementation that we
+                // would expect not to crash, and we want to compare the other implementations
+                // against this one.
+                if (AccessibilityState.isAnyAccessibilityServiceEnabled()) {
+                    mView.requestSendAccessibilityEvent(mView, event);
+                }
+                break;
+            case AccessibilityEventInvestigationGroup.ENABLED_WITH_ACCESSIBILITY_STATE:
+                // Case 2 = "EnabledWithAccessibilityState" group. For this group, we will send the
+                // event through the AccessibilityState instead.
+                AccessibilityState.sendAccessibilityEvent(event);
+                break;
+            case AccessibilityEventInvestigationGroup.ENABLED_WITH_RESTRICTIVE_SERVICE_CHECK:
+                // Case 3 = "EnabledWithRestrictiveServiceCheck" group. For this group, we will send
+                // the event as we normally would, but for a more restrictive group of services.
+                if (AccessibilityState.isComplexUserInteractionServiceEnabled()) {
+                    mView.requestSendAccessibilityEvent(mView, event);
+                }
+                break;
+            case AccessibilityEventInvestigationGroup.ENABLED_WITH_MASK_CHECK:
+                // Case 4 = "EnabledWithMaskCheck" group. For this group, we will not check the
+                // enabled services, but instead look at the requested event types, and only send
+                // the event if some service has requested its type.
+                if (AccessibilityState.relevantEventTypesForCurrentServices()
+                        .contains(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)) {
+                    mView.requestSendAccessibilityEvent(mView, event);
+                }
+                break;
+            case AccessibilityEventInvestigationGroup.ENABLED_WITH_DIRECT_QUERY:
+                // Case 5 = "EnabledWithDirectQuery" group. For this group, we will directly check
+                // the current accessibility state against the framework before sending the event.
+                AccessibilityManager manager =
+                        (AccessibilityManager)
+                                mView.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
+                if (manager.isEnabled()) {
+                    mView.requestSendAccessibilityEvent(mView, event);
+                }
+                break;
+            default:
+                // For any other value (default or bad param), we do not want to send any event.
+                break;
+        }
     }
 
     private void setOnTitleChanged(@Nullable Runnable runnable) {
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageFeatureList.java b/components/messages/android/java/src/org/chromium/components/messages/MessageFeatureList.java
index 0b34805..038b339 100644
--- a/components/messages/android/java/src/org/chromium/components/messages/MessageFeatureList.java
+++ b/components/messages/android/java/src/org/chromium/components/messages/MessageFeatureList.java
@@ -5,6 +5,7 @@
 package org.chromium.components.messages;
 
 import org.chromium.build.annotations.NullMarked;
+import org.chromium.components.messages.MessageFeatureMap.AccessibilityEventInvestigationGroup;
 
 /**
  * Lists base::Features that can be accessed through {@link MessageFeatureMap}.
@@ -21,11 +22,12 @@
     public static final String MESSAGES_ANDROID_EXTRA_HISTOGRAMS = "MessagesAndroidExtraHistograms";
     public static final String MESSAGES_CLOSE_BUTTON = "MessagesCloseButton";
 
-    public static int getMessagesAccessibilityEventInvestigationsParam() {
+    public static @AccessibilityEventInvestigationGroup int
+            getMessagesAccessibilityEventInvestigationsParam() {
         return getFieldTrialParamByFeatureAsInt(
                 MESSAGES_ACCESSIBILITY_EVENT_INVESTIGATIONS,
                 "messages_accessibility_events_investigations_param",
-                0);
+                AccessibilityEventInvestigationGroup.DEFAULT);
     }
 
     public static boolean isMessagesAccessibilityEventInvestigationsEnabled() {
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageFeatureMap.java b/components/messages/android/java/src/org/chromium/components/messages/MessageFeatureMap.java
index 987d29e..3059f49f 100644
--- a/components/messages/android/java/src/org/chromium/components/messages/MessageFeatureMap.java
+++ b/components/messages/android/java/src/org/chromium/components/messages/MessageFeatureMap.java
@@ -4,18 +4,42 @@
 
 package org.chromium.components.messages;
 
+import androidx.annotation.IntDef;
+
 import org.jni_zero.JNINamespace;
 import org.jni_zero.NativeMethods;
 
 import org.chromium.base.FeatureMap;
 import org.chromium.build.annotations.NullMarked;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /** Java accessor for base::Features listed in {@link MessageFeatureList} */
 @JNINamespace("messages")
 @NullMarked
 public final class MessageFeatureMap extends FeatureMap {
     private static final MessageFeatureMap sInstance = new MessageFeatureMap();
 
+    /** Int values for the param of the MessagesAccessibilityEventInvestigations experiment. */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+        AccessibilityEventInvestigationGroup.DEFAULT,
+        AccessibilityEventInvestigationGroup.ENABLED_BASELINE,
+        AccessibilityEventInvestigationGroup.ENABLED_WITH_ACCESSIBILITY_STATE,
+        AccessibilityEventInvestigationGroup.ENABLED_WITH_RESTRICTIVE_SERVICE_CHECK,
+        AccessibilityEventInvestigationGroup.ENABLED_WITH_MASK_CHECK,
+        AccessibilityEventInvestigationGroup.ENABLED_WITH_DIRECT_QUERY,
+    })
+    public @interface AccessibilityEventInvestigationGroup {
+        int DEFAULT = 0;
+        int ENABLED_BASELINE = 1;
+        int ENABLED_WITH_ACCESSIBILITY_STATE = 2;
+        int ENABLED_WITH_RESTRICTIVE_SERVICE_CHECK = 3;
+        int ENABLED_WITH_MASK_CHECK = 4;
+        int ENABLED_WITH_DIRECT_QUERY = 5;
+    }
+
     // Do not instantiate this class.
     private MessageFeatureMap() {}
 
diff --git a/components/omnibox/browser/autocomplete_controller.cc b/components/omnibox/browser/autocomplete_controller.cc
index f9ee3617..a34b48c 100644
--- a/components/omnibox/browser/autocomplete_controller.cc
+++ b/components/omnibox/browser/autocomplete_controller.cc
@@ -1438,20 +1438,22 @@
 
   MlRerank(old_result);
 
+  // If the entrypoints aren't visible, then Lens is active and contextual
+  // suggestions shouldn't be shown.
+  const bool is_lens_active =
+      !autocomplete_provider_client()->AreLensEntrypointsVisible();
   if (update_type == UpdateType::kSyncPass ||
       update_type == UpdateType::kAsyncPass ||
       update_type == UpdateType::kLastAsyncPassExceptDoc) {
-    internal_result_.SortAndCull(
-        input_, template_url_service_, triggered_feature_service_,
-        autocomplete_provider_client()->IsLensEnabled(),
-        old_result.default_match_to_preserve);
+    internal_result_.SortAndCull(input_, template_url_service_,
+                                 triggered_feature_service_, is_lens_active,
+                                 old_result.default_match_to_preserve);
     internal_result_.TransferOldMatches(input_,
                                         &old_result.matches_to_transfer);
   }
 
   internal_result_.SortAndCull(input_, template_url_service_,
-                               triggered_feature_service_,
-                               autocomplete_provider_client()->IsLensEnabled(),
+                               triggered_feature_service_, is_lens_active,
                                old_result.default_match_to_preserve);
 
   if (update_type == UpdateType::kSyncPass) {
diff --git a/components/omnibox/browser/autocomplete_provider_client.cc b/components/omnibox/browser/autocomplete_provider_client.cc
index a0c5423..02af4c0 100644
--- a/components/omnibox/browser/autocomplete_provider_client.cc
+++ b/components/omnibox/browser/autocomplete_provider_client.cc
@@ -49,6 +49,10 @@
   return false;
 }
 
+bool AutocompleteProviderClient::AreLensEntrypointsVisible() const {
+  return false;
+}
+
 bool AutocompleteProviderClient::in_background_state() const {
   return false;
 }
diff --git a/components/omnibox/browser/autocomplete_provider_client.h b/components/omnibox/browser/autocomplete_provider_client.h
index d3fe88ad..d4bfca71 100644
--- a/components/omnibox/browser/autocomplete_provider_client.h
+++ b/components/omnibox/browser/autocomplete_provider_client.h
@@ -228,6 +228,10 @@
   // Returns true if the current profile is eligible for Lens.
   virtual bool IsLensEnabled() const;
 
+  // Returns true if the Lens entrypoints can be shown to the user. That is if
+  // Lens is not already active.
+  virtual bool AreLensEntrypointsVisible() const;
+
   // Returns whether the app is currently in the background state (Mobile only).
   virtual bool in_background_state() const;
 
diff --git a/components/omnibox/browser/mock_autocomplete_provider_client.h b/components/omnibox/browser/mock_autocomplete_provider_client.h
index afcf96f4..f6ba07ac 100644
--- a/components/omnibox/browser/mock_autocomplete_provider_client.h
+++ b/components/omnibox/browser/mock_autocomplete_provider_client.h
@@ -153,6 +153,7 @@
   MOCK_CONST_METHOD0(IsHistoryEmbeddingsEnabled, bool());
   MOCK_CONST_METHOD0(IsHistoryEmbeddingsSettingVisible, bool());
   MOCK_CONST_METHOD0(IsLensEnabled, bool());
+  MOCK_CONST_METHOD0(AreLensEntrypointsVisible, bool());
   MOCK_CONST_METHOD1(GetLensSuggestInputsWhenReady,
                      base::CallbackListSubscription(
                          LensOverlaySuggestInputsCallback callback));
diff --git a/components/omnibox/browser/zero_suggest_provider.cc b/components/omnibox/browser/zero_suggest_provider.cc
index 6627d26..e1017e5b 100644
--- a/components/omnibox/browser/zero_suggest_provider.cc
+++ b/components/omnibox/browser/zero_suggest_provider.cc
@@ -560,9 +560,7 @@
   }
 
   // Do not start a request if async requests are disallowed.
-  if (input.omit_asynchronous_matches() ||
-      omnibox_feature_configs::ContextualSearch::Get()
-          .IsEnabledWithPrefetch()) {
+  if (input.omit_asynchronous_matches()) {
     return;
   }
 
@@ -704,6 +702,13 @@
   loader_.reset();
   done_ = true;
 
+  // The contextual search experience intentionally updates the cache with
+  // latest received results, but does not publish the matches asynchronously.
+  if (omnibox_feature_configs::ContextualSearch::Get()
+          .IsEnabledWithPrefetch()) {
+    return;
+  }
+
   // For display stability reasons, update the displayed results with the remote
   // response only if they are empty or if an empty result set is received. In
   // the latter case, the displayed results may no longer be valid to be shown.
diff --git a/components/optimization_guide/core/model_execution/on_device_model_service_controller.h b/components/optimization_guide/core/model_execution/on_device_model_service_controller.h
index c0f4e29..b1aeafd 100644
--- a/components/optimization_guide/core/model_execution/on_device_model_service_controller.h
+++ b/components/optimization_guide/core/model_execution/on_device_model_service_controller.h
@@ -158,10 +158,6 @@
     return safety_client_.language_detection_model_path();
   }
 
-  mojo::Remote<on_device_model::mojom::OnDeviceModel>& base_model_remote() {
-    return base_model_controller_->remote();
-  }
-
  private:
   // A set of (references to) compatible, versioned dependencies that implement
   // a ModelBasedCapability.
@@ -234,11 +230,6 @@
 
     on_device_model::ModelAssetPaths PopulateModelPaths();
 
-    // TODO(holte): Eliminate this accessor
-    mojo::Remote<on_device_model::mojom::OnDeviceModel>& remote() {
-      return remote_;
-    }
-
     OnDeviceModelMetadata* model_metadata() { return model_metadata_.get(); }
 
     base::WeakPtr<BaseModelController> GetWeakPtr() {
diff --git a/components/optimization_guide/internal b/components/optimization_guide/internal
index 46d95254..aa7ee4b5 160000
--- a/components/optimization_guide/internal
+++ b/components/optimization_guide/internal
@@ -1 +1 @@
-Subproject commit 46d95254ba6bdec345073553c6f04b32cfe0a62b
+Subproject commit aa7ee4b538e9adb19542ef9b4be4674196aaf859
diff --git a/components/policy/resources/templates/policy_definitions/Miscellaneous/WebXRImmersiveArEnabled.yaml b/components/policy/resources/templates/policy_definitions/Miscellaneous/WebXRImmersiveArEnabled.yaml
index 1039da2..bdf8fa3 100644
--- a/components/policy/resources/templates/policy_definitions/Miscellaneous/WebXRImmersiveArEnabled.yaml
+++ b/components/policy/resources/templates/policy_definitions/Miscellaneous/WebXRImmersiveArEnabled.yaml
@@ -21,7 +21,7 @@
     sessions
   value: false
 owners:
-- bialpio@chromium.org
+- alcooper@chromium.org
 - xr-dev@chromium.org
 schema:
   type: boolean
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 8348fdf..a8cd65b 100644
--- a/components/saved_tab_groups/internal/saved_tab_group_model.cc
+++ b/components/saved_tab_groups/internal/saved_tab_group_model.cc
@@ -543,6 +543,30 @@
   }
 }
 
+void SavedTabGroupModel::UpdatePositionForSharedGroupFromSync(
+    const base::Uuid& group_id,
+    std::optional<size_t> position) {
+  const SavedTabGroup* group = Get(group_id);
+  if (!group || !group->is_shared_tab_group() ||
+      group->position() == position) {
+    return;
+  }
+
+  // Remove the tab group, set position and reinsert.
+  const int index = GetIndexOf(group_id).value();
+  SavedTabGroup saved_group = RemoveImpl(index);
+  if (position.has_value()) {
+    saved_group.SetPosition(position.value());
+  } else {
+    saved_group.SetPinned(false);
+  }
+  InsertGroupImpl(std::move(saved_group));
+
+  for (SavedTabGroupModelObserver& observer : observers_) {
+    observer.SavedTabGroupUpdatedFromSync(group_id, /*tab_guid=*/std::nullopt);
+  }
+}
+
 void SavedTabGroupModel::UpdateLastUpdaterCacheGuidForGroup(
     const std::optional<std::string>& cache_guid,
     const LocalTabGroupID& group_id,
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 98312f0..37bc3a37 100644
--- a/components/saved_tab_groups/internal/saved_tab_group_model.h
+++ b/components/saved_tab_groups/internal/saved_tab_group_model.h
@@ -207,6 +207,11 @@
                              base::Time time,
                              TriggerSource source);
 
+  // Update the position for a share group from sync. If the position is
+  // nullopt, the group will be moved to the end of the list.
+  void UpdatePositionForSharedGroupFromSync(const base::Uuid& group_id,
+                                            std::optional<size_t> position);
+
   // Update the last updater cache guid for a give group and optionally a tab.
   void UpdateLastUpdaterCacheGuidForGroup(
       const std::optional<std::string>& cache_guid,
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 4a696fe..f844f7d3 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
@@ -1382,6 +1382,49 @@
                                 .last_seen_time());
 }
 
+TEST_F(SavedTabGroupModelTest, UpdatePositionForSharedGroupFromSyncFromSync) {
+  RemoveTestData();
+
+  // Create some tab groups with initial orders.
+  SavedTabGroup group_1(u"Group 1", tab_groups::TabGroupColorId::kRed, {}, 0);
+  SavedTabGroup group_2(u"Group 2", tab_groups::TabGroupColorId::kOrange, {},
+                        1);
+  SavedTabGroup group_3(u"Group 3", tab_groups::TabGroupColorId::kYellow, {},
+                        2);
+  SavedTabGroup group_4(u"Group 4", tab_groups::TabGroupColorId::kGreen, {},
+                        std::nullopt);
+
+  group_1.SetCollaborationId(CollaborationId("collaboration"));
+  group_2.SetCollaborationId(CollaborationId("collaboration"));
+  group_3.SetCollaborationId(CollaborationId("collaboration"));
+  group_4.SetCollaborationId(CollaborationId("collaboration"));
+
+  // This is the order we expect the groups in the model to be.
+  std::vector<SavedTabGroup> groups = {group_3, group_2, group_4, group_1};
+
+  // Add the groups into the model in an arbitrary order.
+  saved_tab_group_model_->AddedLocally(group_1);
+  saved_tab_group_model_->AddedLocally(group_2);
+  saved_tab_group_model_->AddedLocally(group_3);
+  saved_tab_group_model_->AddedLocally(group_4);
+
+  // Change the order of groups.
+  saved_tab_group_model_->UpdatePositionForSharedGroupFromSync(
+      group_1.saved_guid(), std::nullopt);
+  saved_tab_group_model_->UpdatePositionForSharedGroupFromSync(
+      group_2.saved_guid(), 1);
+  saved_tab_group_model_->UpdatePositionForSharedGroupFromSync(
+      group_3.saved_guid(), 0);
+  saved_tab_group_model_->UpdatePositionForSharedGroupFromSync(
+      group_4.saved_guid(), 2);
+
+  EXPECT_EQ(saved_tab_group_model_->saved_tab_groups().size(), groups.size());
+  for (size_t i = 0; i < groups.size(); ++i) {
+    EXPECT_EQ(groups[i].saved_guid(),
+              saved_tab_group_model_->saved_tab_groups()[i].saved_guid());
+  }
+}
+
 }  // namespace
 
 }  // namespace tab_groups
diff --git a/components/saved_tab_groups/internal/shared_tab_group_account_data_sync_bridge.cc b/components/saved_tab_groups/internal/shared_tab_group_account_data_sync_bridge.cc
index 2839986a..e161e95 100644
--- a/components/saved_tab_groups/internal/shared_tab_group_account_data_sync_bridge.cc
+++ b/components/saved_tab_groups/internal/shared_tab_group_account_data_sync_bridge.cc
@@ -46,6 +46,11 @@
   return tab_guid.AsLowercaseString() + "|" + collaboration_id.value();
 }
 
+std::string CreateClientTagForSharedGroup(const SavedTabGroup& group) {
+  return group.saved_guid().AsLowercaseString() + "|" +
+         group.collaboration_id().value().value();
+}
+
 // Returns the client tag for this specifics object. Note that
 // SharedTabGroupAccountDataSpecifics uses the client tag as a storage key.
 std::string GetClientTagFromSpecifics(
@@ -98,6 +103,24 @@
   return entity_data;
 }
 
+std::unique_ptr<syncer::EntityData> CreateEntityDataFromSharedTabGroup(
+    const SavedTabGroupModel& model,
+    const SavedTabGroup& tab_group) {
+  CHECK(tab_group.is_shared_tab_group());
+
+  sync_pb::SharedTabGroupAccountDataSpecifics specifics;
+  specifics.set_guid(tab_group.saved_guid().AsLowercaseString());
+  specifics.set_collaboration_id(tab_group.collaboration_id()->value());
+
+  sync_pb::SharedTabGroupDetails* tab_group_details =
+      specifics.mutable_shared_tab_group_details();
+  if (tab_group.position().has_value()) {
+    tab_group_details->set_pinned_position(tab_group.position().value());
+  }
+
+  return CreateEntityDataFromSpecifics(specifics);
+}
+
 bool SharedTabExistsForSpecifics(
     const SavedTabGroupModel& model,
     const sync_pb::SharedTabGroupAccountDataSpecifics& specifics) {
@@ -115,6 +138,28 @@
   return group->GetTab(tab_id) != nullptr;
 }
 
+bool IsTabDetailsValid(
+    const sync_pb::SharedTabGroupAccountDataSpecifics& specifics) {
+  if (!specifics.has_shared_tab_details()) {
+    // Non-tab account specifics should be handled here.
+    return false;
+  }
+
+  const sync_pb::SharedTabDetails& tab_details = specifics.shared_tab_details();
+  if (!base::Uuid::ParseCaseInsensitive(tab_details.shared_tab_group_guid())
+           .is_valid() ||
+      !tab_details.has_last_seen_timestamp_windows_epoch()) {
+    return false;
+  }
+
+  return true;
+}
+
+bool IsTabGroupDetailsValid(
+    const sync_pb::SharedTabGroupAccountDataSpecifics& specifics) {
+  return specifics.has_shared_tab_group_details();
+}
+
 }  // namespace
 
 SharedTabGroupAccountDataSyncBridge::SharedTabGroupAccountDataSyncBridge(
@@ -182,6 +227,8 @@
         // find the specifics in-memory.
         if (specifics.has_shared_tab_details()) {
           UpdateTabDetailsModel(specifics);
+        } else if (specifics.has_shared_tab_group_details()) {
+          UpdateTabGroupDetailsModel(specifics);
         }
 
         break;
@@ -276,19 +323,7 @@
     return false;
   }
 
-  if (!specifics.has_shared_tab_details()) {
-    // Non-tab account specifics should be handled here.
-    return false;
-  }
-
-  const sync_pb::SharedTabDetails& tab_details = specifics.shared_tab_details();
-  if (!base::Uuid::ParseCaseInsensitive(tab_details.shared_tab_group_guid())
-           .is_valid() ||
-      !tab_details.has_last_seen_timestamp_windows_epoch()) {
-    return false;
-  }
-
-  return true;
+  return IsTabDetailsValid(specifics) || IsTabGroupDetailsValid(specifics);
 }
 
 sync_pb::EntitySpecifics
@@ -354,6 +389,7 @@
 void SharedTabGroupAccountDataSyncBridge::SavedTabGroupModelLoaded() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   ApplyMissingTabData();
+  ApplyMissingTabGroupData();
 }
 
 void SharedTabGroupAccountDataSyncBridge::SavedTabGroupTabLastSeenTimeUpdated(
@@ -417,32 +453,42 @@
 
   const SavedTabGroup* group = model_->Get(group_guid);
   CHECK(group);
-  if (!group->is_shared_tab_group() || !tab_guid) {
+  if (!group->is_shared_tab_group()) {
     return;
   }
 
-  const SavedTabGroupTab* tab = group->GetTab(tab_guid.value());
-  if (tab) {
-    return;
+  if (tab_guid) {
+    MaybeRemoveTabDetailsOnGroupUpdate(*group, tab_guid);
+  } else {
+    // Handle shared tab group details.
+    WriteTabGroupDetailToSyncIfPositionChanged(*group);
   }
-
-  // This is an update for a shared tab deletion from local. Remove the
-  // corresponding entity from sync.
-  const std::string storage_key = CreateClientTagForSharedTab(
-      group->collaboration_id().value(), tab_guid.value());
-  RemoveEntitySpecifics(storage_key);
 }
 
 void SharedTabGroupAccountDataSyncBridge::SavedTabGroupUpdatedFromSync(
     const base::Uuid& group_guid,
     const std::optional<base::Uuid>& tab_guid) {
-  // Regardless of update source, we need to detect tab deletions and clean them
-  // up from sync.
-  SavedTabGroupUpdatedLocally(group_guid, tab_guid);
+  if (!is_initialized_) {
+    // Ignore any changes before the model is successfully initialized.
+    return;
+  }
+
+  const SavedTabGroup* group = model_->Get(group_guid);
+  CHECK(group);
+  if (!group->is_shared_tab_group()) {
+    return;
+  }
+
+  MaybeRemoveTabDetailsOnGroupUpdate(*group, tab_guid);
 }
 
 void SharedTabGroupAccountDataSyncBridge::SavedTabGroupRemovedLocally(
     const SavedTabGroup& removed_group) {
+  if (!is_initialized_) {
+    // Ignore any changes before the model is successfully initialized.
+    return;
+  }
+
   if (!removed_group.is_shared_tab_group()) {
     return;
   }
@@ -451,6 +497,9 @@
   for (const SavedTabGroupTab& tab : removed_group.saved_tabs()) {
     RemoveEntitySpecifics(CreateClientTagForSharedTab(removed_group, tab));
   }
+
+  // Remove tab group details entity.
+  RemoveEntitySpecifics(CreateClientTagForSharedGroup(removed_group));
 }
 
 void SharedTabGroupAccountDataSyncBridge::SavedTabGroupRemovedFromSync(
@@ -458,6 +507,54 @@
   SavedTabGroupRemovedLocally(removed_group);
 }
 
+void SharedTabGroupAccountDataSyncBridge::SavedTabGroupAddedLocally(
+    const base::Uuid& guid) {
+  if (!is_initialized_) {
+    // Ignore any changes before the model is successfully initialized.
+    return;
+  }
+
+  const SavedTabGroup* group = model_->Get(guid);
+
+  if (!group || !group->is_shared_tab_group()) {
+    return;
+  }
+
+  WriteTabGroupDetailToSyncIfPositionChanged(*group);
+}
+
+void SharedTabGroupAccountDataSyncBridge::SavedTabGroupAddedFromSync(
+    const base::Uuid& guid) {
+  if (!is_initialized_) {
+    // Ignore any changes before the model is successfully initialized.
+    return;
+  }
+
+  const SavedTabGroup* group = model_->Get(guid);
+
+  if (!group || !group->is_shared_tab_group()) {
+    return;
+  }
+
+  std::string client_tag = CreateClientTagForSharedGroup(*group);
+  std::optional<sync_pb::SharedTabGroupAccountDataSpecifics> specifics =
+      GetSpecificsForStorageKey(client_tag);
+  if (specifics.has_value()) {
+    UpdateTabGroupDetailsModel(specifics.value());
+  }
+}
+
+void SharedTabGroupAccountDataSyncBridge::SavedTabGroupReorderedLocally() {
+  if (!is_initialized_) {
+    // Ignore any changes before the model is successfully initialized.
+    return;
+  }
+
+  for (const SavedTabGroup* group : model_->GetSharedTabGroupsOnly()) {
+    WriteTabGroupDetailToSyncIfPositionChanged(*group);
+  }
+}
+
 bool SharedTabGroupAccountDataSyncBridge::IsInitialized() const {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   return is_initialized_;
@@ -516,9 +613,12 @@
       // GUID is used as a storage key, so it should always match.
       continue;
     }
+
     specifics_[storage_key] = specifics;
     if (specifics.has_shared_tab_details()) {
       UpdateTabDetailsModel(specifics);
+    } else if (specifics.has_shared_tab_group_details()) {
+      UpdateTabGroupDetailsModel(specifics);
     }
   }
 
@@ -565,6 +665,29 @@
   storage_keys_for_missing_tabs_.erase(storage_key);
 }
 
+void SharedTabGroupAccountDataSyncBridge::UpdateTabGroupDetailsModel(
+    const sync_pb::SharedTabGroupAccountDataSpecifics& specifics) {
+  CHECK(specifics.has_shared_tab_group_details());
+
+  const std::string storage_key = GetClientTagFromSpecifics(specifics);
+  const base::Uuid tab_group_id =
+      base::Uuid::ParseCaseInsensitive(specifics.guid());
+  const SavedTabGroup* group = model_->Get(tab_group_id);
+  if (!group) {
+    storage_keys_for_missing_tab_groups_.insert(storage_key);
+    return;
+  }
+
+  // If the group is in the model, update its position based on the specifics.
+  std::optional<size_t> position;
+  if (specifics.shared_tab_group_details().has_pinned_position()) {
+    position = specifics.shared_tab_group_details().pinned_position();
+  }
+  model_->UpdatePositionForSharedGroupFromSync(tab_group_id, position);
+
+  storage_keys_for_missing_tab_groups_.erase(storage_key);
+}
+
 void SharedTabGroupAccountDataSyncBridge::ApplyMissingTabData() {
   // Find previously missing tabs have now been added. Create a copy of
   // the strings from `storage_keys_for_missing_tabs_` so this set can
@@ -585,6 +708,28 @@
   }
 }
 
+void SharedTabGroupAccountDataSyncBridge::ApplyMissingTabGroupData() {
+  // Find previously missing tab groups have now been added. Create a copy of
+  // the strings from `storage_keys_for_missing_tab_groups_` so this set can
+  // be mutated in `UpdateTabGroupDetailsModel`.
+  std::vector<std::string> groups_in_model;
+  groups_in_model.reserve(storage_keys_for_missing_tab_groups_.size());
+
+  for (const std::string& storage_key : storage_keys_for_missing_tab_groups_) {
+    const sync_pb::SharedTabGroupAccountDataSpecifics& specifics =
+        specifics_.at(storage_key);
+    const base::Uuid tab_group_id =
+        base::Uuid::ParseCaseInsensitive(specifics.guid());
+    if (model_->Get(tab_group_id)) {
+      groups_in_model.emplace_back(storage_key);
+    }
+  }
+
+  for (const std::string& storage_key : groups_in_model) {
+    UpdateTabGroupDetailsModel(specifics_.at(storage_key));
+  }
+}
+
 void SharedTabGroupAccountDataSyncBridge::WriteEntityToSync(
     std::unique_ptr<syncer::EntityData> entity) {
   std::unique_ptr<syncer::DataTypeStore::WriteBatch> batch =
@@ -598,7 +743,10 @@
   if (specifics.has_shared_tab_details()) {
     // For tab details, remove them from the missing tabs.
     storage_keys_for_missing_tabs_.erase(storage_key);
+  } else if (specifics.has_shared_tab_group_details()) {
+    storage_keys_for_missing_tab_groups_.erase(storage_key);
   }
+
   batch->WriteData(storage_key, specifics.SerializeAsString());
 
   change_processor()->Put(storage_key, std::move(entity),
@@ -664,4 +812,49 @@
   return CreateEntityDataFromSpecifics(specifics);
 }
 
+void SharedTabGroupAccountDataSyncBridge::MaybeRemoveTabDetailsOnGroupUpdate(
+    const SavedTabGroup& group,
+    const std::optional<base::Uuid>& tab_guid) {
+  if (!tab_guid) {
+    return;
+  }
+
+  const SavedTabGroupTab* tab = group.GetTab(tab_guid.value());
+  if (tab) {
+    return;
+  }
+
+  // This is an update for a shared tab deletion from local. Remove the
+  // corresponding entity from sync.
+  const std::string storage_key = CreateClientTagForSharedTab(
+      group.collaboration_id().value(), tab_guid.value());
+  RemoveEntitySpecifics(storage_key);
+}
+
+void SharedTabGroupAccountDataSyncBridge::
+    WriteTabGroupDetailToSyncIfPositionChanged(const SavedTabGroup& group) {
+  std::string client_tag = CreateClientTagForSharedGroup(group);
+  std::optional<sync_pb::SharedTabGroupAccountDataSpecifics> specifics =
+      GetSpecificsForStorageKey(client_tag);
+  bool has_changed = false;
+  if (specifics.has_value()) {
+    std::optional<size_t> specifics_pinned_position;
+    if (specifics->has_shared_tab_group_details()) {
+      if (specifics->shared_tab_group_details().has_pinned_position()) {
+        specifics_pinned_position =
+            specifics->shared_tab_group_details().pinned_position();
+      }
+    }
+    if (group.position() != specifics_pinned_position) {
+      has_changed = true;
+    }
+  } else {
+    has_changed = true;
+  }
+
+  if (has_changed) {
+    WriteEntityToSync(CreateEntityDataFromSharedTabGroup(*model_, group));
+  }
+}
+
 }  // namespace tab_groups
diff --git a/components/saved_tab_groups/internal/shared_tab_group_account_data_sync_bridge.h b/components/saved_tab_groups/internal/shared_tab_group_account_data_sync_bridge.h
index 645ff11a..d1da9dc 100644
--- a/components/saved_tab_groups/internal/shared_tab_group_account_data_sync_bridge.h
+++ b/components/saved_tab_groups/internal/shared_tab_group_account_data_sync_bridge.h
@@ -68,6 +68,8 @@
   void SavedTabGroupModelLoaded() override;
   void SavedTabGroupTabLastSeenTimeUpdated(const base::Uuid& saved_tab_id,
                                            TriggerSource source) override;
+  void SavedTabGroupAddedLocally(const base::Uuid& guid) override;
+  void SavedTabGroupAddedFromSync(const base::Uuid& guid) override;
   void SavedTabGroupUpdatedLocally(
       const base::Uuid& group_guid,
       const std::optional<base::Uuid>& tab_guid) override;
@@ -77,6 +79,7 @@
   void SavedTabGroupRemovedLocally(const SavedTabGroup& removed_group) override;
   void SavedTabGroupRemovedFromSync(
       const SavedTabGroup& removed_group) override;
+  void SavedTabGroupReorderedLocally() override;
 
   // Returns whether the sync bridge has initialized by reading data
   // from the on-disk store.
@@ -107,10 +110,17 @@
   void UpdateTabDetailsModel(
       const sync_pb::SharedTabGroupAccountDataSpecifics& specifics);
 
+  void UpdateTabGroupDetailsModel(
+      const sync_pb::SharedTabGroupAccountDataSpecifics& specifics);
+
   // Look for tabs specified in `storage_keys_for_missing_tabs_` and
   // apply their corresponding model updates.
   void ApplyMissingTabData();
 
+  // Look for tab groups specified in `storage_keys_for_missing_tab_groups_` and
+  // apply their corresponding model updates.
+  void ApplyMissingTabGroupData();
+
   // Write a new entity to sync. This is used when the model is updated
   // with a new value and sync needs to be triggered.
   void WriteEntityToSync(std::unique_ptr<syncer::EntityData> entity);
@@ -126,6 +136,14 @@
       const SavedTabGroupModel& model,
       const SavedTabGroupTab& tab);
 
+  // Remove tab details on tab group update locally or from sync if available.
+  void MaybeRemoveTabDetailsOnGroupUpdate(
+      const SavedTabGroup& group,
+      const std::optional<base::Uuid>& tab_guid);
+
+  // Write tab group detail to sync only if the tab group details has changed.
+  void WriteTabGroupDetailToSyncIfPositionChanged(const SavedTabGroup& group);
+
   SEQUENCE_CHECKER(sequence_checker_);
 
   // In charge of actually persisting changes to disk, or loading previous data.
@@ -146,6 +164,8 @@
   // still stored in sync bridge cache as well as written to disk.
   std::set<std::string> storage_keys_for_missing_tabs_;
 
+  std::set<std::string> storage_keys_for_missing_tab_groups_;
+
   // Observes the SavedTabGroupModel.
   base::ScopedObservation<SavedTabGroupModel, SavedTabGroupModelObserver>
       saved_tab_group_model_observation_{this};
diff --git a/components/saved_tab_groups/internal/shared_tab_group_account_data_sync_bridge_unittest.cc b/components/saved_tab_groups/internal/shared_tab_group_account_data_sync_bridge_unittest.cc
index 792fe667..a9829a9 100644
--- a/components/saved_tab_groups/internal/shared_tab_group_account_data_sync_bridge_unittest.cc
+++ b/components/saved_tab_groups/internal/shared_tab_group_account_data_sync_bridge_unittest.cc
@@ -72,6 +72,11 @@
   return tab_guid.AsLowercaseString() + "|" + collaboration_id.value();
 }
 
+std::string CreateClientTagForSharedGroup(const SavedTabGroup& group) {
+  return group.saved_guid().AsLowercaseString() + "|" +
+         group.collaboration_id().value().value();
+}
+
 sync_pb::SharedTabGroupAccountDataSpecifics CreateTabGroupAccountSpecifics(
     const CollaborationId& collaboration_id,
     const SavedTabGroupTab& tab,
@@ -120,6 +125,21 @@
   return extended_specifics.extra_field_for_testing();
 }
 
+sync_pb::SharedTabGroupAccountDataSpecifics
+CreateTabGroupAccountSpecificsForGroup(const CollaborationId& collaboration_id,
+                                       const SavedTabGroup& group,
+                                       std::optional<size_t> position) {
+  sync_pb::SharedTabGroupAccountDataSpecifics specifics;
+  specifics.set_guid(group.saved_guid().AsLowercaseString());
+  specifics.set_collaboration_id(collaboration_id.value());
+  sync_pb::SharedTabGroupDetails* shared_tab_group_details =
+      specifics.mutable_shared_tab_group_details();
+  if (position.has_value()) {
+    shared_tab_group_details->set_pinned_position(position.value());
+  }
+  return specifics;
+}
+
 syncer::EntityData CreateEntityData(
     const sync_pb::SharedTabGroupAccountDataSpecifics& specifics,
     base::Time creation_time = base::Time::Now()) {
@@ -229,30 +249,43 @@
     return group;
   }
 
-  size_t GetNumEntriesInStore(bool is_tab_details) {
-    std::map<std::string, sync_pb::SharedTabGroupAccountDataSpecifics> data =
-        syncer::DataTypeStoreTestUtil::ReadAllDataAsProtoAndWait<
-            sync_pb::SharedTabGroupAccountDataSpecifics>(*store_);
+  std::map<std::string, sync_pb::SharedTabGroupAccountDataSpecifics>
+  GetEntriesInStore(bool is_tab_details) {
+    std::unique_ptr<syncer::DataTypeStore::RecordList> entries;
+    base::RunLoop run_loop;
+    store_->ReadAllData(base::BindLambdaForTesting(
+        [&run_loop, &entries](
+            const std::optional<syncer::ModelError>& error,
+            std::unique_ptr<syncer::DataTypeStore::RecordList> data) {
+          entries = std::move(data);
+          run_loop.Quit();
+        }));
+    run_loop.Run();
 
-    size_t size = 0;
-    for (const auto& [storage_key, specifics] : data) {
+    std::map<std::string, sync_pb::SharedTabGroupAccountDataSpecifics> result;
+    for (const auto& record : *entries) {
+      sync_pb::SharedTabGroupAccountDataSpecifics specifics;
+      if (!specifics.ParseFromString(record.value)) {
+        CHECK(false);
+      }
+
       if (is_tab_details && specifics.has_shared_tab_details()) {
-        ++size;
+        result[record.id] = specifics;
       }
       if (!is_tab_details && specifics.has_shared_tab_group_details()) {
-        ++size;
+        result[record.id] = specifics;
       }
     }
 
-    return size;
+    return result;
   }
 
   size_t GetNumTabDetailsInStore() {
-    return GetNumEntriesInStore(/*is_tab_details=*/true);
+    return GetEntriesInStore(/*is_tab_details=*/true).size();
   }
 
   size_t GetNumTabGroupDetailsInStore() {
-    return GetNumEntriesInStore(/*is_tab_details=*/false);
+    return GetEntriesInStore(/*is_tab_details=*/false).size();
   }
 
   // Cleans up the bridge and the model, used to simulate browser restart.
@@ -331,6 +364,9 @@
       CreateTabGroupAccountSpecifics(kCollaborationId, group.saved_tabs().at(0),
                                      last_seen_time),
       base::Time::Now())));
+
+  EXPECT_TRUE(bridge().IsEntityDataValid(CreateEntityData(
+      CreateTabGroupAccountSpecificsForGroup(kCollaborationId, group, 0))));
 }
 
 TEST_F(SharedTabGroupAccountDataSyncBridgeTest, ShouldResolveConflicts) {
@@ -669,6 +705,8 @@
   const base::Uuid& tab_id2 = created_tab2.saved_tab_guid();
 
   InitializeBridgeAndModel();
+  const std::string storage_key = CreateClientTagForSharedGroup(created_group);
+  EXPECT_CALL(mock_processor(), Put(Eq(storage_key), _, _)).Times(1);
   model().AddedLocally(created_group);
 
   EXPECT_EQ(model().Count(), 1);
@@ -854,6 +892,8 @@
   const base::Uuid& tab_id2 = created_group.saved_tabs()[1].saved_tab_guid();
 
   InitializeBridgeAndModel();
+  const std::string storage_key = CreateClientTagForSharedGroup(created_group);
+  EXPECT_CALL(mock_processor(), Put(Eq(storage_key), _, _)).Times(1);
   model().AddedLocally(created_group);
 
   ASSERT_EQ(model().Count(), 1);
@@ -879,10 +919,12 @@
   // should be deleted.
   EXPECT_CALL(mock_processor(), Delete(Eq(storage_key1), _, _)).Times(1);
   EXPECT_CALL(mock_processor(), Delete(Eq(storage_key2), _, _)).Times(1);
+  EXPECT_CALL(mock_processor(), Delete(Eq(storage_key), _, _)).Times(1);
   model().RemovedLocally(group_id);
   EXPECT_EQ(GetNumTabDetailsInStore(), 0u);
   EXPECT_FALSE(bridge().GetSpecificsForStorageKey(storage_key1));
   EXPECT_FALSE(bridge().GetSpecificsForStorageKey(storage_key2));
+  EXPECT_FALSE(bridge().GetSpecificsForStorageKey(storage_key));
 }
 
 TEST_F(SharedTabGroupAccountDataSyncBridgeTest,
@@ -895,6 +937,8 @@
   const base::Uuid& tab_id2 = created_group.saved_tabs()[1].saved_tab_guid();
 
   InitializeBridgeAndModel();
+  const std::string storage_key = CreateClientTagForSharedGroup(created_group);
+  EXPECT_CALL(mock_processor(), Put(Eq(storage_key), _, _)).Times(1);
   model().AddedLocally(created_group);
 
   ASSERT_EQ(model().Count(), 1);
@@ -920,10 +964,12 @@
   // tabs should be deleted.
   EXPECT_CALL(mock_processor(), Delete(Eq(storage_key1), _, _)).Times(1);
   EXPECT_CALL(mock_processor(), Delete(Eq(storage_key2), _, _)).Times(1);
+  EXPECT_CALL(mock_processor(), Delete(Eq(storage_key), _, _)).Times(1);
   model().RemovedFromSync(group_id);
   EXPECT_EQ(GetNumTabDetailsInStore(), 0u);
   EXPECT_FALSE(bridge().GetSpecificsForStorageKey(storage_key1));
   EXPECT_FALSE(bridge().GetSpecificsForStorageKey(storage_key2));
+  EXPECT_FALSE(bridge().GetSpecificsForStorageKey(storage_key));
 }
 
 TEST_F(SharedTabGroupAccountDataSyncBridgeTest,
@@ -1076,5 +1122,195 @@
             "extra_field_for_testing");
 }
 
+TEST_F(SharedTabGroupAccountDataSyncBridgeTest,
+       IncrementalUpdateShouldSetPosition) {
+  const CollaborationId kCollaborationId("collaboration");
+  const SavedTabGroup created_group = CreateGroupWithLocalIds(kCollaborationId);
+  const base::Uuid& created_group_id = created_group.saved_guid();
+
+  // Add group locally.
+  InitializeBridgeAndModel();
+  model().AddedLocally(created_group);
+
+  // Receive update from sync.
+  syncer::EntityChangeList change_list;
+  std::optional<size_t> position = 5;
+  change_list.push_back(
+      CreateAddEntityChange(CreateTabGroupAccountSpecificsForGroup(
+          kCollaborationId, created_group, position)));
+  EXPECT_EQ(GetNumTabGroupDetailsInStore(), 1u);
+  bridge().ApplyIncrementalSyncChanges(bridge().CreateMetadataChangeList(),
+                                       std::move(change_list));
+
+  // Verify position is set correctly.
+  const SavedTabGroup* group = model().Get(created_group_id);
+  EXPECT_EQ(position, group->position());
+  EXPECT_EQ(GetNumTabGroupDetailsInStore(), 1u);
+}
+
+TEST_F(SharedTabGroupAccountDataSyncBridgeTest,
+       TabGroupAddedFromSyncShouldSetPosition) {
+  const CollaborationId kCollaborationId("collaboration");
+  const SavedTabGroup created_group = CreateGroupWithLocalIds(kCollaborationId);
+  const base::Uuid& created_group_id = created_group.saved_guid();
+
+  InitializeBridgeAndModel();
+
+  // Receive update from sync.
+  syncer::EntityChangeList change_list;
+  std::optional<size_t> position = 5;
+  change_list.push_back(
+      CreateAddEntityChange(CreateTabGroupAccountSpecificsForGroup(
+          kCollaborationId, created_group, position)));
+  bridge().ApplyIncrementalSyncChanges(bridge().CreateMetadataChangeList(),
+                                       std::move(change_list));
+  EXPECT_EQ(GetNumTabGroupDetailsInStore(), 1u);
+
+  // Add group from sync.
+  model().AddedFromSync(created_group);
+
+  // Verify position is set correctly.
+  const SavedTabGroup* group = model().Get(created_group_id);
+  EXPECT_EQ(position, group->position());
+  EXPECT_EQ(GetNumTabGroupDetailsInStore(), 1u);
+}
+
+TEST_F(SharedTabGroupAccountDataSyncBridgeTest,
+       TabGroupAddedLocallyShouldSavePosition) {
+  const CollaborationId kCollaborationId("collaboration");
+  const int kPosition = 5;
+  SavedTabGroup created_group = CreateGroupWithLocalIds(kCollaborationId);
+  created_group.SetPosition(kPosition);
+  std::string client_tag = CreateClientTagForSharedGroup(created_group);
+
+  InitializeBridgeAndModel();
+  model().AddedLocally(created_group);
+
+  // Verify the position is updated.
+  EXPECT_EQ(GetNumTabGroupDetailsInStore(), 1u);
+  auto entries = GetEntriesInStore(/*is_tab_details=*/false);
+  ASSERT_TRUE(entries.contains(client_tag));
+  ASSERT_EQ(kPosition,
+            entries[client_tag].shared_tab_group_details().pinned_position());
+}
+
+TEST_F(SharedTabGroupAccountDataSyncBridgeTest,
+       TabGroupTogglePinStateShouldSavePosition) {
+  const CollaborationId kCollaborationId("collaboration");
+  SavedTabGroup created_group = CreateGroupWithLocalIds(kCollaborationId);
+  base::Uuid guid = created_group.saved_guid();
+  std::string client_tag = CreateClientTagForSharedGroup(created_group);
+
+  InitializeBridgeAndModel();
+  model().AddedLocally(created_group);
+
+  // Verify unpinned position.
+  EXPECT_EQ(GetNumTabGroupDetailsInStore(), 1u);
+  auto entries = GetEntriesInStore(/*is_tab_details=*/false);
+  ASSERT_TRUE(entries.contains(client_tag));
+  ASSERT_FALSE(
+      entries[client_tag].shared_tab_group_details().has_pinned_position());
+
+  // Pin the tab group.
+  model().TogglePinState(guid);
+
+  // Verify pinned position.
+  entries = GetEntriesInStore(/*is_tab_details=*/false);
+  ASSERT_TRUE(entries.contains(client_tag));
+  ASSERT_EQ(0,
+            entries[client_tag].shared_tab_group_details().pinned_position());
+}
+
+TEST_F(SharedTabGroupAccountDataSyncBridgeTest,
+       TabGroupRemovedLocallyShouldRemovePosition) {
+  const CollaborationId kCollaborationId("collaboration");
+  SavedTabGroup created_group = CreateGroupWithLocalIds(kCollaborationId);
+  base::Uuid guid = created_group.saved_guid();
+  std::string client_tag = CreateClientTagForSharedGroup(created_group);
+
+  InitializeBridgeAndModel();
+  model().AddedLocally(created_group);
+
+  EXPECT_EQ(GetNumTabGroupDetailsInStore(), 1u);
+
+  model().RemovedLocally(guid);
+
+  EXPECT_EQ(GetNumTabGroupDetailsInStore(), 0u);
+}
+
+TEST_F(SharedTabGroupAccountDataSyncBridgeTest,
+       TabGroupRemovedFromSyncShouldRemovePosition) {
+  const CollaborationId kCollaborationId("collaboration");
+  SavedTabGroup created_group = CreateGroupWithLocalIds(kCollaborationId);
+  base::Uuid guid = created_group.saved_guid();
+  std::string client_tag = CreateClientTagForSharedGroup(created_group);
+
+  InitializeBridgeAndModel();
+  model().AddedLocally(created_group);
+
+  EXPECT_EQ(GetNumTabGroupDetailsInStore(), 1u);
+
+  model().RemovedFromSync(guid);
+
+  EXPECT_EQ(GetNumTabGroupDetailsInStore(), 0u);
+}
+
+TEST_F(SharedTabGroupAccountDataSyncBridgeTest,
+       TabGroupReorderLocallyShouldSavePosition) {
+  const CollaborationId kCollaborationId1("collaboration1");
+  SavedTabGroup created_group1 = CreateGroupWithLocalIds(kCollaborationId1);
+  created_group1.SetPosition(0);
+  base::Uuid guid1 = created_group1.saved_guid();
+  std::string client_tag1 = CreateClientTagForSharedGroup(created_group1);
+
+  const CollaborationId kCollaborationId2("collaboration2");
+  SavedTabGroup created_group2 = CreateGroupWithLocalIds(kCollaborationId2);
+  created_group2.SetPosition(1);
+  std::string client_tag2 = CreateClientTagForSharedGroup(created_group2);
+
+  // Add 2 groups.
+  InitializeBridgeAndModel();
+  model().AddedLocally(created_group1);
+  model().AddedLocally(created_group2);
+
+  // Verify initial positions.
+  EXPECT_EQ(GetNumTabGroupDetailsInStore(), 2u);
+  auto entries = GetEntriesInStore(/*is_tab_details=*/false);
+  ASSERT_TRUE(entries.contains(client_tag1));
+  ASSERT_EQ(0,
+            entries[client_tag1].shared_tab_group_details().pinned_position());
+  ASSERT_TRUE(entries.contains(client_tag2));
+  ASSERT_EQ(1,
+            entries[client_tag2].shared_tab_group_details().pinned_position());
+
+  // Reorder group.
+  model().ReorderGroupLocally(guid1, 1);
+
+  // Verify modified positions.
+  EXPECT_EQ(GetNumTabGroupDetailsInStore(), 2u);
+  entries = GetEntriesInStore(/*is_tab_details=*/false);
+  ASSERT_TRUE(entries.contains(client_tag1));
+  ASSERT_EQ(1,
+            entries[client_tag1].shared_tab_group_details().pinned_position());
+  ASSERT_TRUE(entries.contains(client_tag2));
+  ASSERT_EQ(0,
+            entries[client_tag2].shared_tab_group_details().pinned_position());
+}
+
+TEST_F(SharedTabGroupAccountDataSyncBridgeTest,
+       TabGroupShouldOnlySaveIfPositionChanged) {
+  const CollaborationId kCollaborationId("collaboration");
+  SavedTabGroup created_group = CreateGroupWithLocalIds(kCollaborationId);
+  base::Uuid guid = created_group.saved_guid();
+  std::string client_tag = CreateClientTagForSharedGroup(created_group);
+
+  InitializeBridgeAndModel();
+
+  EXPECT_CALL(mock_processor(), Put(Eq(client_tag), _, _)).Times(1);
+  model().AddedLocally(created_group);
+  model().UpdateArchivalStatus(guid, true);
+  model().UpdateArchivalStatus(guid, false);
+}
+
 }  // namespace
 }  // namespace tab_groups
diff --git a/components/saved_tab_groups/public/saved_tab_group.cc b/components/saved_tab_groups/public/saved_tab_group.cc
index a6d97c2..762ebbd 100644
--- a/components/saved_tab_groups/public/saved_tab_group.cc
+++ b/components/saved_tab_groups/public/saved_tab_group.cc
@@ -429,10 +429,15 @@
     base::Time update_time) {
   SetTitle(title);
   SetColor(color);
-  if (position.has_value()) {
-    SetPosition(position.value());
-  } else {
-    SetPinned(false);
+
+  // Do not merge position for shared tab group since the position is saved from
+  // elsewhere.
+  if (!is_shared_tab_group()) {
+    if (position.has_value()) {
+      SetPosition(position.value());
+    } else {
+      SetPinned(false);
+    }
   }
 
   SetCreatorCacheGuid(creator_cache_guid);
diff --git a/components/saved_tab_groups/public/saved_tab_group_unittest.cc b/components/saved_tab_groups/public/saved_tab_group_unittest.cc
index 800e56b2..a9b6d9c 100644
--- a/components/saved_tab_groups/public/saved_tab_group_unittest.cc
+++ b/components/saved_tab_groups/public/saved_tab_group_unittest.cc
@@ -378,4 +378,24 @@
             kOriginatingTabGroupGuid);
 }
 
+TEST(SavedTabGroupTest, MergeRemoteGroupPosition) {
+  std::u16string title = u"title";
+  tab_groups::TabGroupColorId color = tab_groups::TabGroupColorId::kBlue;
+  std::optional<size_t> position = 0;
+
+  // Saved group should merge position.
+  SavedTabGroup saved_group = CreateDefaultEmptySavedTabGroup();
+  saved_group.MergeRemoteGroupMetadata(title, color, position, std::nullopt,
+                                       std::nullopt, base::Time::Now());
+  EXPECT_EQ(position, saved_group.position());
+
+  // Shared group should not merge position.
+  SavedTabGroup shared_group =
+      CreateDefaultEmptySavedTabGroup().CloneAsSharedTabGroup(
+          CollaborationId("collaboration"));
+  shared_group.MergeRemoteGroupMetadata(title, color, position, std::nullopt,
+                                        std::nullopt, base::Time::Now());
+  EXPECT_EQ(std::nullopt, shared_group.position());
+}
+
 }  // namespace tab_groups
diff --git a/components/services/storage/dom_storage/local_storage_impl.cc b/components/services/storage/dom_storage/local_storage_impl.cc
index 0c14486..9c8e490 100644
--- a/components/services/storage/dom_storage/local_storage_impl.cc
+++ b/components/services/storage/dom_storage/local_storage_impl.cc
@@ -603,6 +603,8 @@
 
 LocalStorageImpl::~LocalStorageImpl() {
   DCHECK_EQ(connection_state_, CONNECTION_SHUTDOWN);
+  // ShutDown() should run before this destructor and clear `areas_`.
+  CHECK(areas_.empty());
   base::trace_event::MemoryDumpManager::GetInstance()->UnregisterDumpProvider(
       this);
 }
diff --git a/components/services/storage/partition_impl.cc b/components/services/storage/partition_impl.cc
index a30aff5..15e641a 100644
--- a/components/services/storage/partition_impl.cc
+++ b/components/services/storage/partition_impl.cc
@@ -7,6 +7,7 @@
 #include <memory>
 #include <utility>
 
+#include "base/debug/crash_logging.h"
 #include "base/functional/bind.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/task/thread_pool.h"
@@ -90,6 +91,18 @@
       base::SequencedTaskRunner::GetCurrentDefault(), std::move(receiver));
 }
 
+#if BUILDFLAG(IS_MAC)
+void PartitionImpl::BindLocalStorageControlAndReportLifecycle(
+    mojom::LocalStorageLifecycle lifecycle,
+    mojo::PendingReceiver<mojom::LocalStorageControl> receiver) {
+  SCOPED_CRASH_KEY_NUMBER("396030877", "local_storage_lifecycle",
+                          static_cast<int>(lifecycle));
+  local_storage_ = std::make_unique<LocalStorageImpl>(
+      path_.value_or(base::FilePath()),
+      base::SequencedTaskRunner::GetCurrentDefault(), std::move(receiver));
+}
+#endif  // BUILDFLAG(IS_MAC)
+
 void PartitionImpl::OnDisconnect() {
   if (receivers_.empty()) {
     // Deletes |this|.
diff --git a/components/services/storage/partition_impl.h b/components/services/storage/partition_impl.h
index 6f3443e2..ba21d6f 100644
--- a/components/services/storage/partition_impl.h
+++ b/components/services/storage/partition_impl.h
@@ -48,6 +48,11 @@
       mojo::PendingReceiver<mojom::SessionStorageControl> receiver) override;
   void BindLocalStorageControl(
       mojo::PendingReceiver<mojom::LocalStorageControl> receiver) override;
+#if BUILDFLAG(IS_MAC)
+  void BindLocalStorageControlAndReportLifecycle(
+      mojom::LocalStorageLifecycle lifecycle,
+      mojo::PendingReceiver<mojom::LocalStorageControl> receiver) override;
+#endif  // BUILDFLAG(IS_MAC)
 
  private:
   void OnDisconnect();
diff --git a/components/services/storage/public/mojom/partition.mojom b/components/services/storage/public/mojom/partition.mojom
index f6d650f..0959952 100644
--- a/components/services/storage/public/mojom/partition.mojom
+++ b/components/services/storage/public/mojom/partition.mojom
@@ -7,12 +7,33 @@
 import "components/services/storage/public/mojom/local_storage_control.mojom";
 import "components/services/storage/public/mojom/session_storage_control.mojom";
 
+// Mac only enum used as a crash key to help investigate Storage process
+// crashes.
+// TODO(crbug.com/396030877): Fix the bug and remove this enum.
+[EnableIf=is_mac]
+enum LocalStorageLifecycle {
+  kInitializing,
+  kInitializingWithUnboundStorageService,
+  kRecovering,
+  kRecoveringWithUnboundStorageService,
+};
+
 // Partition controls an isolated storage partition owned by the Storage
 // Service. This is analogous to the browser's own storage partition concept.
 interface Partition {
   // Binds the main control interface for Session Storage in this partition.
-  BindSessionStorageControl(pending_receiver<SessionStorageControl> receiver);
+  BindSessionStorageControl(
+      pending_receiver<SessionStorageControl> receiver);
 
   // Binds the main control interface for Local Storage in this partition.
   BindLocalStorageControl(pending_receiver<LocalStorageControl> receiver);
+
+  // Similar to the above method but with a `lifecycle` param. This is included
+  // as a crash key to help root cause a Storage process crash in LocalStorage
+  // code on macOS.
+  // TODO(crbug.com/396030877): Fix the bug and remove this param.
+  [EnableIf=is_mac]
+  BindLocalStorageControlAndReportLifecycle(
+      LocalStorageLifecycle lifecycle,
+      pending_receiver<LocalStorageControl> receiver);
 };
diff --git a/components/viz/common/quads/selection.h b/components/viz/common/quads/selection.h
index b0ff04122..3713462d 100644
--- a/components/viz/common/quads/selection.h
+++ b/components/viz/common/quads/selection.h
@@ -5,8 +5,6 @@
 #ifndef COMPONENTS_VIZ_COMMON_QUADS_SELECTION_H_
 #define COMPONENTS_VIZ_COMMON_QUADS_SELECTION_H_
 
-#include "base/strings/stringprintf.h"
-
 namespace viz {
 
 template <typename BoundType>
@@ -14,12 +12,8 @@
   Selection() = default;
   ~Selection() = default;
 
-  BoundType start, end;
-
-  std::string ToString() const {
-    return base::StringPrintf("Selection(%s, %s)", start.ToString().c_str(),
-                              end.ToString().c_str());
-  }
+  BoundType start;
+  BoundType end;
 
   friend bool operator==(const Selection<BoundType>&,
                          const Selection<BoundType>&) = default;
diff --git a/components/webxr/OWNERS b/components/webxr/OWNERS
index 3bd7798..fba6307f 100644
--- a/components/webxr/OWNERS
+++ b/components/webxr/OWNERS
@@ -1,6 +1,5 @@
 alcooper@chromium.org
 bajones@chromium.org
-bialpio@chromium.org
 
 # WebXR Test related
 bsheedy@chromium.org
diff --git a/content/browser/accessibility/browser_accessibility_state_impl_win.cc b/content/browser/accessibility/browser_accessibility_state_impl_win.cc
index c2ecee72..1b69200 100644
--- a/content/browser/accessibility/browser_accessibility_state_impl_win.cc
+++ b/content/browser/accessibility/browser_accessibility_state_impl_win.cc
@@ -21,6 +21,7 @@
 #include "base/files/file_path.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
 #include "base/task/thread_pool.h"
 #include "base/win/registry.h"
 #include "content/browser/web_contents/web_contents_impl.h"
diff --git a/content/browser/back_forward_cache_basics_browsertest.cc b/content/browser/back_forward_cache_basics_browsertest.cc
index 0cb2693..1d04ba9 100644
--- a/content/browser/back_forward_cache_basics_browsertest.cc
+++ b/content/browser/back_forward_cache_basics_browsertest.cc
@@ -4,6 +4,7 @@
 
 #include <array>
 
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "content/browser/back_forward_cache_browsertest.h"
 #include "content/browser/web_contents/web_contents_impl.h"
@@ -788,8 +789,7 @@
   // RelatedActiveContents metrics because the related active contents
   // count is > 1.
   if (ShouldCreateNewHostForAllFrames() ||
-      (!NavigateSameSite() &&
-       SiteIsolationPolicy::UseDedicatedProcessesForAllSites())) {
+      (!NavigateSameSite() && AreStrictSiteInstancesEnabled())) {
     ExpectNotRestored(
         {NotRestoredReason::kRelatedActiveContentsExist,
          NotRestoredReason::kBlocklistedFeatures,
diff --git a/content/browser/back_forward_cache_features_browsertest.cc b/content/browser/back_forward_cache_features_browsertest.cc
index d8b85be..38f54aa 100644
--- a/content/browser/back_forward_cache_features_browsertest.cc
+++ b/content/browser/back_forward_cache_features_browsertest.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "base/containers/contains.h"
+#include "base/strings/stringprintf.h"
 #include "base/threading/platform_thread.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
diff --git a/content/browser/browsing_instance.cc b/content/browser/browsing_instance.cc
index 3fd633d..8a6358d 100644
--- a/content/browser/browsing_instance.cc
+++ b/content/browser/browsing_instance.cc
@@ -138,8 +138,8 @@
 
   // Check to see if we can use the default SiteInstance for sites that don't
   // need to be isolated in their own process.
-  if (allow_default_instance &&
-      SiteInstanceImpl::CanBePlacedInDefaultSiteInstance(
+  if (!ShouldUseDefaultSiteInstanceGroup() && allow_default_instance &&
+      SiteInstanceImpl::CanBePlacedInDefaultSiteInstanceOrGroup(
           isolation_context_, url_info.url, site_info)) {
     scoped_refptr<SiteInstanceImpl> site_instance =
         default_site_instance_.get();
@@ -183,6 +183,7 @@
   // Explicitly prevent the default SiteInstance from being added since
   // the map is only supposed to contain instances that map to a single site.
   if (site_instance->IsDefaultSiteInstance()) {
+    DCHECK(!ShouldUseDefaultSiteInstanceGroup());
     CHECK(!default_site_instance_);
     default_site_instance_ = site_instance;
     return;
diff --git a/content/browser/btm/btm_bounce_detector_unittest.cc b/content/browser/btm/btm_bounce_detector_unittest.cc
index 0d70cff4..2f17336 100644
--- a/content/browser/btm/btm_bounce_detector_unittest.cc
+++ b/content/browser/btm/btm_bounce_detector_unittest.cc
@@ -11,6 +11,7 @@
 #include "base/functional/callback_forward.h"
 #include "base/functional/callback_helpers.h"
 #include "base/strings/strcat.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
diff --git a/content/browser/btm/btm_navigation_flow_detector_browsertest.cc b/content/browser/btm/btm_navigation_flow_detector_browsertest.cc
index e08d3ec..f3d0b7da1 100644
--- a/content/browser/btm/btm_navigation_flow_detector_browsertest.cc
+++ b/content/browser/btm/btm_navigation_flow_detector_browsertest.cc
@@ -5,6 +5,7 @@
 #include "content/browser/btm/btm_navigation_flow_detector.h"
 
 #include "base/base64.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/to_string.h"
 #include "base/test/bind.h"
 #include "base/test/gmock_expected_support.h"
diff --git a/content/browser/child_process_launcher_helper.h b/content/browser/child_process_launcher_helper.h
index bee6bc2..620a337 100644
--- a/content/browser/child_process_launcher_helper.h
+++ b/content/browser/child_process_launcher_helper.h
@@ -242,7 +242,8 @@
   void OnChildProcessStarted(pid_t process_id,
                              std::unique_ptr<LaunchResult> launch_result);
   void ClearProcessStorage();
-  void SetNormalTermination();
+  void SetExitCode(int exit_code);
+  std::optional<int> GetExitCode();
 
 #if defined(__OBJC__)
   NSObject* GetProcess();
@@ -356,7 +357,7 @@
 
 #if BUILDFLAG(IS_IOS)
   std::unique_ptr<base::ScopedTempDir> scoped_temp_dir_;
-  bool normal_termination_ = false;
+  std::optional<int> exit_code_;
 #endif
 
   // Histogram shared memory region. Ownership of the memory region object is
diff --git a/content/browser/child_process_launcher_helper_android.cc b/content/browser/child_process_launcher_helper_android.cc
index 30e3fe9e..3459186a 100644
--- a/content/browser/child_process_launcher_helper_android.cc
+++ b/content/browser/child_process_launcher_helper_android.cc
@@ -249,11 +249,19 @@
 static jboolean
 JNI_ChildProcessLauncherHelperImpl_ServiceGroupImportanceEnabled(JNIEnv* env) {
   // Not this is called on the launcher thread, not UI thread.
+  //
+  // Note that service grouping is mandatory for site isolation on pre-U devices
+  // to avoid cached process limit. By service grouping, cached chrome renderer
+  // processes in a group are counted as one. On pre-U devices the cached
+  // process limit is usually 32 or such. U+ devices has a larger limit 1024 or
+  // such.
   return (SiteIsolationPolicy::AreIsolatedOriginsEnabled() ||
           SiteIsolationPolicy::UseDedicatedProcessesForAllSites() ||
           SiteIsolationPolicy::AreDynamicIsolatedOriginsEnabled() ||
           SiteIsolationPolicy::ArePreloadedIsolatedOriginsEnabled()) &&
-         base::FeatureList::IsEnabled(kServiceGroupImportance);
+         (base::android::android_info::sdk_int() <
+              base::android::android_info::SDK_VERSION_U ||
+          base::FeatureList::IsEnabled(kServiceGroupImportance));
 }
 
 // static
diff --git a/content/browser/child_process_launcher_helper_ios.mm b/content/browser/child_process_launcher_helper_ios.mm
index 109f1df..221c428 100644
--- a/content/browser/child_process_launcher_helper_ios.mm
+++ b/content/browser/child_process_launcher_helper_ios.mm
@@ -53,24 +53,6 @@
   }
 }
 
-bool TerminateNow(pid_t process_id) {
-  NSObject* process = nullptr;
-  {
-    base::AutoLock guard(*g_process_table_lock_);
-    auto it = g_process_table_->find(process_id);
-    if (it != g_process_table_->end()) {
-      it->second->SetNormalTermination();
-      process = it->second->GetProcess();
-    }
-  }
-
-  if (!process) {
-    return false;
-  }
-  InvalidateProcess(process);
-  return true;
-}
-
 bool WaitForExit(pid_t process_id, int* exit_code, base::TimeDelta timeout) {
   base::TimeTicks wakeup_time = base::TimeTicks::Now() + timeout;
   constexpr uint32_t kMaxSleepInMicroseconds = 1 << 18;  // ~256 ms.
@@ -84,7 +66,7 @@
       if (it != g_process_table_->end()) {
         if (it->second->GetProcess() == nullptr) {
           if (exit_code) {
-            *exit_code = 0;
+            *exit_code = it->second->GetExitCode().value_or(0);
           }
           return true;
         }
@@ -108,6 +90,27 @@
   }
 }
 
+bool TerminateNow(pid_t process_id, int exit_code, bool wait) {
+  NSObject* process = nullptr;
+  {
+    base::AutoLock guard(*g_process_table_lock_);
+    auto it = g_process_table_->find(process_id);
+    if (it != g_process_table_->end()) {
+      it->second->SetExitCode(exit_code);
+      process = it->second->GetProcess();
+    }
+  }
+
+  if (!process) {
+    return false;
+  }
+  InvalidateProcess(process);
+  if (wait) {
+    return WaitForExit(process_id, nullptr, base::Seconds(60));
+  }
+  return true;
+}
+
 // Object used to pass the result of the launch from the async
 // dispatch_queue to the LauncherThread.
 class LaunchResult {
@@ -415,9 +418,15 @@
     info.status = base::TERMINATION_STATUS_LAUNCH_FAILED;
   } else if (static_cast<ProcessStorage*>(process_storage_.get())->Process() ==
              nullptr) {
-    info.status = normal_termination_
-                      ? base::TERMINATION_STATUS_NORMAL_TERMINATION
-                      : base::TERMINATION_STATUS_PROCESS_CRASHED;
+    if (exit_code_.has_value()) {
+      if (exit_code_.value() == RESULT_CODE_NORMAL_EXIT) {
+        info.status = base::TERMINATION_STATUS_NORMAL_TERMINATION;
+      } else {
+        info.status = base::TERMINATION_STATUS_PROCESS_WAS_KILLED;
+      }
+    } else {
+      info.status = base::TERMINATION_STATUS_PROCESS_CRASHED;
+    }
   } else {
     info.status = base::TERMINATION_STATUS_STILL_RUNNING;
   }
@@ -430,8 +439,12 @@
   }
 }
 
-void ChildProcessLauncherHelper::SetNormalTermination() {
-  normal_termination_ = true;
+void ChildProcessLauncherHelper::SetExitCode(int exit_code) {
+  exit_code_ = exit_code;
+}
+
+std::optional<int> ChildProcessLauncherHelper::GetExitCode() {
+  return exit_code_;
 }
 
 NSObject* ChildProcessLauncherHelper::GetProcess() {
diff --git a/content/browser/code_cache/generated_code_cache_browsertest.cc b/content/browser/code_cache/generated_code_cache_browsertest.cc
index f4d0fc89..8faec12 100644
--- a/content/browser/code_cache/generated_code_cache_browsertest.cc
+++ b/content/browser/code_cache/generated_code_cache_browsertest.cc
@@ -5,6 +5,7 @@
 #include "content/browser/code_cache/generated_code_cache.h"
 
 #include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/test_future.h"
diff --git a/content/browser/content_security_policy_browsertest.cc b/content/browser/content_security_policy_browsertest.cc
index 3a5119b..559cd05 100644
--- a/content/browser/content_security_policy_browsertest.cc
+++ b/content/browser/content_security_policy_browsertest.cc
@@ -10,6 +10,7 @@
 #include "base/memory/raw_ref.h"
 #include "base/notreached.h"
 #include "base/path_service.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/threading/thread_restrictions.h"
 #include "content/browser/renderer_host/render_frame_host_impl.h"
diff --git a/content/browser/dom_storage/dom_storage_context_wrapper.cc b/content/browser/dom_storage/dom_storage_context_wrapper.cc
index 83e16b0..d7a3599b 100644
--- a/content/browser/dom_storage/dom_storage_context_wrapper.cc
+++ b/content/browser/dom_storage/dom_storage_context_wrapper.cc
@@ -22,7 +22,6 @@
 #include "build/build_config.h"
 #include "components/services/storage/dom_storage/local_storage_impl.h"
 #include "components/services/storage/dom_storage/session_storage_impl.h"
-#include "components/services/storage/public/mojom/partition.mojom.h"
 #include "components/services/storage/public/mojom/storage_policy_update.mojom.h"
 #include "components/services/storage/public/mojom/storage_usage_info.mojom.h"
 #include "content/browser/dom_storage/session_storage_namespace_impl.h"
@@ -44,6 +43,9 @@
 #include "third_party/blink/public/common/storage_key/storage_key.h"
 
 namespace content {
+#if BUILDFLAG(IS_MAC)
+using LocalStorageLifecycle = storage::mojom::LocalStorageLifecycle;
+#endif  // BUILDFLAG(IS_MAC)
 namespace {
 
 void AdaptSessionStorageUsageInfo(
@@ -72,6 +74,22 @@
   std::move(callback).Run(result);
 }
 
+#if BUILDFLAG(IS_MAC)
+LocalStorageLifecycle GetLocalStorageLifecycle(
+    bool recovering,
+    bool storage_service_remote_was_bound) {
+  if (recovering) {
+    return storage_service_remote_was_bound
+               ? LocalStorageLifecycle::kRecovering
+               : LocalStorageLifecycle::kRecoveringWithUnboundStorageService;
+  } else {
+    return storage_service_remote_was_bound
+               ? LocalStorageLifecycle::kInitializing
+               : LocalStorageLifecycle::kInitializingWithUnboundStorageService;
+  }
+}
+#endif  // BUILDFLAG(IS_MAC)
+
 }  // namespace
 
 scoped_refptr<DOMStorageContextWrapper> DOMStorageContextWrapper::Create(
@@ -100,8 +118,18 @@
       base::BindRepeating(&DOMStorageContextWrapper::OnMemoryPressure,
                           base::Unretained(this)));
 
+#if BUILDFLAG(IS_MAC)
+  // Binding Session or Local storage will result in the storage service getting
+  // bound. So, we capture this state before those calls.
+  LocalStorageLifecycle lifecycle = GetLocalStorageLifecycle(
+      /*recovering=*/false, partition_->IsStorageServiceRemoteValid());
+#endif  // BUILDFLAG(IS_MAC)
   MaybeBindSessionStorageControl();
+#if BUILDFLAG(IS_MAC)
+  MaybeBindLocalStorageControlAndReportLifecycle(lifecycle);
+#else
   MaybeBindLocalStorageControl();
+#endif  // BUILDFLAG(IS_MAC)
 }
 
 DOMStorageContextWrapper::~DOMStorageContextWrapper() {
@@ -331,8 +359,18 @@
 
 void DOMStorageContextWrapper::RecoverFromStorageServiceCrash() {
   DCHECK(partition_);
+#if BUILDFLAG(IS_MAC)
+  // Binding Session or Local storage will result in the storage service getting
+  // bound. So, we capture this state before those calls.
+  LocalStorageLifecycle lifecycle = GetLocalStorageLifecycle(
+      /*recovering=*/true, partition_->IsStorageServiceRemoteValid());
+#endif  // BUILDFLAG(IS_MAC)
   MaybeBindSessionStorageControl();
+#if BUILDFLAG(IS_MAC)
+  MaybeBindLocalStorageControlAndReportLifecycle(lifecycle);
+#else
   MaybeBindLocalStorageControl();
+#endif  // BUILDFLAG(IS_MAC)
 
   // Make sure the service is aware of namespaces we asked a previous instance
   // to create, so it can properly service renderers trying to manipulate those
@@ -359,6 +397,19 @@
       local_storage_control_.BindNewPipeAndPassReceiver());
 }
 
+#if BUILDFLAG(IS_MAC)
+void DOMStorageContextWrapper::MaybeBindLocalStorageControlAndReportLifecycle(
+    LocalStorageLifecycle lifecycle) {
+  if (!partition_) {
+    return;
+  }
+  local_storage_control_.reset();
+  partition_->GetStorageServicePartition()
+      ->BindLocalStorageControlAndReportLifecycle(
+          lifecycle, local_storage_control_.BindNewPipeAndPassReceiver());
+}
+#endif  // BUILDFLAG(IS_MAC)
+
 scoped_refptr<SessionStorageNamespaceImpl>
 DOMStorageContextWrapper::MaybeGetExistingNamespace(
     const std::string& namespace_id) const {
diff --git a/content/browser/dom_storage/dom_storage_context_wrapper.h b/content/browser/dom_storage/dom_storage_context_wrapper.h
index 63ca08b..6289ed4 100644
--- a/content/browser/dom_storage/dom_storage_context_wrapper.h
+++ b/content/browser/dom_storage/dom_storage_context_wrapper.h
@@ -17,6 +17,7 @@
 #include "base/thread_annotations.h"
 #include "base/threading/sequence_bound.h"
 #include "components/services/storage/public/mojom/local_storage_control.mojom.h"
+#include "components/services/storage/public/mojom/partition.mojom.h"
 #include "components/services/storage/public/mojom/session_storage_control.mojom.h"
 #include "components/services/storage/public/mojom/storage_usage_info.mojom.h"
 #include "content/browser/child_process_security_policy_impl.h"
@@ -141,6 +142,10 @@
 
   void MaybeBindSessionStorageControl();
   void MaybeBindLocalStorageControl();
+#if BUILDFLAG(IS_MAC)
+  void MaybeBindLocalStorageControlAndReportLifecycle(
+      storage::mojom::LocalStorageLifecycle lifecycle);
+#endif  // BUILDFLAG(IS_MAC)
   scoped_refptr<SessionStorageNamespaceImpl> MaybeGetExistingNamespace(
       const std::string& namespace_id) const;
 
diff --git a/content/browser/interest_group/auction_nonce_manager_unittest.cc b/content/browser/interest_group/auction_nonce_manager_unittest.cc
index ceb49e6..55909dd 100644
--- a/content/browser/interest_group/auction_nonce_manager_unittest.cc
+++ b/content/browser/interest_group/auction_nonce_manager_unittest.cc
@@ -3,10 +3,12 @@
 // found in the LICENSE file.
 
 #include "content/browser/interest_group/auction_nonce_manager.h"
+
 #include <memory>
 #include <string>
 
 #include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/uuid.h"
 #include "content/public/test/browser_test_utils.h"
diff --git a/content/browser/interest_group/tools/adjustable_auction.cc b/content/browser/interest_group/tools/adjustable_auction.cc
index 09fc0b1..3dbc565a 100644
--- a/content/browser/interest_group/tools/adjustable_auction.cc
+++ b/content/browser/interest_group/tools/adjustable_auction.cc
@@ -13,6 +13,7 @@
 #include "base/functional/bind.h"
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/gtest_util.h"
diff --git a/content/browser/isolated_origin_browsertest.cc b/content/browser/isolated_origin_browsertest.cc
index a43efed..7019df9a 100644
--- a/content/browser/isolated_origin_browsertest.cc
+++ b/content/browser/isolated_origin_browsertest.cc
@@ -3815,7 +3815,7 @@
   const SiteInstanceImpl* const newshell_site_instance_impl =
       static_cast<SiteInstanceImpl*>(
           new_shell->web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
-  if (AreAllSitesIsolatedForTesting()) {
+  if (AreStrictSiteInstancesEnabled()) {
     // At this point, the popup and the opener should still be in separate
     // SiteInstances.
     EXPECT_NE(newshell_site_instance_impl, root_site_instance_impl);
@@ -4976,9 +4976,15 @@
 }
 
 namespace {
-bool HasDefaultSiteInstance(RenderFrameHost* rfh) {
-  return static_cast<SiteInstanceImpl*>(rfh->GetSiteInstance())
-      ->IsDefaultSiteInstance();
+bool HasDefaultSiteInstanceOrGroup(RenderFrameHost* rfh) {
+  SiteInstanceImpl* site_instance =
+      static_cast<SiteInstanceImpl*>(rfh->GetSiteInstance());
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    return site_instance->group() ==
+           site_instance->DefaultSiteInstanceGroupForBrowsingInstance();
+  } else {
+    return site_instance->IsDefaultSiteInstance();
+  }
 }
 }  // namespace
 
@@ -5052,9 +5058,9 @@
     EXPECT_EQ(a->GetSiteInstance(), d->GetSiteInstance());
     EXPECT_NE(b->GetSiteInstance(), c->GetSiteInstance());
 
-    EXPECT_FALSE(HasDefaultSiteInstance(a));
-    EXPECT_FALSE(HasDefaultSiteInstance(b));
-    EXPECT_FALSE(HasDefaultSiteInstance(c));
+    EXPECT_FALSE(HasDefaultSiteInstanceOrGroup(a));
+    EXPECT_FALSE(HasDefaultSiteInstanceOrGroup(b));
+    EXPECT_FALSE(HasDefaultSiteInstanceOrGroup(c));
   } else {
     // All sites that are not isolated should be in the same default
     // SiteInstance process.
@@ -5064,12 +5070,18 @@
               c->GetProcess()->GetDeprecatedID());
 
     EXPECT_NE(a->GetSiteInstance(), b->GetSiteInstance());
-    EXPECT_EQ(a->GetSiteInstance(), c->GetSiteInstance());
+    if (ShouldUseDefaultSiteInstanceGroup()) {
+      EXPECT_NE(a->GetSiteInstance(), c->GetSiteInstance());
+      EXPECT_EQ(a->GetSiteInstance()->GetSiteInstanceGroupId(),
+                c->GetSiteInstance()->GetSiteInstanceGroupId());
+    } else {
+      EXPECT_EQ(a->GetSiteInstance(), c->GetSiteInstance());
+    }
     EXPECT_EQ(a->GetSiteInstance(), d->GetSiteInstance());
     EXPECT_NE(b->GetSiteInstance(), c->GetSiteInstance());
 
-    EXPECT_TRUE(HasDefaultSiteInstance(a));
-    EXPECT_FALSE(HasDefaultSiteInstance(b));
+    EXPECT_TRUE(HasDefaultSiteInstanceOrGroup(a));
+    EXPECT_FALSE(HasDefaultSiteInstanceOrGroup(b));
   }
 }
 
@@ -6133,18 +6145,36 @@
   EXPECT_TRUE(first_instance->GetProcess()->GetProcessLock().allows_any_site());
 }
 
-// TODO(crbug.com/390571607, yangsharon): Enable these tests once default
-// SiteInstanceGroups has been implemented.
-class IsolatedOriginTestWithStrictSiteInstances : public IsolatedOriginTest {
+class IsolatedOriginTestWithDefaultSiteInstanceGroups
+    : public IsolatedOriginTest,
+      public ::testing::WithParamInterface<bool> {
+ public:
   void SetUpCommandLine(base::CommandLine* command_line) override {
     IsolatedOriginTest::SetUpCommandLine(command_line);
     command_line->AppendSwitch(switches::kDisableSiteIsolation);
     command_line->RemoveSwitch(switches::kSitePerProcess);
+
+    if (IsDefaultSiteInstanceGroupEnabled()) {
+      feature_list_.InitAndEnableFeature(features::kDefaultSiteInstanceGroups);
+    } else {
+      feature_list_.InitAndDisableFeature(features::kDefaultSiteInstanceGroups);
+    }
   }
+
+  static std::string DescribeParams(
+      const testing::TestParamInfo<ParamType>& info) {
+    return info.param ? "UseDefaultSiteInstanceGroups"
+                      : "UseDefaultSiteInstances";
+  }
+
+ private:
+  bool IsDefaultSiteInstanceGroupEnabled() const { return GetParam(); }
+
+  base::test::ScopedFeatureList feature_list_;
 };
 
-IN_PROC_BROWSER_TEST_F(IsolatedOriginTestWithStrictSiteInstances,
-                       DISABLED_NonIsolatedFramesCanShareDefaultProcess) {
+IN_PROC_BROWSER_TEST_P(IsolatedOriginTestWithDefaultSiteInstanceGroups,
+                       NonIsolatedFramesCanShareDefaultProcess) {
   GURL top_url(
       embedded_test_server()->GetURL("/frame_tree/page_with_two_frames.html"));
   ASSERT_FALSE(IsIsolatedOrigin(url::Origin::Create(top_url)));
@@ -6172,22 +6202,42 @@
     observer.Wait();
   }
 
-  // All 3 frames are from different sites, so each should have its own
-  // SiteInstance.
-  EXPECT_NE(root->current_frame_host()->GetSiteInstance(),
-            child1->current_frame_host()->GetSiteInstance());
-  EXPECT_NE(root->current_frame_host()->GetSiteInstance(),
-            child2->current_frame_host()->GetSiteInstance());
-  EXPECT_NE(child1->current_frame_host()->GetSiteInstance(),
-            child2->current_frame_host()->GetSiteInstance());
-  EXPECT_EQ(
-      " Site A ------------ proxies for B C\n"
-      "   |--Site B ------- proxies for A C\n"
-      "   +--Site C ------- proxies for A B\n"
-      "Where A = http://127.0.0.1/\n"
-      "      B = http://bar.com/\n"
-      "      C = http://baz.com/",
-      DepictFrameTree(*root));
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    // All 3 frames are different sites, so each should have its own
+    // SiteInstance.
+    EXPECT_NE(root->current_frame_host()->GetSiteInstance(),
+              child1->current_frame_host()->GetSiteInstance());
+    EXPECT_NE(root->current_frame_host()->GetSiteInstance(),
+              child2->current_frame_host()->GetSiteInstance());
+    EXPECT_NE(child1->current_frame_host()->GetSiteInstance(),
+              child2->current_frame_host()->GetSiteInstance());
+
+    // All 3 sites should share the default SiteInstanceGroup and be in the same
+    // process, so no proxies are needed.
+    EXPECT_EQ(
+        " Site A\n"
+        "   |--Site B\n"
+        "   +--Site C\n"
+        "Where A = http://127.0.0.1/\n"
+        "      B = http://bar.com/\n"
+        "      C = http://baz.com/",
+        DepictFrameTree(*root));
+  } else {
+    // All 3 frames are in the default SiteInstance.
+    EXPECT_EQ(root->current_frame_host()->GetSiteInstance(),
+              child1->current_frame_host()->GetSiteInstance());
+    EXPECT_EQ(root->current_frame_host()->GetSiteInstance(),
+              child2->current_frame_host()->GetSiteInstance());
+    EXPECT_EQ(child1->current_frame_host()->GetSiteInstance(),
+              child2->current_frame_host()->GetSiteInstance());
+
+    EXPECT_EQ(
+        " Site A\n"
+        "   |--Site A\n"
+        "   +--Site A\n"
+        "Where A = http://unisolated.invalid/",
+        DepictFrameTree(*root));
+  }
 
   // But none are isolated, so all should share the default process for their
   // BrowsingInstance.
@@ -6199,11 +6249,10 @@
 
 // Creates a non-isolated main frame with an isolated child and non-isolated
 // grandchild. With strict site isolation disabled and
-// kProcessSharingWithStrictSiteInstances enabled, the main frame and the
-// grandchild should be in the same process even though they have different
-// SiteInstances.
-IN_PROC_BROWSER_TEST_F(IsolatedOriginTestWithStrictSiteInstances,
-                       DISABLED_IsolatedChildWithNonIsolatedGrandchild) {
+// default SiteInstanceGroups enabled, the main frame and the grandchild should
+// be in the default SiteInstanceGroup with different SiteInstances.
+IN_PROC_BROWSER_TEST_P(IsolatedOriginTestWithDefaultSiteInstanceGroups,
+                       IsolatedChildWithNonIsolatedGrandchild) {
   GURL top_url(
       embedded_test_server()->GetURL("www.foo.com", "/page_with_iframe.html"));
   ASSERT_FALSE(IsIsolatedOrigin(url::Origin::Create(top_url)));
@@ -6243,76 +6292,112 @@
   observer.Wait();
   EXPECT_EQ(non_isolated_url, grandchild->current_url());
 
-  EXPECT_NE(root->current_frame_host()->GetSiteInstance(),
-            grandchild->current_frame_host()->GetSiteInstance());
   EXPECT_NE(child->current_frame_host()->GetSiteInstance(),
             grandchild->current_frame_host()->GetSiteInstance());
   EXPECT_EQ(root->current_frame_host()->GetProcess(),
             grandchild->current_frame_host()->GetProcess());
-  EXPECT_EQ(
-      " Site A ------------ proxies for B C\n"
-      "   +--Site B ------- proxies for A C\n"
-      "        +--Site C -- proxies for A B\n"
-      "Where A = http://foo.com/\n"
-      "      B = http://isolated.foo.com/\n"
-      "      C = http://bar.com/",
-      DepictFrameTree(*root));
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_NE(root->current_frame_host()->GetSiteInstance(),
+              grandchild->current_frame_host()->GetSiteInstance());
+    EXPECT_EQ(
+        " Site A ------------ proxies for B\n"
+        "   +--Site B ------- proxies for {A,C}\n"
+        "        +--Site C -- proxies for B\n"
+        "Where A = http://foo.com/\n"
+        "      B = http://isolated.foo.com/\n"
+        "      C = http://bar.com/",
+        DepictFrameTree(*root));
+  } else {
+    EXPECT_EQ(root->current_frame_host()->GetSiteInstance(),
+              grandchild->current_frame_host()->GetSiteInstance());
+    EXPECT_EQ(
+        " Site A ------------ proxies for B\n"
+        "   +--Site B ------- proxies for A\n"
+        "        +--Site A -- proxies for B\n"
+        "Where A = http://unisolated.invalid/\n"
+        "      B = http://isolated.foo.com/",
+        DepictFrameTree(*root));
+  }
 }
 
 // Navigate a frame into and out of an isolated origin. This should not
-// confuse BrowsingInstance into holding onto a stale default_process_.
-IN_PROC_BROWSER_TEST_F(
-    IsolatedOriginTestWithStrictSiteInstances,
-    DISABLED_SubframeNavigatesOutofIsolationThenToIsolation) {
+// confuse BrowsingInstance into holding onto a stale default SiteInstance or
+// default SiteInstanceGroup
+IN_PROC_BROWSER_TEST_P(IsolatedOriginTestWithDefaultSiteInstanceGroups,
+                       SubframeNavigatesOutOfIsolationThenToIsolation) {
+  // Navigate to an isolated site, with a same-site subframe.
   GURL isolated_url(embedded_test_server()->GetURL("isolated.foo.com",
                                                    "/page_with_iframe.html"));
   ASSERT_TRUE(IsIsolatedOrigin(url::Origin::Create(isolated_url)));
   EXPECT_TRUE(NavigateToURL(shell(), isolated_url));
 
   FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
+  SiteInstanceImpl* root_instance =
+      root->current_frame_host()->GetSiteInstance();
   FrameTreeNode* child = root->child_at(0);
-  EXPECT_EQ(web_contents()->GetSiteInstance(),
-            child->current_frame_host()->GetSiteInstance());
+  SiteInstanceImpl* child_instance =
+      child->current_frame_host()->GetSiteInstance();
+  EXPECT_EQ(web_contents()->GetSiteInstance(), child_instance);
+  EXPECT_EQ(root_instance->group(), child_instance->group());
   EXPECT_FALSE(child->current_frame_host()->IsCrossProcessSubframe());
+  EXPECT_FALSE(HasDefaultSiteInstanceOrGroup(child->current_frame_host()));
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_NE(child_instance->group(),
+              child_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_FALSE(child_instance->IsDefaultSiteInstance());
+  }
 
+  // Navigate the child to a non-isolated page.
   GURL non_isolated_url(
-      embedded_test_server()->GetURL("www.foo.com", "/title3.html"));
+      embedded_test_server()->GetURL("www.bar.com", "/title3.html"));
   ASSERT_FALSE(IsIsolatedOrigin(url::Origin::Create(non_isolated_url)));
   NavigateIframeToURL(web_contents(), "test_iframe", non_isolated_url);
+  child_instance = child->current_frame_host()->GetSiteInstance();
+
   EXPECT_EQ(child->current_url(), non_isolated_url);
+  EXPECT_TRUE(HasDefaultSiteInstanceOrGroup(child->current_frame_host()));
+  EXPECT_NE(root_instance->group(), child_instance->group());
+  // Keep this value for comparing later.
+  SiteInstanceGroup* first_default_group = child_instance->group();
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_EQ(child_instance->group(),
+              child_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_TRUE(child_instance->IsDefaultSiteInstance());
+  }
 
-  // Verify that the child frame is an OOPIF with a different SiteInstance.
-  EXPECT_NE(web_contents()->GetSiteInstance(),
-            child->current_frame_host()->GetSiteInstance());
-  EXPECT_NE(root->current_frame_host()->GetProcess(),
-            child->current_frame_host()->GetProcess());
+  // Navigate to an isolated page without an iframe. This should cause the
+  // default SiteInstance/Group to be deleted.
+  GURL isolated_url2(
+      embedded_test_server()->GetURL("isolated.foo.com", "/title1.html"));
+  ASSERT_TRUE(IsIsolatedOrigin(url::Origin::Create(isolated_url2)));
+  EXPECT_TRUE(NavigateToURL(shell(), isolated_url2));
+  EXPECT_FALSE(
+      HasDefaultSiteInstanceOrGroup(web_contents()->GetPrimaryMainFrame()));
 
-  // Navigating the child to the isolated origin again.
-  NavigateIframeToURL(web_contents(), "test_iframe", isolated_url);
-  EXPECT_EQ(child->current_url(), isolated_url);
-  EXPECT_EQ(web_contents()->GetSiteInstance(),
-            child->current_frame_host()->GetSiteInstance());
-
-  // And navigate out of the isolated origin one last time.
-  NavigateIframeToURL(web_contents(), "test_iframe", non_isolated_url);
-  EXPECT_EQ(child->current_url(), non_isolated_url);
-  EXPECT_NE(web_contents()->GetSiteInstance(),
-            child->current_frame_host()->GetSiteInstance());
-  EXPECT_NE(root->current_frame_host()->GetProcess(),
-            child->current_frame_host()->GetProcess());
-  EXPECT_EQ(
-      " Site A ------------ proxies for B\n"
-      "   +--Site B ------- proxies for A\n"
-      "Where A = http://isolated.foo.com/\n"
-      "      B = http://foo.com/",
-      DepictFrameTree(*root));
+  // Navigate the main frame to bar.com. We expect bar's SiteInstance to be in
+  // the default SiteInstance/Group, but a different one from the subframe
+  // navigation, since that should have been destroyed when we navigated away,
+  // as it was the last and only SiteInstance in the default SiteInstance/Group.
+  EXPECT_TRUE(NavigateToURL(shell(), non_isolated_url));
+  FrameTreeNode* bar = web_contents()->GetPrimaryFrameTree().root();
+  SiteInstanceImpl* bar_instance = bar->current_frame_host()->GetSiteInstance();
+  // The SiteInstanceGroup and process from the first time we navigated to
+  // bar.com should not be reused.
+  EXPECT_NE(bar_instance->group(), first_default_group);
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_EQ(bar_instance->group(),
+              bar_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_TRUE(bar_instance->IsDefaultSiteInstance());
+  }
 }
 
 // Ensure a popup and its opener can go in the same process, even though
-// they have different SiteInstances with kProcessSharingWithStrictSiteInstances
-// enabled.
-IN_PROC_BROWSER_TEST_F(IsolatedOriginTestWithStrictSiteInstances,
-                       DISABLED_NonIsolatedPopup) {
+// they have different SiteInstances with default SiteInstanceGroups enabled.
+IN_PROC_BROWSER_TEST_P(IsolatedOriginTestWithDefaultSiteInstanceGroups,
+                       NonIsolatedPopup) {
   GURL foo_url(
       embedded_test_server()->GetURL("www.foo.com", "/page_with_iframe.html"));
   EXPECT_TRUE(NavigateToURL(shell(), foo_url));
@@ -6333,32 +6418,53 @@
     ASSERT_TRUE(manager.WaitForNavigationFinished());
   }
 
-  // The popup and the opener should not share a SiteInstance, but should
-  // end up in the same process.
-  EXPECT_NE(new_shell->web_contents()->GetPrimaryMainFrame()->GetSiteInstance(),
-            root->current_frame_host()->GetSiteInstance());
+  // The popup and the opener should not share a SiteInstance in default
+  // SiteInstanceGroup mode, but should end up in the same process in either
+  // mode.
   EXPECT_EQ(root->current_frame_host()->GetProcess(),
             new_shell->web_contents()->GetPrimaryMainFrame()->GetProcess());
-  EXPECT_EQ(
-      " Site A ------------ proxies for B\n"
-      "   +--Site A ------- proxies for B\n"
-      "Where A = http://foo.com/\n"
-      "      B = http://bar.com/",
-      DepictFrameTree(*root));
-  EXPECT_EQ(
-      " Site A ------------ proxies for B\n"
-      "Where A = http://bar.com/\n"
-      "      B = http://foo.com/",
-      DepictFrameTree(*static_cast<WebContentsImpl*>(new_shell->web_contents())
-                           ->GetPrimaryFrameTree()
-                           .root()));
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_NE(
+        new_shell->web_contents()->GetPrimaryMainFrame()->GetSiteInstance(),
+        root->current_frame_host()->GetSiteInstance());
+    // There should be no proxies between the popup and opener since they share
+    // a SiteInstanceGroup.
+    EXPECT_EQ(
+        " Site A\n"
+        "   +--Site A\n"
+        "Where A = http://foo.com/",
+        DepictFrameTree(*root));
+    EXPECT_EQ(
+        " Site A\n"
+        "Where A = http://bar.com/",
+        DepictFrameTree(
+            *static_cast<WebContentsImpl*>(new_shell->web_contents())
+                 ->GetPrimaryFrameTree()
+                 .root()));
+  } else {
+    EXPECT_EQ(
+        new_shell->web_contents()->GetPrimaryMainFrame()->GetSiteInstance(),
+        root->current_frame_host()->GetSiteInstance());
+    EXPECT_EQ(
+        " Site A\n"
+        "   +--Site A\n"
+        "Where A = http://unisolated.invalid/",
+        DepictFrameTree(*root));
+    EXPECT_EQ(
+        " Site A\n"
+        "Where A = http://unisolated.invalid/",
+        DepictFrameTree(
+            *static_cast<WebContentsImpl*>(new_shell->web_contents())
+                 ->GetPrimaryFrameTree()
+                 .root()));
+  }
 }
 
 // Check that when a cross-site, non-isolated-origin iframe opens a popup,
 // navigates it to an isolated origin, and then the popup navigates back to its
 // opener iframe's site, the popup and the opener iframe end up in the same
 // process and can script each other.  See https://crbug.com/796912.
-IN_PROC_BROWSER_TEST_F(IsolatedOriginTestWithStrictSiteInstances,
+IN_PROC_BROWSER_TEST_P(IsolatedOriginTestWithDefaultSiteInstanceGroups,
                        PopupNavigatesToIsolatedOriginAndBack) {
   // Start on a page with same-site iframe.
   GURL foo_url(
@@ -6415,6 +6521,64 @@
   EXPECT_EQ(bar_url2.spec(), EvalJs(child, "window.w.location.href;"));
 }
 
+// Make sure a navigation from about:blank in the default SiteInstanceGroup to
+// an isolated site correctly gets a different process. Also makes sure opening
+// a popup is same-SiteInstanceGroup.
+IN_PROC_BROWSER_TEST_P(IsolatedOriginTestWithDefaultSiteInstanceGroups,
+                       InitialEmptyDocumentToIsolatedSite) {
+  GURL foo_url(embedded_test_server()->GetURL("www.bar.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), foo_url));
+
+  ShellAddedObserver new_shell1_observer;
+  EXPECT_TRUE(ExecJs(shell(), "window.w = window.open();"));
+  Shell* new_shell1 = new_shell1_observer.GetShell();
+
+  ShellAddedObserver new_shell2_observer;
+  EXPECT_TRUE(ExecJs(shell(), "window.w = window.open();"));
+  Shell* new_shell2 = new_shell2_observer.GetShell();
+
+  // Everything should be in the same default SiteInstanceGroup, and thus same
+  // process, so far.
+  SiteInstanceImpl* root_site_instance =
+      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
+  SiteInstanceImpl* popup1_site_instance = static_cast<SiteInstanceImpl*>(
+      new_shell1->web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
+  SiteInstanceImpl* popup2_site_instance = static_cast<SiteInstanceImpl*>(
+      new_shell2->web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
+  EXPECT_EQ(root_site_instance->group(), popup1_site_instance->group());
+  EXPECT_EQ(popup2_site_instance->group(), popup1_site_instance->group());
+
+  // Navigate popup1 to an isolated site.
+  GURL isolated_url(
+      embedded_test_server()->GetURL("isolated.foo.com", "/title1.html"));
+  EXPECT_TRUE(IsIsolatedOrigin(isolated_url));
+  {
+    TestNavigationManager manager(new_shell1->web_contents(), isolated_url);
+    EXPECT_TRUE(
+        ExecJs(new_shell1, "location.href = '" + isolated_url.spec() + "';"));
+    ASSERT_TRUE(manager.WaitForNavigationFinished());
+  }
+
+  SiteInstanceImpl* isolated_site_instance = static_cast<SiteInstanceImpl*>(
+      new_shell1->web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
+  EXPECT_NE(isolated_site_instance->group(), popup2_site_instance->group());
+  EXPECT_NE(isolated_site_instance->group(), root_site_instance->group());
+  EXPECT_NE(isolated_site_instance->group(), popup2_site_instance->group());
+  EXPECT_EQ(popup2_site_instance->group(), root_site_instance->group());
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_EQ(
+        popup2_site_instance->group(),
+        popup2_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+    EXPECT_NE(
+        isolated_site_instance->group(),
+        isolated_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_TRUE(popup2_site_instance->IsDefaultSiteInstance());
+    EXPECT_TRUE(root_site_instance->IsDefaultSiteInstance());
+    EXPECT_FALSE(isolated_site_instance->IsDefaultSiteInstance());
+  }
+}
+
 class WildcardOriginIsolationTest : public IsolatedOriginTestBase {
  public:
   WildcardOriginIsolationTest() = default;
@@ -7679,4 +7843,9 @@
   }
 }
 
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    IsolatedOriginTestWithDefaultSiteInstanceGroups,
+    ::testing::Bool(),
+    &IsolatedOriginTestWithDefaultSiteInstanceGroups::DescribeParams);
 }  // namespace content
diff --git a/content/browser/media/cdm_registry_impl.cc b/content/browser/media/cdm_registry_impl.cc
index 3425512..ccd989d6 100644
--- a/content/browser/media/cdm_registry_impl.cc
+++ b/content/browser/media/cdm_registry_impl.cc
@@ -106,16 +106,6 @@
         base::UmaHistogramBoolean(
             uma_prefix + ".Support." + media::GetCodecNameForUMA(video_codec),
             is_supported);
-
-        // When the codec is supported for hardware security, report whether
-        // clear lead is supported or not.
-        if (is_supported) {
-          bool is_clear_lead_supported =
-              video_codecs.at(video_codec).supports_clear_lead;
-          base::UmaHistogramBoolean(uma_prefix + ".ClearLeadSupport." +
-                                        media::GetCodecNameForUMA(video_codec),
-                                    is_clear_lead_supported);
-        }
       }
     }
   }
diff --git a/content/browser/media/media_capabilities_browsertest.cc b/content/browser/media/media_capabilities_browsertest.cc
index 123088e..a5436de 100644
--- a/content/browser/media/media_capabilities_browsertest.cc
+++ b/content/browser/media/media_capabilities_browsertest.cc
@@ -8,6 +8,7 @@
 #include "base/command_line.h"
 #include "base/files/file_util.h"
 #include "base/strings/string_split.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/to_string.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/scoped_feature_list.h"
diff --git a/content/browser/mojo_sandbox_browsertest.cc b/content/browser/mojo_sandbox_browsertest.cc
index bac07e6..60e1b63 100644
--- a/content/browser/mojo_sandbox_browsertest.cc
+++ b/content/browser/mojo_sandbox_browsertest.cc
@@ -43,34 +43,32 @@
   MojoSandboxTest(const MojoSandboxTest&) = delete;
   MojoSandboxTest& operator=(const MojoSandboxTest&) = delete;
 
-  using BeforeStartCallback = base::OnceCallback<void(UtilityProcessHost*)>;
+  using BeforeStartCallback = base::OnceCallback<void(UtilityProcessHost&)>;
 
-  void StartProcess(BeforeStartCallback callback = BeforeStartCallback()) {
-    host_ = std::make_unique<UtilityProcessHost>();
-    host_->SetMetricsName("mojo_sandbox_test_process");
-    if (callback)
-      std::move(callback).Run(host_.get());
-    ASSERT_TRUE(host_->Start());
-  }
+  mojo::Remote<mojom::TestService> StartProcessAndBindTestInterface(
+      bool unsandboxed = false) {
+    UtilityProcessHost::Options options;
 
-  mojo::Remote<mojom::TestService> BindTestService() {
+    options.WithMetricsName("mojo_sandbox_test_process");
+
+    if (unsandboxed) {
+      options.WithSandboxType(sandbox::mojom::Sandbox::kNoSandbox);
+    }
     mojo::Remote<mojom::TestService> test_service;
-    host_->GetChildProcess()->BindServiceInterface(
+    options.WithBoundServiceInterfaceOnChildProcess(
         test_service.BindNewPipeAndPassReceiver());
+
+    UtilityProcessHost::Start(std::move(options));
+
     return test_service;
   }
-
-  void TearDownOnMainThread() override { host_.reset(); }
-
- protected:
-  std::unique_ptr<UtilityProcessHost> host_;
 };
 
 // Ensures that a read-only shared memory region can be created within a
 // sandboxed process.
 IN_PROC_BROWSER_TEST_F(MojoSandboxTest, SubprocessReadOnlySharedMemoryRegion) {
-  StartProcess();
-  mojo::Remote<mojom::TestService> test_service = BindTestService();
+  mojo::Remote<mojom::TestService> test_service =
+      StartProcessAndBindTestInterface();
 
   bool got_response = false;
   base::RunLoop run_loop;
@@ -93,8 +91,8 @@
 // Ensures that a writable shared memory region can be created within a
 // sandboxed process.
 IN_PROC_BROWSER_TEST_F(MojoSandboxTest, SubprocessWritableSharedMemoryRegion) {
-  StartProcess();
-  mojo::Remote<mojom::TestService> test_service = BindTestService();
+  mojo::Remote<mojom::TestService> test_service =
+      StartProcessAndBindTestInterface();
 
   bool got_response = false;
   base::RunLoop run_loop;
@@ -117,8 +115,8 @@
 // Ensures that an unsafe shared memory region can be created within a
 // sandboxed process.
 IN_PROC_BROWSER_TEST_F(MojoSandboxTest, SubprocessUnsafeSharedMemoryRegion) {
-  StartProcess();
-  mojo::Remote<mojom::TestService> test_service = BindTestService();
+  mojo::Remote<mojom::TestService> test_service =
+      StartProcessAndBindTestInterface();
 
   bool got_response = false;
   base::RunLoop run_loop;
@@ -140,8 +138,8 @@
 
 // Test for sandbox::policy::IsProcessSandboxed().
 IN_PROC_BROWSER_TEST_F(MojoSandboxTest, IsProcessSandboxed) {
-  StartProcess();
-  mojo::Remote<mojom::TestService> test_service = BindTestService();
+  mojo::Remote<mojom::TestService> test_service =
+      StartProcessAndBindTestInterface();
 
   // The browser should not be considered sandboxed.
   EXPECT_FALSE(sandbox::policy::Sandbox::IsProcessSandboxed());
@@ -167,10 +165,8 @@
 #define MAYBE_NotIsProcessSandboxed NotIsProcessSandboxed
 #endif
 IN_PROC_BROWSER_TEST_F(MojoSandboxTest, MAYBE_NotIsProcessSandboxed) {
-  StartProcess(base::BindOnce([](UtilityProcessHost* host) {
-    host->SetSandboxType(sandbox::mojom::Sandbox::kNoSandbox);
-  }));
-  mojo::Remote<mojom::TestService> test_service = BindTestService();
+  mojo::Remote<mojom::TestService> test_service =
+      StartProcessAndBindTestInterface(/*unsandboxed=*/true);
 
   // The browser should not be considered sandboxed.
   EXPECT_FALSE(sandbox::policy::Sandbox::IsProcessSandboxed());
diff --git a/content/browser/power_monitor_browsertest.cc b/content/browser/power_monitor_browsertest.cc
index 2658c0d5..8de54d1 100644
--- a/content/browser/power_monitor_browsertest.cc
+++ b/content/browser/power_monitor_browsertest.cc
@@ -147,13 +147,13 @@
       base::OnceClosure utility_bound_closure) {
     utility_bound_closure_ = std::move(utility_bound_closure);
 
-    UtilityProcessHost* host = new UtilityProcessHost();
-    host->SetMetricsName("test_process");
-    host->SetName(u"TestProcess");
-    EXPECT_TRUE(host->Start());
-
-    host->GetChildProcess()->BindReceiver(
-        power_monitor_test->BindNewPipeAndPassReceiver());
+    UtilityProcessHost::Start(
+        UtilityProcessHost::Options()
+            .WithMetricsName("test_process")
+            .WithName(u"TestProcess")
+            .WithBoundReceiverOnChildProcessForTesting(
+                power_monitor_test->BindNewPipeAndPassReceiver())
+            .Pass());
   }
 
   void set_renderer_bound_closure(base::OnceClosure closure) {
diff --git a/content/browser/preloading/prerender/prerender_browsertest.cc b/content/browser/preloading/prerender/prerender_browsertest.cc
index 01349d33..1b65121 100644
--- a/content/browser/preloading/prerender/prerender_browsertest.cc
+++ b/content/browser/preloading/prerender/prerender_browsertest.cc
@@ -23,6 +23,7 @@
 #include "base/scoped_observation.h"
 #include "base/strings/escape.h"
 #include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/synchronization/lock.h"
 #include "base/task/single_thread_task_runner.h"
diff --git a/content/browser/preloading/prerender/prerender_host.cc b/content/browser/preloading/prerender/prerender_host.cc
index 8e4d2198..5185918 100644
--- a/content/browser/preloading/prerender/prerender_host.cc
+++ b/content/browser/preloading/prerender/prerender_host.cc
@@ -46,6 +46,10 @@
 #include "third_party/blink/public/common/navigation/preloading_headers.h"
 #include "url/origin.h"
 
+#if BUILDFLAG(IS_ANDROID)
+#include "base/strings/stringprintf.h"
+#endif
+
 namespace content {
 
 namespace {
diff --git a/content/browser/process_internals/process_internals_handler_impl.cc b/content/browser/process_internals/process_internals_handler_impl.cc
index 8b22cb0..88d4079 100644
--- a/content/browser/process_internals/process_internals_handler_impl.cc
+++ b/content/browser/process_internals/process_internals_handler_impl.cc
@@ -10,6 +10,7 @@
 #include <vector>
 
 #include "base/strings/strcat.h"
+#include "base/strings/stringprintf.h"
 #include "content/browser/child_process_security_policy_impl.h"
 #include "content/browser/process_internals/process_internals.mojom.h"
 #include "content/browser/process_lock.h"
diff --git a/content/browser/renderer_host/browsing_context_group_swap_browsertest.cc b/content/browser/renderer_host/browsing_context_group_swap_browsertest.cc
index 269b3f3..0b255470 100644
--- a/content/browser/renderer_host/browsing_context_group_swap_browsertest.cc
+++ b/content/browser/renderer_host/browsing_context_group_swap_browsertest.cc
@@ -6,6 +6,7 @@
 
 #include <optional>
 
+#include "base/strings/stringprintf.h"
 #include "base/test/scoped_feature_list.h"
 #include "content/browser/renderer_host/navigation_request.h"
 #include "content/public/browser/web_contents_observer.h"
diff --git a/content/browser/renderer_host/input/composited_scrolling_browsertest.cc b/content/browser/renderer_host/input/composited_scrolling_browsertest.cc
index 2b9da97..93ca5937 100644
--- a/content/browser/renderer_host/input/composited_scrolling_browsertest.cc
+++ b/content/browser/renderer_host/input/composited_scrolling_browsertest.cc
@@ -9,6 +9,7 @@
 #include "base/functional/bind.h"
 #include "base/numerics/angle_conversions.h"
 #include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
diff --git a/content/browser/renderer_host/navigation_controller_history_intervention_browsertest.cc b/content/browser/renderer_host/navigation_controller_history_intervention_browsertest.cc
index ced39b47..ca0c127 100644
--- a/content/browser/renderer_host/navigation_controller_history_intervention_browsertest.cc
+++ b/content/browser/renderer_host/navigation_controller_history_intervention_browsertest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/strings/stringprintf.h"
 #include "base/test/scoped_feature_list.h"
 #include "content/browser/renderer_host/frame_tree.h"
 #include "content/browser/renderer_host/navigation_controller_impl.h"
diff --git a/content/browser/renderer_host/navigation_transitions/navigation_entry_screenshot_browsertest.cc b/content/browser/renderer_host/navigation_transitions/navigation_entry_screenshot_browsertest.cc
index 6b7d7d2..e9f48ae 100644
--- a/content/browser/renderer_host/navigation_transitions/navigation_entry_screenshot_browsertest.cc
+++ b/content/browser/renderer_host/navigation_transitions/navigation_entry_screenshot_browsertest.cc
@@ -1201,6 +1201,12 @@
 // Regression test for https://crbug.com/368289857.
 IN_PROC_BROWSER_TEST_P(NavigationEntryScreenshotBrowserTest,
                        NavigateWhileHidden_NotCaptured) {
+  // TODO(crbug.com/390571607): Update this test to support default
+  // SiteInstanceGroup in all parameterization modes.
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    GTEST_SKIP();
+  }
+
   const size_t page_size = GetUncompressedScreenshotSizeInBytes();
   const size_t memory_budget = 10 * page_size;
   auto* manager = GetManagerForTab(web_contents());
diff --git a/content/browser/renderer_host/proactively_swap_browsing_instances_browsertest.cc b/content/browser/renderer_host/proactively_swap_browsing_instances_browsertest.cc
index af7d50e..601f925 100644
--- a/content/browser/renderer_host/proactively_swap_browsing_instances_browsertest.cc
+++ b/content/browser/renderer_host/proactively_swap_browsing_instances_browsertest.cc
@@ -274,14 +274,20 @@
           web_contents->GetPrimaryMainFrame()->GetSiteInstance());
 
   // Check that A and B are in different BrowsingInstances and renderer
-  // process. Without full site isolation, A and B are both default
-  // SiteInstances of different BrowsingInstances.
+  // processes. Without full site isolation, A and B are either both default
+  // SiteInstances or in the default SiteInstanceGroup of different
+  // BrowsingInstances.
   EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
   EXPECT_NE(a_site_instance->GetProcess(), b_site_instance->GetProcess());
-  EXPECT_EQ(!AreAllSitesIsolatedForTesting(),
-            a_site_instance->IsDefaultSiteInstance());
-  EXPECT_EQ(!AreAllSitesIsolatedForTesting(),
-            b_site_instance->IsDefaultSiteInstance());
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_EQ(a_site_instance->group(),
+              a_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+    EXPECT_EQ(b_site_instance->group(),
+              b_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_TRUE(a_site_instance->IsDefaultSiteInstance());
+    EXPECT_TRUE(b_site_instance->IsDefaultSiteInstance());
+  }
 }
 
 // Different from renderer-initiated cross-site navigations, browser-initiated
@@ -311,14 +317,20 @@
           web_contents->GetPrimaryMainFrame()->GetSiteInstance());
 
   // Check that A and B are in different BrowsingInstances and renderer
-  // processes. Without full site isolation, A and B are both default
-  // SiteInstances of different BrowsingInstances.
+  // processes. Without full site isolation, A and B are either both default
+  // SiteInstances or in the default SiteInstanceGroup of different
+  // BrowsingInstances.
   EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
   EXPECT_NE(a_site_instance->GetProcess(), b_site_instance->GetProcess());
-  EXPECT_EQ(!AreAllSitesIsolatedForTesting(),
-            a_site_instance->IsDefaultSiteInstance());
-  EXPECT_EQ(!AreAllSitesIsolatedForTesting(),
-            b_site_instance->IsDefaultSiteInstance());
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_EQ(a_site_instance->group(),
+              a_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+    EXPECT_EQ(b_site_instance->group(),
+              b_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_TRUE(a_site_instance->IsDefaultSiteInstance());
+    EXPECT_TRUE(b_site_instance->IsDefaultSiteInstance());
+  }
 }
 
 // A test ContentBrowserClient implementation that enforce process-per-site mode
@@ -380,10 +392,15 @@
   // processes.
   EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
   EXPECT_NE(b_site_instance->GetProcess(), original_process);
-  EXPECT_EQ(!AreAllSitesIsolatedForTesting(),
-            a_site_instance->IsDefaultSiteInstance());
-  EXPECT_EQ(!AreAllSitesIsolatedForTesting(),
-            b_site_instance->IsDefaultSiteInstance());
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_EQ(a_site_instance->group(),
+              a_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+    EXPECT_EQ(b_site_instance->group(),
+              b_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_TRUE(a_site_instance->IsDefaultSiteInstance());
+    EXPECT_TRUE(b_site_instance->IsDefaultSiteInstance());
+  }
 
   // Make sure we will use process-per-site for C.
   // Note this is enforcing process-per-site for all sites, which is why we turn
@@ -400,8 +417,12 @@
   // Check that B and C are in different BrowsingInstances and renderer
   // processes.
   EXPECT_FALSE(b_site_instance->IsRelatedSiteInstance(c_site_instance.get()));
-  EXPECT_EQ(!AreAllSitesIsolatedForTesting(),
-            c_site_instance->IsDefaultSiteInstance());
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_EQ(c_site_instance->group(),
+              c_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_TRUE(c_site_instance->IsDefaultSiteInstance());
+  }
   EXPECT_NE(c_site_instance->GetProcess(), original_process);
   // C is using the process for C's site.
   EXPECT_EQ(c_site_instance->GetProcess(),
@@ -419,8 +440,12 @@
           web_contents->GetPrimaryMainFrame()->GetSiteInstance());
   EXPECT_FALSE(b2_site_instance->IsRelatedSiteInstance(c_site_instance.get()));
   EXPECT_FALSE(b2_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
-  EXPECT_EQ(!AreAllSitesIsolatedForTesting(),
-            b2_site_instance->IsDefaultSiteInstance());
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_EQ(b2_site_instance->group(),
+              b2_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_TRUE(b2_site_instance->IsDefaultSiteInstance());
+  }
   EXPECT_NE(b2_site_instance->GetProcess(), original_process);
   // Check that B and C are in different renderer processes.
   EXPECT_NE(b2_site_instance->GetProcess(), c_site_instance->GetProcess());
@@ -473,10 +498,15 @@
   // Check that A and B are in different BrowsingInstances but B should use the
   // sole process assigned to site B.
   EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
-  EXPECT_EQ(!AreAllSitesIsolatedForTesting(),
-            a_site_instance->IsDefaultSiteInstance());
-  EXPECT_EQ(!AreAllSitesIsolatedForTesting(),
-            b_site_instance->IsDefaultSiteInstance());
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_EQ(a_site_instance->group(),
+              a_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+    EXPECT_EQ(b_site_instance->group(),
+              b_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_TRUE(a_site_instance->IsDefaultSiteInstance());
+    EXPECT_TRUE(b_site_instance->IsDefaultSiteInstance());
+  }
   EXPECT_NE(b_site_instance->GetProcess(), original_process);
   EXPECT_EQ(b_site_instance->GetProcess(), process_for_b);
   EXPECT_EQ(b_site_instance->GetProcess(),
@@ -1302,7 +1332,7 @@
   // same renderer process.
   // If site isolation is turned off, it will hit the case at crbug.com/1094147.
   EXPECT_FALSE(site_instance_2->IsRelatedSiteInstance(site_instance_3.get()));
-  if (AreAllSitesIsolatedForTesting()) {
+  if (AreStrictSiteInstancesEnabled()) {
     EXPECT_EQ(site_instance_2->GetProcess(), site_instance_3->GetProcess());
   } else {
     EXPECT_NE(site_instance_2->GetProcess(), site_instance_3->GetProcess());
diff --git a/content/browser/renderer_host/render_frame_host_impl_browsertest.cc b/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
index 6319452..03abd477 100644
--- a/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
+++ b/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
@@ -8604,7 +8604,9 @@
                                         ->RequiresDedicatedProcess();
 
   // `shell()` and `second_shell` opened different sites.
-  if (requires_dedicated_process) {
+  // TODO(crbug.com/419469455): Make sure metrics are updated correctly with the
+  // introduction of default SiteInstanceGroup.
+  if (requires_dedicated_process || ShouldUseDefaultSiteInstanceGroup()) {
     EXPECT_THAT(histogram.GetAllSamples(
                     "SiteIsolation."
                     "NewProcessUsedForNavigationWhenSameSiteProcessExists"),
@@ -8618,7 +8620,7 @@
 
   ASSERT_TRUE(NavigateToURL(second_shell, url));
   // Now `shell()` and `second_shell` opened the same site.
-  if (requires_dedicated_process) {
+  if (requires_dedicated_process || ShouldUseDefaultSiteInstanceGroup()) {
     EXPECT_THAT(
         histogram.GetAllSamples(
             "SiteIsolation."
diff --git a/content/browser/renderer_host/render_frame_host_manager.cc b/content/browser/renderer_host/render_frame_host_manager.cc
index 68bc5be..a194fbb 100644
--- a/content/browser/renderer_host/render_frame_host_manager.cc
+++ b/content/browser/renderer_host/render_frame_host_manager.cc
@@ -3602,7 +3602,10 @@
   // does not yet support OOPIFs (https://crbug.com/1101214).
   // TODO(crbug.com/40704573): Remove this block when default
   // SiteInstances support file: URLs.
-  if (!frame_tree_node_->IsMainFrame()) {
+  // TODO(crbug.com/419595581): Make sure default SiteInstanceGroup is safe for
+  // Android WebView before enabling experiments on that platform.
+  if (!frame_tree_node_->IsMainFrame() &&
+      !ShouldUseDefaultSiteInstanceGroup()) {
     RenderFrameHostImpl* parent = frame_tree_node_->parent();
     auto& parent_isolation_context =
         parent->GetSiteInstance()->GetIsolationContext();
diff --git a/content/browser/renderer_host/render_process_host_unittest.cc b/content/browser/renderer_host/render_process_host_unittest.cc
index 041ec18..61ff38dc 100644
--- a/content/browser/renderer_host/render_process_host_unittest.cc
+++ b/content/browser/renderer_host/render_process_host_unittest.cc
@@ -1391,10 +1391,13 @@
     refuse_reason_ = reason;
   }
 
-  std::optional<SpareProcessRefusedByEmbedderReason>
-  ShouldUseSpareRenderProcessHost(BrowserContext* browser_context,
-                                  const GURL& site_url) override {
-    return refuse_reason_;
+  bool ShouldUseSpareRenderProcessHost(
+      BrowserContext* browser_context,
+      const GURL& site_url,
+      std::optional<SpareProcessRefusedByEmbedderReason>& refused_reason)
+      override {
+    refused_reason = refuse_reason_;
+    return false;
   }
 
  private:
diff --git a/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc b/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
index 1d741f0..6453ca21 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
@@ -265,6 +265,7 @@
       "</body>"
       "</html>");
   EXPECT_TRUE(NavigateToURL(shell(), page));
+  SimulateEndOfPaintHoldingOnPrimaryMainFrame(shell()->web_contents());
 
   auto* wc = shell()->web_contents();
   ASSERT_TRUE(ExecJs(wc, "focusSelectMenu();"));
@@ -465,6 +466,8 @@
       "</html>");
 
   EXPECT_TRUE(NavigateToURL(shell(), page));
+  SimulateEndOfPaintHoldingOnPrimaryMainFrame(shell()->web_contents());
+
   auto* wc = shell()->web_contents();
   Attach();
   SendCommandSync("Debugger.enable");
diff --git a/content/browser/renderer_host/scroll_into_view_browsertest.cc b/content/browser/renderer_host/scroll_into_view_browsertest.cc
index b9bb46d..f12cc12c 100644
--- a/content/browser/renderer_host/scroll_into_view_browsertest.cc
+++ b/content/browser/renderer_host/scroll_into_view_browsertest.cc
@@ -7,6 +7,7 @@
 
 #include "base/json/json_reader.h"
 #include "base/strings/strcat.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/values.h"
diff --git a/content/browser/renderer_host/spare_render_process_host_manager_browsertest.cc b/content/browser/renderer_host/spare_render_process_host_manager_browsertest.cc
index 4010947..67a1250 100644
--- a/content/browser/renderer_host/spare_render_process_host_manager_browsertest.cc
+++ b/content/browser/renderer_host/spare_render_process_host_manager_browsertest.cc
@@ -536,10 +536,13 @@
     return true;
   }
 
-  std::optional<SpareProcessRefusedByEmbedderReason>
-  ShouldUseSpareRenderProcessHost(BrowserContext* browser_context,
-                                  const GURL& site_url) override {
-    return SpareProcessRefusedByEmbedderReason::DefaultDisabled;
+  bool ShouldUseSpareRenderProcessHost(
+      BrowserContext* browser_context,
+      const GURL& site_url,
+      std::optional<SpareProcessRefusedByEmbedderReason>& refused_reason)
+      override {
+    refused_reason = std::nullopt;
+    return false;
   }
 };
 
diff --git a/content/browser/renderer_host/spare_render_process_host_manager_impl.cc b/content/browser/renderer_host/spare_render_process_host_manager_impl.cc
index 3e3c20a2..0591c576 100644
--- a/content/browser/renderer_host/spare_render_process_host_manager_impl.cc
+++ b/content/browser/renderer_host/spare_render_process_host_manager_impl.cc
@@ -659,10 +659,11 @@
   // ShouldUseSpareRenderProcessHost starts covering non-process-per-site
   // scenarios).
   std::optional<ContentBrowserClient::SpareProcessRefusedByEmbedderReason>
-      refuse_reason =
-          GetContentClient()->browser()->ShouldUseSpareRenderProcessHost(
-              browser_context, site_instance->GetSiteInfo().site_url());
-  if (refuse_reason.has_value()) {
+      refuse_reason;
+  if (!GetContentClient()->browser()->ShouldUseSpareRenderProcessHost(
+          browser_context, site_instance->GetSiteInfo().site_url(),
+          refuse_reason)) {
+    CHECK(refuse_reason.has_value());
     return refuse_reason;
   }
 
diff --git a/content/browser/renderer_host/unassigned_site_instance_browsertest.cc b/content/browser/renderer_host/unassigned_site_instance_browsertest.cc
index a7ef8cfe..863b1ac2 100644
--- a/content/browser/renderer_host/unassigned_site_instance_browsertest.cc
+++ b/content/browser/renderer_host/unassigned_site_instance_browsertest.cc
@@ -7,6 +7,7 @@
 
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/test/scoped_feature_list.h"
 #include "content/browser/process_lock.h"
diff --git a/content/browser/security/coop/cross_origin_opener_policy_browsertest.cc b/content/browser/security/coop/cross_origin_opener_policy_browsertest.cc
index a743620..beec338d 100644
--- a/content/browser/security/coop/cross_origin_opener_policy_browsertest.cc
+++ b/content/browser/security/coop/cross_origin_opener_policy_browsertest.cc
@@ -4,6 +4,7 @@
 
 #include "base/command_line.h"
 #include "base/strings/escape.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/test/gtest_util.h"
 #include "base/test/scoped_feature_list.h"
@@ -284,14 +285,20 @@
 // Android to limit the number of processes. Testing these particularities of
 // the process model and their interaction with cross-origin isolation requires
 // to disable SiteIsolation.
+// TODO(crbug.com/390571607): Remove this version of the test in favour of the
+// SiteInstanceGroup version below once default SiteInstanceGroups are
+// enabled by default.
 class NoSiteIsolationCrossOriginIsolationBrowserTest
     : public CrossOriginOpenerPolicyBrowserTest {
  public:
   NoSiteIsolationCrossOriginIsolationBrowserTest() {
     // Disable the heuristic to isolate COOP pages from the default
-    // SiteInstance. This is otherwise on by default on Android.
+    // SiteInstance. This is otherwise on by default on Android. Disable default
+    // SiteInstanceGroups, as that's covered in the SiteInstanceGroup version of
+    // this test.
     feature_list_.InitWithFeatures(
-        {}, {features::kSiteIsolationForCrossOriginOpenerPolicy});
+        {}, {features::kSiteIsolationForCrossOriginOpenerPolicy,
+             features::kDefaultSiteInstanceGroups});
   }
 
   void SetUpOnMainThread() override {
@@ -332,6 +339,20 @@
   base::test::ScopedFeatureList feature_list_;
 };
 
+// This test is the same as NoSiteIsolationCrossOriginIsolationBrowserTest,
+// except it enables and uses default SiteInstanceGroup instead of default
+// SiteInstance.
+class DefaultSiteInstanceGroupCrossOriginIsolationBrowserTest
+    : public NoSiteIsolationCrossOriginIsolationBrowserTest {
+ public:
+  DefaultSiteInstanceGroupCrossOriginIsolationBrowserTest() {
+    feature_list_.InitWithFeatures({features::kDefaultSiteInstanceGroups}, {});
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
 using VirtualBrowsingContextGroupTest = CrossOriginOpenerPolicyBrowserTest;
 using SoapByDefaultVirtualBrowsingContextGroupTest =
     CrossOriginOpenerPolicyBrowserTest;
@@ -4325,6 +4346,11 @@
                          NoSiteIsolationCrossOriginIsolationBrowserTest,
                          kTestParams,
                          CrossOriginOpenerPolicyBrowserTest::DescribeParams);
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    DefaultSiteInstanceGroupCrossOriginIsolationBrowserTest,
+    kTestParams,
+    CrossOriginOpenerPolicyBrowserTest::DescribeParams);
 INSTANTIATE_TEST_SUITE_P(All,
                          ProcessReuseOnPrerenderCOOPSwapBrowserTest,
                          kTestParams,
@@ -5221,6 +5247,64 @@
   }
 }
 
+IN_PROC_BROWSER_TEST_P(DefaultSiteInstanceGroupCrossOriginIsolationBrowserTest,
+                       COICanLiveInDefaultSiteInstanceGroup) {
+  GURL isolated_page(
+      https_server()->GetURL("a.test",
+                             "/set-header"
+                             "?cross-origin-opener-policy: same-origin"
+                             "&cross-origin-embedder-policy: require-corp"));
+  GURL non_isolated_page(https_server()->GetURL("a.test", "/title1.html"));
+
+  EXPECT_TRUE(NavigateToURL(shell(), isolated_page));
+  SiteInstanceImpl* main_frame_si = current_frame_host()->GetSiteInstance();
+  EXPECT_TRUE(main_frame_si->IsCrossOriginIsolated());
+  EXPECT_EQ(main_frame_si->group(),
+            main_frame_si->DefaultSiteInstanceGroupForBrowsingInstance());
+
+  {
+    // Open a popup to a page with similar isolation. Pages that have compatible
+    // cross origin isolation should be put in the same default
+    // SiteInstanceGroup.
+    ShellAddedObserver shell_observer;
+    EXPECT_TRUE(ExecJs(current_frame_host(),
+                       JsReplace("window.open($1);", isolated_page)));
+    WebContentsImpl* popup = static_cast<WebContentsImpl*>(
+        shell_observer.GetShell()->web_contents());
+    EXPECT_TRUE(WaitForLoadStop(popup));
+
+    SiteInstanceImpl* popup_si =
+        popup->GetPrimaryMainFrame()->GetSiteInstance();
+    EXPECT_TRUE(popup_si->IsCrossOriginIsolated());
+    EXPECT_EQ(popup_si->group(),
+              popup_si->DefaultSiteInstanceGroupForBrowsingInstance());
+    EXPECT_EQ(popup_si, main_frame_si);
+
+    popup->Close();
+  }
+
+  {
+    // Open a popup to a same origin non-isolated page. This page should live in
+    // a different BrowsingInstance in the default non-isolated
+    // SiteInstanceGroup.
+    ShellAddedObserver shell_observer;
+    EXPECT_TRUE(ExecJs(current_frame_host(),
+                       JsReplace("window.open($1);", non_isolated_page)));
+    WebContentsImpl* popup = static_cast<WebContentsImpl*>(
+        shell_observer.GetShell()->web_contents());
+    EXPECT_TRUE(WaitForLoadStop(popup));
+
+    SiteInstanceImpl* popup_si =
+        popup->GetPrimaryMainFrame()->GetSiteInstance();
+    EXPECT_FALSE(popup_si->IsCrossOriginIsolated());
+    EXPECT_EQ(popup_si->group(),
+              popup_si->DefaultSiteInstanceGroupForBrowsingInstance());
+    EXPECT_NE(popup_si, main_frame_si);
+
+    popup->Close();
+  }
+}
+
 IN_PROC_BROWSER_TEST_P(CrossOriginOpenerPolicyBrowserTest,
                        ConsoleErrorOnWindowLocationAccess) {
   const GURL non_coop_page = https_server()->GetURL("a.test", "/title1.html");
diff --git a/content/browser/security/dip/document_isolation_policy_browsertest.cc b/content/browser/security/dip/document_isolation_policy_browsertest.cc
index bbc743f..025fbeb9 100644
--- a/content/browser/security/dip/document_isolation_policy_browsertest.cc
+++ b/content/browser/security/dip/document_isolation_policy_browsertest.cc
@@ -6,6 +6,7 @@
 
 #include "base/command_line.h"
 #include "base/strings/escape.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/test/gtest_util.h"
 #include "base/test/scoped_feature_list.h"
diff --git a/content/browser/service_host/service_process_host_impl.cc b/content/browser/service_host/service_process_host_impl.cc
index 96c9563..d9c14f9 100644
--- a/content/browser/service_host/service_process_host_impl.cc
+++ b/content/browser/service_host/service_process_host_impl.cc
@@ -39,35 +39,42 @@
 // TODO(crbug.com/40633267): Once UtilityProcessHost is used only by service
 // processes, its logic can be inlined here.
 void LaunchServiceProcess(mojo::GenericPendingReceiver receiver,
-                          ServiceProcessHost::Options options,
+                          ServiceProcessHost::Options service_options,
                           sandbox::mojom::Sandbox sandbox) {
-  UtilityProcessHost* host =
-      new UtilityProcessHost(std::make_unique<UtilityProcessClient>(
-          *receiver.interface_name(), options.site,
-          std::move(options.process_callback)));
-  host->SetName(!options.display_name.empty()
-                    ? options.display_name
-                    : base::UTF8ToUTF16(*receiver.interface_name()));
-  host->SetMetricsName(*receiver.interface_name());
   if (!ShouldEnableSandbox(sandbox)) {
     sandbox = sandbox::mojom::Sandbox::kNoSandbox;
   }
-  host->SetSandboxType(sandbox);
-  host->SetExtraCommandLineSwitches(std::move(options.extra_switches));
-  if (options.child_flags) {
-    host->set_child_flags(*options.child_flags);
+  UtilityProcessHost::Options utility_options;
+
+  const auto service_interface_name = receiver.interface_name().value();
+
+  utility_options
+      .WithName(!service_options.display_name.empty()
+                    ? service_options.display_name
+                    : base::UTF8ToUTF16(service_interface_name))
+      .WithMetricsName(service_interface_name)
+      .WithSandboxType(sandbox)
+      .WithExtraCommandLineSwitches(std::move(service_options.extra_switches));
+
+  if (service_options.child_flags) {
+    utility_options.WithChildFlags(*service_options.child_flags);
   }
 #if BUILDFLAG(IS_WIN)
-  if (!options.preload_libraries.empty()) {
-    host->SetPreloadLibraries(options.preload_libraries);
+  if (!service_options.preload_libraries.empty()) {
+    utility_options.WithPreloadLibraries(service_options.preload_libraries);
   }
 #endif  // BUILDFLAG(IS_WIN)
-  if (options.allow_gpu_client.has_value() &&
-      options.allow_gpu_client.value()) {
-    host->SetAllowGpuClient();
+  if (service_options.allow_gpu_client.has_value() &&
+      service_options.allow_gpu_client.value()) {
+    utility_options.WithGpuClientAllowed();
   }
-  host->Start();
-  host->GetChildProcess()->BindServiceInterface(std::move(receiver));
+
+  utility_options.WithBoundServiceInterfaceOnChildProcess(std::move(receiver));
+
+  UtilityProcessHost::Start(std::move(utility_options),
+                            std::make_unique<UtilityProcessClient>(
+                                service_interface_name, service_options.site,
+                                std::move(service_options.process_callback)));
 }
 
 }  // namespace
diff --git a/content/browser/service_host/utility_process_host.cc b/content/browser/service_host/utility_process_host.cc
index e2c72b43..7db71d2 100644
--- a/content/browser/service_host/utility_process_host.cc
+++ b/content/browser/service_host/utility_process_host.cc
@@ -148,21 +148,30 @@
   g_utility_main_thread_factory = create;
 }
 
-UtilityProcessHost::UtilityProcessHost()
-    : UtilityProcessHost(nullptr /* client */) {}
-
-UtilityProcessHost::UtilityProcessHost(std::unique_ptr<Client> client)
+UtilityProcessHost::Options::Options()
     : sandbox_type_(sandbox::mojom::Sandbox::kUtility),
 #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
       child_flags_(ChildProcessHost::CHILD_ALLOW_SELF),
 #else
       child_flags_(ChildProcessHost::CHILD_NORMAL),
 #endif
-      started_(false),
-      name_(u"utility process"),
-      file_data_(std::make_unique<ChildProcessLauncherFileData>()),
 #if BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
       allowed_gpu_(false),
+#endif  // BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
+      file_data_(std::make_unique<ChildProcessLauncherFileData>()),
+      name_(u"utility process") {
+}
+
+UtilityProcessHost::Options::~Options() = default;
+
+UtilityProcessHost::Options& UtilityProcessHost::Options::operator=(
+    UtilityProcessHost::Options&&) = default;
+UtilityProcessHost::Options::Options(Options&&) = default;
+
+UtilityProcessHost::UtilityProcessHost(Options options,
+                                       std::unique_ptr<Client> client)
+    : options_(std::move(options)),
+#if BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
       gpu_client_(nullptr, base::OnTaskRunnerDeleter(nullptr)),
 #endif  // BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
       client_(std::move(client)) {
@@ -178,82 +187,132 @@
   }
 }
 
-base::WeakPtr<UtilityProcessHost> UtilityProcessHost::AsWeakPtr() {
-  return weak_ptr_factory_.GetWeakPtr();
-}
-
-void UtilityProcessHost::SetSandboxType(sandbox::mojom::Sandbox sandbox_type) {
+UtilityProcessHost::Options& UtilityProcessHost::Options::WithSandboxType(
+    sandbox::mojom::Sandbox sandbox_type) {
   sandbox_type_ = sandbox_type;
+  return *this;
 }
 
-const ChildProcessData& UtilityProcessHost::GetData() {
-  return process_->GetData();
-}
-
-#if BUILDFLAG(IS_POSIX)
-void UtilityProcessHost::SetEnv(const base::EnvironmentMap& env) {
-  env_ = env;
-}
-#endif
-
-bool UtilityProcessHost::Start() {
-  return StartProcess();
-}
-
-void UtilityProcessHost::SetMetricsName(const std::string& metrics_name) {
-  metrics_name_ = metrics_name;
-}
-
-void UtilityProcessHost::SetName(const std::u16string& name) {
+UtilityProcessHost::Options& UtilityProcessHost::Options::WithName(
+    const std::u16string& name) {
   name_ = name;
+  return *this;
 }
 
-void UtilityProcessHost::SetExtraCommandLineSwitches(
+UtilityProcessHost::Options& UtilityProcessHost::Options::WithMetricsName(
+    const std::string& metrics_name) {
+  metrics_name_ = metrics_name;
+  return *this;
+}
+
+UtilityProcessHost::Options& UtilityProcessHost::Options::WithChildFlags(
+    int flags) {
+  child_flags_ = flags;
+  return *this;
+}
+
+UtilityProcessHost::Options&
+UtilityProcessHost::Options::WithExtraCommandLineSwitches(
     std::vector<std::string> switches) {
   extra_switches_ = std::move(switches);
+  return *this;
 }
 
 #if BUILDFLAG(IS_WIN)
-void UtilityProcessHost::SetPreloadLibraries(
+UtilityProcessHost::Options& UtilityProcessHost::Options::WithPreloadLibraries(
     const std::vector<base::FilePath>& preloads) {
   preload_libraries_ = preloads;
+  return *this;
 }
 #endif  // BUILDFLAG(IS_WIN)
 
-void UtilityProcessHost::SetAllowGpuClient() {
+UtilityProcessHost::Options&
+UtilityProcessHost::Options::WithGpuClientAllowed() {
 #if BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
   allowed_gpu_ = true;
 #endif  // BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
+  return *this;
 }
 
 #if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_MAC)
-void UtilityProcessHost::AddFileToPreload(
+UtilityProcessHost::Options& UtilityProcessHost::Options::WithFileToPreload(
     std::string key,
     std::variant<base::FilePath, base::ScopedFD> file) {
   DCHECK_EQ(file_data_->files_to_preload.count(key), 0u);
   file_data_->files_to_preload.insert({std::move(key), std::move(file)});
+  return *this;
 }
 #endif  // BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_MAC)
 
+#if BUILDFLAG(IS_POSIX)
+UtilityProcessHost::Options& UtilityProcessHost::Options::WithEnvironment(
+    const base::EnvironmentMap& env) {
+  env_ = env;
+  return *this;
+}
+#endif
+
 #if BUILDFLAG(USE_ZYGOTE)
-void UtilityProcessHost::SetZygoteForTesting(ZygoteCommunication* handle) {
+UtilityProcessHost::Options& UtilityProcessHost::Options::WithZygoteForTesting(
+    ZygoteCommunication* handle) {
   zygote_for_testing_ = handle;
+  return *this;
 }
 #endif  // BUILDFLAG(USE_ZYGOTE)
 
+UtilityProcessHost::Options&
+UtilityProcessHost::Options::WithBoundReceiverOnChildProcessForTesting(
+    mojo::GenericPendingReceiver receiver) {
+  CHECK(!receiver_to_bind_.has_value()) << "Can only bind one receiver.";
+  receiver_to_bind_.emplace(std::move(receiver));
+  return *this;
+}
+
+UtilityProcessHost::Options&
+UtilityProcessHost::Options::WithBoundServiceInterfaceOnChildProcess(
+    mojo::GenericPendingReceiver receiver) {
+  CHECK(!service_interface_to_bind_.has_value())
+      << "Can only bind one service interface.";
+  service_interface_to_bind_.emplace(std::move(receiver));
+  return *this;
+}
+
+UtilityProcessHost::Options UtilityProcessHost::Options::Pass() {
+  return std::move(*this);
+}
+
+// static
+bool UtilityProcessHost::Start(Options options,
+                               std::unique_ptr<Client> client) {
+  UtilityProcessHost* host =
+      new UtilityProcessHost(std::move(options), std::move(client));
+  if (!host->StartProcess()) {
+    return false;
+  }
+
+  host->MaybeBindMojoInterfaces();
+
+  return true;
+}
+
 mojom::ChildProcess* UtilityProcessHost::GetChildProcess() {
   return static_cast<ChildProcessHostImpl*>(process_->GetHost())
       ->child_process();
 }
 
-bool UtilityProcessHost::StartProcess() {
-  if (started_) {
-    return true;
+void UtilityProcessHost::MaybeBindMojoInterfaces() {
+  if (options_.receiver_to_bind_) {
+    GetChildProcess()->BindReceiver(std::move(*options_.receiver_to_bind_));
   }
+  if (options_.service_interface_to_bind_) {
+    GetChildProcess()->BindServiceInterface(
+        std::move(*options_.service_interface_to_bind_));
+  }
+}
 
-  started_ = true;
-  process_->SetName(name_);
-  process_->SetMetricsName(metrics_name_);
+bool UtilityProcessHost::StartProcess() {
+  process_->SetName(options_.name_);
+  process_->SetMetricsName(options_.metrics_name_);
 
   if (RenderProcessHost::run_renderer_in_process()) {
     DCHECK(g_utility_main_thread_factory);
@@ -263,218 +322,223 @@
         InProcessChildThreadParams(GetIOThreadTaskRunner({}),
                                    process_->GetInProcessMojoInvitation())));
     in_process_thread_->Start();
-  } else {
-    const base::CommandLine& browser_command_line =
-        *base::CommandLine::ForCurrentProcess();
+    return true;
+  }
 
-    bool has_cmd_prefix =
-        browser_command_line.HasSwitch(switches::kUtilityCmdPrefix);
+  const base::CommandLine& browser_command_line =
+      *base::CommandLine::ForCurrentProcess();
+
+  bool has_cmd_prefix =
+      browser_command_line.HasSwitch(switches::kUtilityCmdPrefix);
 
 #if BUILDFLAG(IS_ANDROID)
-    // readlink("/prof/self/exe") sometimes fails on Android at startup.
-    // As a workaround skip calling it here, since the executable name is
-    // not needed on Android anyway. See crbug.com/500854.
-    std::unique_ptr<base::CommandLine> cmd_line =
-        std::make_unique<base::CommandLine>(base::CommandLine::NO_PROGRAM);
-    if (metrics_name_ == network::mojom::NetworkService::Name_ &&
-        base::FeatureList::IsEnabled(features::kWarmUpNetworkProcess)) {
-      process_->EnableWarmUpConnection();
-    }
+  // readlink("/prof/self/exe") sometimes fails on Android at startup.
+  // As a workaround skip calling it here, since the executable name is
+  // not needed on Android anyway. See crbug.com/500854.
+  std::unique_ptr<base::CommandLine> cmd_line =
+      std::make_unique<base::CommandLine>(base::CommandLine::NO_PROGRAM);
+  if (options_.metrics_name_ == network::mojom::NetworkService::Name_ &&
+      base::FeatureList::IsEnabled(features::kWarmUpNetworkProcess)) {
+    process_->EnableWarmUpConnection();
+  }
 #else  // BUILDFLAG(IS_ANDROID)
 #if BUILDFLAG(IS_MAC)
-    if (sandbox_type_ == sandbox::mojom::Sandbox::kServiceWithJit) {
-      DCHECK_EQ(child_flags_, ChildProcessHost::CHILD_RENDERER);
-    }
+  if (options_.sandbox_type_ == sandbox::mojom::Sandbox::kServiceWithJit) {
+    DCHECK_EQ(options_.child_flags_, ChildProcessHost::CHILD_RENDERER);
+  }
 #endif  // BUILDFLAG(IS_MAC)
-    int child_flags = child_flags_;
+  int child_flags = options_.child_flags_;
 
-    // When running under gdb, forking /proc/self/exe ends up forking the gdb
-    // executable instead of Chromium. It is almost safe to assume that no
-    // updates will happen while a developer is running with
-    // |switches::kUtilityCmdPrefix|. See ChildProcessHost::GetChildPath() for
-    // a similar case with Valgrind.
-    if (has_cmd_prefix) {
-      child_flags = ChildProcessHost::CHILD_NORMAL;
-    }
+  // When running under gdb, forking /proc/self/exe ends up forking the gdb
+  // executable instead of Chromium. It is almost safe to assume that no
+  // updates will happen while a developer is running with
+  // |switches::kUtilityCmdPrefix|. See ChildProcessHost::GetChildPath() for
+  // a similar case with Valgrind.
+  if (has_cmd_prefix) {
+    child_flags = ChildProcessHost::CHILD_NORMAL;
+  }
 
-    base::FilePath exe_path = ChildProcessHost::GetChildPath(child_flags);
-    if (exe_path.empty()) {
-      NOTREACHED() << "Unable to get utility process binary name.";
-    }
+  base::FilePath exe_path = ChildProcessHost::GetChildPath(child_flags);
+  if (exe_path.empty()) {
+    NOTREACHED() << "Unable to get utility process binary name.";
+  }
 
-    std::unique_ptr<base::CommandLine> cmd_line =
-        std::make_unique<base::CommandLine>(exe_path);
+  std::unique_ptr<base::CommandLine> cmd_line =
+      std::make_unique<base::CommandLine>(exe_path);
 #endif  // BUILDFLAG(IS_ANDROID)
 
-    cmd_line->AppendSwitchASCII(switches::kProcessType,
-                                switches::kUtilityProcess);
-    // Specify the type of utility process for debugging/profiling purposes.
-    cmd_line->AppendSwitchASCII(switches::kUtilitySubType, metrics_name_);
-    std::string locale = GetContentClient()->browser()->GetApplicationLocale();
-    cmd_line->AppendSwitchASCII(switches::kLang, locale);
+  cmd_line->AppendSwitchASCII(switches::kProcessType,
+                              switches::kUtilityProcess);
+  // Specify the type of utility process for debugging/profiling purposes.
+  cmd_line->AppendSwitchASCII(switches::kUtilitySubType,
+                              options_.metrics_name_);
+  std::string locale = GetContentClient()->browser()->GetApplicationLocale();
+  cmd_line->AppendSwitchASCII(switches::kLang, locale);
 
 #if BUILDFLAG(IS_WIN)
-    cmd_line->AppendArgNative(UtilityToAppLaunchPrefetchArg(metrics_name_));
+  cmd_line->AppendArgNative(
+      UtilityToAppLaunchPrefetchArg(options_.metrics_name_));
 #endif  // BUILDFLAG(IS_WIN)
 
-    sandbox::policy::SetCommandLineFlagsForSandboxType(cmd_line.get(),
-                                                       sandbox_type_);
+  sandbox::policy::SetCommandLineFlagsForSandboxType(cmd_line.get(),
+                                                     options_.sandbox_type_);
 
-    // Browser command-line switches to propagate to the utility process.
-    static const char* const kSwitchNames[] = {
-        network::switches::kAdditionalTrustTokenKeyCommitments,
-        network::switches::kForceEffectiveConnectionType,
-        network::switches::kHostResolverRules,
-        network::switches::kIgnoreBadMessageForTesting,
-        network::switches::kIgnoreCertificateErrorsSPKIList,
-        network::switches::kTestThirdPartyCookiePhaseout,
-        network::switches::kDisableSharedDictionaryStorageCleanupForTesting,
-        network::switches::kStoreProbabilisticRevealTokens,
-        sandbox::policy::switches::kNoSandbox,
+  // Browser command-line switches to propagate to the utility process.
+  static const char* const kSwitchNames[] = {
+      network::switches::kAdditionalTrustTokenKeyCommitments,
+      network::switches::kForceEffectiveConnectionType,
+      network::switches::kHostResolverRules,
+      network::switches::kIgnoreBadMessageForTesting,
+      network::switches::kIgnoreCertificateErrorsSPKIList,
+      network::switches::kTestThirdPartyCookiePhaseout,
+      network::switches::kDisableSharedDictionaryStorageCleanupForTesting,
+      network::switches::kStoreProbabilisticRevealTokens,
+      sandbox::policy::switches::kNoSandbox,
 #if BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_CHROMEOS)
-        switches::kDisableDevShmUsage,
+      switches::kDisableDevShmUsage,
 #endif
 #if BUILDFLAG(IS_MAC)
-        sandbox::policy::switches::kDisableMetalShaderCache,
-        sandbox::policy::switches::kEnableSandboxLogging,
+      sandbox::policy::switches::kDisableMetalShaderCache,
+      sandbox::policy::switches::kEnableSandboxLogging,
 #endif
-        switches::kEnableBackgroundThreadPool,
-        switches::kEnableExperimentalCookieFeatures,
-        switches::kForceTextDirection,
-        switches::kForceUIDirection,
-        switches::kIgnoreCertificateErrors,
-        switches::kOverrideUseSoftwareGLForTests,
-        switches::kOverrideEnabledCdmInterfaceVersion,
-        switches::kDisableAcceleratedMjpegDecode,
-        switches::kUseFakeDeviceForMediaStream,
-        switches::kUseFakeMjpegDecodeAccelerator,
-        switches::kUseFileForFakeVideoCapture,
-        switches::kUseMockCertVerifierForTesting,
-        switches::kMockCertVerifierDefaultResultForTesting,
-        switches::kUtilityStartupDialog,
-        switches::kUseANGLE,
-        switches::kUseGL,
-        switches::kEnableExperimentalWebPlatformFeatures,
-        // These flags are used by the audio service:
-        switches::kAudioBufferSize,
-        switches::kDisableAudioInput,
-        switches::kDisableAudioOutput,
-        switches::kFailAudioStreamCreation,
-        switches::kMuteAudio,
-        switches::kUseFileForFakeAudioCapture,
+      switches::kEnableBackgroundThreadPool,
+      switches::kEnableExperimentalCookieFeatures,
+      switches::kForceTextDirection,
+      switches::kForceUIDirection,
+      switches::kIgnoreCertificateErrors,
+      switches::kOverrideUseSoftwareGLForTests,
+      switches::kOverrideEnabledCdmInterfaceVersion,
+      switches::kDisableAcceleratedMjpegDecode,
+      switches::kUseFakeDeviceForMediaStream,
+      switches::kUseFakeMjpegDecodeAccelerator,
+      switches::kUseFileForFakeVideoCapture,
+      switches::kUseMockCertVerifierForTesting,
+      switches::kMockCertVerifierDefaultResultForTesting,
+      switches::kUtilityStartupDialog,
+      switches::kUseANGLE,
+      switches::kUseGL,
+      switches::kEnableExperimentalWebPlatformFeatures,
+      // These flags are used by the audio service:
+      switches::kAudioBufferSize,
+      switches::kDisableAudioInput,
+      switches::kDisableAudioOutput,
+      switches::kFailAudioStreamCreation,
+      switches::kMuteAudio,
+      switches::kUseFileForFakeAudioCapture,
 #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_FREEBSD) || \
     BUILDFLAG(IS_SOLARIS)
-        switches::kAlsaInputDevice,
-        switches::kAlsaOutputDevice,
+      switches::kAlsaInputDevice,
+      switches::kAlsaOutputDevice,
 #endif
 #if BUILDFLAG(USE_CRAS)
-        switches::kUseCras,
+      switches::kUseCras,
 #endif
 #if BUILDFLAG(IS_WIN)
-        switches::kDisableHighResTimer,
-        switches::kEnableExclusiveAudio,
-        switches::kForceWaveAudio,
-        switches::kRaiseTimerFrequency,
-        switches::kTrySupportedChannelLayouts,
-        switches::kWaveOutBuffers,
-        switches::kWebXrForceRuntime,
-        sandbox::policy::switches::kAddXrAppContainerCaps,
+      switches::kDisableHighResTimer,
+      switches::kEnableExclusiveAudio,
+      switches::kForceWaveAudio,
+      switches::kRaiseTimerFrequency,
+      switches::kTrySupportedChannelLayouts,
+      switches::kWaveOutBuffers,
+      switches::kWebXrForceRuntime,
+      sandbox::policy::switches::kAddXrAppContainerCaps,
 #endif
 #if BUILDFLAG(ENABLE_VR)
-        device::switches::kWebXrHandAnonymizationStrategy,
+      device::switches::kWebXrHandAnonymizationStrategy,
 #endif
-        network::switches::kIpAddressSpaceOverrides,
+      network::switches::kIpAddressSpaceOverrides,
 #if BUILDFLAG(IS_CHROMEOS)
-        switches::kSchedulerBoostUrgent,
+      switches::kSchedulerBoostUrgent,
 #endif
-        switches::kFakeBackgroundBlurTogglePeriod,
+      switches::kFakeBackgroundBlurTogglePeriod,
 #if BUILDFLAG(USE_LINUX_VIDEO_ACCELERATION)
-        switches::kHardwareVideoDecodeFrameRate,
+      switches::kHardwareVideoDecodeFrameRate,
 #endif
 #if BUILDFLAG(IS_OZONE)
-        switches::kRenderNodeOverride,
+      switches::kRenderNodeOverride,
 #endif
-    };
-    cmd_line->CopySwitchesFrom(browser_command_line, kSwitchNames);
+  };
+  cmd_line->CopySwitchesFrom(browser_command_line, kSwitchNames);
 
-    network_session_configurator::CopyNetworkSwitches(browser_command_line,
-                                                      cmd_line.get());
+  network_session_configurator::CopyNetworkSwitches(browser_command_line,
+                                                    cmd_line.get());
 
-    if (has_cmd_prefix) {
-      // Launch the utility child process with some prefix
-      // (usually "xterm -e gdb --args").
-      cmd_line->PrependWrapper(browser_command_line.GetSwitchValueNative(
-          switches::kUtilityCmdPrefix));
-    }
+  if (has_cmd_prefix) {
+    // Launch the utility child process with some prefix
+    // (usually "xterm -e gdb --args").
+    cmd_line->PrependWrapper(
+        browser_command_line.GetSwitchValueNative(switches::kUtilityCmdPrefix));
+  }
 
-    for (const auto& extra_switch : extra_switches_) {
-      cmd_line->AppendSwitch(extra_switch);
-    }
+  for (const auto& extra_switch : options_.extra_switches_) {
+    cmd_line->AppendSwitch(extra_switch);
+  }
 
 #if BUILDFLAG(IS_WIN)
-    if (media::IsMediaFoundationD3D11VideoCaptureEnabled()) {
-      // MediaFoundationD3D11VideoCapture requires Gpu memory buffers,
-      // which are unavailable if the GPU process isn't running or if
-      // D3D shared images are not supported.
-      if (!GpuDataManagerImpl::GetInstance()->IsGpuCompositingDisabled() &&
-          GpuDataManagerImpl::GetInstance()->GetGPUInfo().shared_image_d3d) {
-        cmd_line->AppendSwitch(switches::kVideoCaptureUseGpuMemoryBuffer);
-      }
+  if (media::IsMediaFoundationD3D11VideoCaptureEnabled()) {
+    // MediaFoundationD3D11VideoCapture requires Gpu memory buffers,
+    // which are unavailable if the GPU process isn't running or if
+    // D3D shared images are not supported.
+    if (!GpuDataManagerImpl::GetInstance()->IsGpuCompositingDisabled() &&
+        GpuDataManagerImpl::GetInstance()->GetGPUInfo().shared_image_d3d) {
+      cmd_line->AppendSwitch(switches::kVideoCaptureUseGpuMemoryBuffer);
     }
+  }
 #endif
 
 #if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_MAC)
-    file_data_->files_to_preload.merge(GetV8SnapshotFilesToPreload(*cmd_line));
+  options_.file_data_->files_to_preload.merge(
+      GetV8SnapshotFilesToPreload(*cmd_line));
 #endif  // BUILDFLAG(IS_POSIX)
 
 #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
-    // The network service should have access to the parent directories
-    // necessary for its usage.
-    if (sandbox_type_ == sandbox::mojom::Sandbox::kNetwork) {
-      std::vector<base::FilePath> network_context_parent_dirs =
-          GetContentClient()->browser()->GetNetworkContextsParentDirectory();
-      file_data_->files_to_preload[kNetworkContextParentDirsDescriptor] =
-          PassNetworkContextParentDirs(std::move(network_context_parent_dirs));
-    }
+  // The network service should have access to the parent directories
+  // necessary for its usage.
+  if (options_.sandbox_type_ == sandbox::mojom::Sandbox::kNetwork) {
+    std::vector<base::FilePath> network_context_parent_dirs =
+        GetContentClient()->browser()->GetNetworkContextsParentDirectory();
+    options_.file_data_->files_to_preload[kNetworkContextParentDirsDescriptor] =
+        PassNetworkContextParentDirs(std::move(network_context_parent_dirs));
+  }
 #endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
 
 #if BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE) && !BUILDFLAG(IS_WIN)
-    // Pass `kVideoCaptureUseGpuMemoryBuffer` flag to video capture service only
-    // when the video capture use GPU memory buffer enabled.
-    if (metrics_name_ == video_capture::mojom::VideoCaptureService::Name_) {
-      bool pass_gpu_buffer_flag =
-          switches::IsVideoCaptureUseGpuMemoryBufferEnabled();
+  // Pass `kVideoCaptureUseGpuMemoryBuffer` flag to video capture service only
+  // when the video capture use GPU memory buffer enabled.
+  if (options_.metrics_name_ ==
+      video_capture::mojom::VideoCaptureService::Name_) {
+    bool pass_gpu_buffer_flag =
+        switches::IsVideoCaptureUseGpuMemoryBufferEnabled();
 #if BUILDFLAG(IS_LINUX)
-      // Check if NV12 GPU memory buffer supported at the same time.
-      pass_gpu_buffer_flag =
-          pass_gpu_buffer_flag &&
-          GpuDataManagerImpl::GetInstance()->IsGpuMemoryBufferNV12Supported();
+    // Check if NV12 GPU memory buffer supported at the same time.
+    pass_gpu_buffer_flag =
+        pass_gpu_buffer_flag &&
+        GpuDataManagerImpl::GetInstance()->IsGpuMemoryBufferNV12Supported();
 #endif  // BUILDFLAG(IS_LINUX)
-      if (pass_gpu_buffer_flag) {
-        cmd_line->AppendSwitch(switches::kVideoCaptureUseGpuMemoryBuffer);
-      }
+    if (pass_gpu_buffer_flag) {
+      cmd_line->AppendSwitch(switches::kVideoCaptureUseGpuMemoryBuffer);
     }
+  }
 #endif  // BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE) && !BUILDFLAG(IS_WIN)
 
-    std::unique_ptr<UtilitySandboxedProcessLauncherDelegate> delegate =
-        std::make_unique<UtilitySandboxedProcessLauncherDelegate>(
-            sandbox_type_, env_, *cmd_line);
+  std::unique_ptr<UtilitySandboxedProcessLauncherDelegate> delegate =
+      std::make_unique<UtilitySandboxedProcessLauncherDelegate>(
+          options_.sandbox_type_, options_.env_, *cmd_line);
 
 #if BUILDFLAG(IS_WIN)
-    if (!preload_libraries_.empty()) {
-      delegate->SetPreloadLibraries(preload_libraries_);
-    }
+  if (!options_.preload_libraries_.empty()) {
+    delegate->SetPreloadLibraries(options_.preload_libraries_);
+  }
 #endif  // BUILDFLAG(IS_WIN)
 
 #if BUILDFLAG(USE_ZYGOTE)
-    if (zygote_for_testing_.has_value()) {
-      delegate->SetZygote(zygote_for_testing_.value());
-    }
+  if (options_.zygote_for_testing_.has_value()) {
+    delegate->SetZygote(options_.zygote_for_testing_.value());
+  }
 #endif  // BUILDFLAG(USE_ZYGOTE)
 
-    process_->LaunchWithFileData(std::move(delegate), std::move(cmd_line),
-                                 std::move(file_data_), true);
-  }
+  process_->LaunchWithFileData(std::move(delegate), std::move(cmd_line),
+                               std::move(options_.file_data_), true);
 
   return true;
 }
@@ -502,7 +566,7 @@
 }
 
 std::optional<std::string> UtilityProcessHost::GetServiceName() {
-  return metrics_name_;
+  return options_.metrics_name_;
 }
 
 }  // namespace content
diff --git a/content/browser/service_host/utility_process_host.h b/content/browser/service_host/utility_process_host.h
index d13e6db4..4335d7ff 100644
--- a/content/browser/service_host/utility_process_host.h
+++ b/content/browser/service_host/utility_process_host.h
@@ -12,7 +12,6 @@
 #include <vector>
 
 #include "base/environment.h"
-#include "base/memory/weak_ptr.h"
 #include "base/process/launch.h"
 #include "build/build_config.h"
 #include "build/chromecast_buildflags.h"
@@ -43,24 +42,14 @@
 namespace content {
 class BrowserChildProcessHostImpl;
 class InProcessChildThreadParams;
-struct ChildProcessData;
 
 typedef base::Thread* (*UtilityMainThreadFactoryFunction)(
     const InProcessChildThreadParams&);
 
-// This class acts as the browser-side host to a utility child process.  A
-// utility process is a short-lived process that is created to run a specific
-// task.  This class lives solely on the UI thread.
-// If you need a single method call in the process, use StartFooBar(p).
-// If you need multiple batches of work to be done in the process, use
-// StartBatchMode(), then multiple calls to StartFooBar(p), then finish with
-// EndBatchMode().
-// If you need to bind Mojo interfaces, use Start() to start the child
-// process and then call BindInterface().
-//
-// Note: If your class keeps a ptr to an object of this type, grab a weak ptr to
-// avoid a use after free since this object is deleted synchronously but the
-// client notification is asynchronous.  See http://crbug.com/108871.
+// This class acts as the browser-side host to a utility child process hosting a
+// mojo service. This class lives solely on the UI thread. If you need to bind
+// a Mojo interface, specify it via `WithBoundServiceInterfaceOnChildProcess` on
+// the `Options` passed in.
 class CONTENT_EXPORT UtilityProcessHost
     : public BrowserChildProcessHostDelegate {
  public:
@@ -78,71 +67,136 @@
     virtual void OnProcessCrashed() {}
   };
 
-  // This class is self-owned. It must be instantiated using new, and shouldn't
-  // be deleted manually.
-  // TODO(crbug.com/40254698): Make it clearer the caller of the
-  // constructor do not own memory. A static method to create them + private
-  // constructor could be better.
-  UtilityProcessHost();
-  explicit UtilityProcessHost(std::unique_ptr<Client> client);
+  struct CONTENT_EXPORT Options {
+    Options();
+    ~Options();
+
+    Options(const Options&) = delete;
+    Options& operator=(const Options&) = delete;
+
+    Options(Options&&);
+    Options& operator=(Options&&);
+
+    // Makes the process run with a specific sandbox type, or unsandboxed if
+    // Sandbox::kNoSandbox is specified.
+    Options& WithSandboxType(sandbox::mojom::Sandbox sandbox_type);
+
+    // Sets the name of the process to appear in the task manager.
+    Options& WithName(const std::u16string& name);
+
+    // Sets the name used for metrics reporting. This should not be a localized
+    // name. This is recorded to metrics, so update UtilityProcessNameHash enum
+    // in enums.xml if new values are passed here.
+    Options& WithMetricsName(const std::string& metrics_name);
+
+    Options& WithChildFlags(int flags);
+
+    // Provides extra switches to append to the process's command line.
+    Options& WithExtraCommandLineSwitches(std::vector<std::string> switches);
+
+#if BUILDFLAG(IS_WIN)
+    // Specifies libraries to preload before the sandbox is locked down. Paths
+    // should be absolute.
+    Options& WithPreloadLibraries(const std::vector<base::FilePath>& preloads);
+#endif  // BUILDFLAG(IS_WIN)
+
+    // Allows the child process to bind viz.mojom.Gpu.
+    Options& WithGpuClientAllowed();
+
+#if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_MAC)
+    // Adds to ChildProcessLauncherFileData::files_to_preload, which maps |key|
+    // -> |file| in the new process's base::FileDescriptorStore.
+    Options& WithFileToPreload(
+        std::string key,
+        std::variant<base::FilePath, base::ScopedFD> file);
+#endif
+
+#if BUILDFLAG(IS_POSIX)
+    Options& WithEnvironment(const base::EnvironmentMap& env);
+#endif
+
+#if BUILDFLAG(USE_ZYGOTE)
+    Options& WithZygoteForTesting(ZygoteCommunication* handle);
+#endif  // BUILDFLAG(USE_ZYGOTE)
+
+    // Requests that the process bind a receiving pipe targeting the interface
+    // named by `receiver`. Calls to this method generally end up in
+    // `ChildThreadImpl::OnBindReceiver()` and the option is used for testing
+    // only.
+    Options& WithBoundReceiverOnChildProcessForTesting(
+        mojo::GenericPendingReceiver receiver);
+
+    // Requests that the utility process bind a receiving pipe targeting the
+    // service interface named by `receiver`.
+    Options& WithBoundServiceInterfaceOnChildProcess(
+        mojo::GenericPendingReceiver receiver);
+
+    // Passes the contents of this Options object to a newly returned Options
+    // value. This can be called when moving an in-line built Options object
+    // directly into a call to `Start`.
+    Options Pass();
+
+   private:
+    friend class UtilityProcessHost;
+
+    sandbox::mojom::Sandbox sandbox_type_;
+
+    // Map of environment variables to values.
+    base::EnvironmentMap env_;
+
+    // The non-localized name used for metrics reporting.
+    std::string metrics_name_;
+
+    // ChildProcessHost flags to use when starting the child process.
+    int child_flags_;
+
+    // Extra command line switches to append.
+    std::vector<std::string> extra_switches_;
+
+#if BUILDFLAG(IS_WIN)
+    // Libraries to load before sandbox lockdown. Only used on Windows.
+    std::vector<base::FilePath> preload_libraries_;
+#endif  // BUILDFLAG(IS_WIN)
+
+#if BUILDFLAG(USE_ZYGOTE)
+    std::optional<raw_ptr<ZygoteCommunication>> zygote_for_testing_;
+#endif  // BUILDFLAG(USE_ZYGOTE)
+
+#if BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
+    // Whether or not to bind viz::mojom::Gpu to the utility process.
+    bool allowed_gpu_;
+#endif  // BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
+
+    // A mojo receiver to bind once the process starts.
+    std::optional<mojo::GenericPendingReceiver> receiver_to_bind_;
+
+    // A mojo service interface to bind once the process starts.
+    std::optional<mojo::GenericPendingReceiver> service_interface_to_bind_;
+
+    // Extra files and file descriptors to preload in the new process.
+    std::unique_ptr<ChildProcessLauncherFileData> file_data_;
+
+    // The process name used to identify the process in task manager.
+    std::u16string name_;
+  };
+
+  // Creates and starts a new UtilityProcessHost with the specified `Options`.
+  // Pass a `client` if delegate callbacks are needed.
+  static bool Start(Options options, std::unique_ptr<Client> client = nullptr);
 
   UtilityProcessHost(const UtilityProcessHost&) = delete;
   UtilityProcessHost& operator=(const UtilityProcessHost&) = delete;
 
+ private:
+  UtilityProcessHost(Options options, std::unique_ptr<Client> client);
+
   ~UtilityProcessHost() override;
 
-  base::WeakPtr<UtilityProcessHost> AsWeakPtr();
-
-  // Makes the process run with a specific sandbox type, or unsandboxed if
-  // Sandbox::kNoSandbox is specified.
-  void SetSandboxType(sandbox::mojom::Sandbox sandbox_type);
-
-  // Returns information about the utility child process.
-  const ChildProcessData& GetData();
-#if BUILDFLAG(IS_POSIX)
-  void SetEnv(const base::EnvironmentMap& env);
-#endif
-
-  // Starts the utility process.
-  bool Start();
-
-  // Sets the name of the process to appear in the task manager.
-  void SetName(const std::u16string& name);
-
-  // Sets the name used for metrics reporting. This should not be a localized
-  // name. This is recorded to metrics, so update UtilityProcessNameHash enum in
-  // enums.xml if new values are passed here.
-  void SetMetricsName(const std::string& metrics_name);
-
-  void set_child_flags(int flags) { child_flags_ = flags; }
-
-  // Provides extra switches to append to the process's command line.
-  void SetExtraCommandLineSwitches(std::vector<std::string> switches);
-
-  // Allows the child process to bind viz.mojom.Gpu.
-  void SetAllowGpuClient();
-
-#if BUILDFLAG(IS_WIN)
-  // Specifies libraries to preload before the sandbox is locked down. Paths
-  // should be absolute.
-  void SetPreloadLibraries(const std::vector<base::FilePath>& preloads);
-#endif  // BUILDFLAG(IS_WIN)
-
-#if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_MAC)
-  // Adds to ChildProcessLauncherFileData::files_to_preload, which maps |key| ->
-  // |file| in the new process's base::FileDescriptorStore.
-  void AddFileToPreload(std::string key,
-                        std::variant<base::FilePath, base::ScopedFD> file);
-#endif
-
-#if BUILDFLAG(USE_ZYGOTE)
-  void SetZygoteForTesting(ZygoteCommunication* handle);
-#endif  // BUILDFLAG(USE_ZYGOTE)
-
   // Returns a control interface for the running child process.
   mojom::ChildProcess* GetChildProcess();
 
- private:
+  void MaybeBindMojoInterfaces();
+
   // Starts the child process if needed, returns true on success.
   bool StartProcess();
 
@@ -153,23 +207,7 @@
   std::optional<std::string> GetServiceName() override;
   void BindHostReceiver(mojo::GenericPendingReceiver receiver) override;
 
-  // Launch the child process with switches that will setup this sandbox type.
-  sandbox::mojom::Sandbox sandbox_type_;
-
-  // ChildProcessHost flags to use when starting the child process.
-  int child_flags_;
-
-  // Map of environment variables to values.
-  base::EnvironmentMap env_;
-
-  // True if StartProcess() has been called.
-  bool started_;
-
-  // The process name used to identify the process in task manager.
-  std::u16string name_;
-
-  // The non-localized name used for metrics reporting.
-  std::string metrics_name_;
+  Options options_;
 
   // Child process host implementation.
   std::unique_ptr<BrowserChildProcessHostImpl> process_;
@@ -177,21 +215,6 @@
   // Used in single-process mode instead of |process_|.
   std::unique_ptr<base::Thread> in_process_thread_;
 
-  // Extra command line switches to append.
-  std::vector<std::string> extra_switches_;
-
-#if BUILDFLAG(IS_WIN)
-  // Libraries to load before sandbox lockdown. Only used on Windows.
-  std::vector<base::FilePath> preload_libraries_;
-#endif  // BUILDFLAG(IS_WIN)
-
-  // Extra files and file descriptors to preload in the new process.
-  std::unique_ptr<ChildProcessLauncherFileData> file_data_;
-
-#if BUILDFLAG(USE_ZYGOTE)
-  std::optional<raw_ptr<ZygoteCommunication>> zygote_for_testing_;
-#endif  // BUILDFLAG(USE_ZYGOTE)
-
   // Indicates whether the process has been successfully launched yet, or if
   // launch failed.
   enum class LaunchState {
@@ -202,14 +225,10 @@
   LaunchState launch_state_ = LaunchState::kLaunchInProgress;
 
 #if BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
-  bool allowed_gpu_;
   std::unique_ptr<viz::GpuClient, base::OnTaskRunnerDeleter> gpu_client_;
 #endif  // BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
 
   std::unique_ptr<Client> client_;
-
-  // Used to vend weak pointers, and should always be declared last.
-  base::WeakPtrFactory<UtilityProcessHost> weak_ptr_factory_{this};
 };
 
 }  // namespace content
diff --git a/content/browser/service_host/utility_process_host_browsertest.cc b/content/browser/service_host/utility_process_host_browsertest.cc
index 00176ea..60ba2d72 100644
--- a/content/browser/service_host/utility_process_host_browsertest.cc
+++ b/content/browser/service_host/utility_process_host_browsertest.cc
@@ -79,25 +79,22 @@
   void SetUpOnMainThread() override {
     DCHECK_CURRENTLY_ON(BrowserThread::UI);
     BrowserChildProcessObserver::Add(this);
-
-    host_ = new UtilityProcessHost();  // Owned by a global list.
-    host_->SetName(u"TestProcess");
-    host_->SetMetricsName(kTestProcessName);
   }
 
-  void TearDownOnMainThread() override {
-    // `host_` is about to be deleted during BrowserMainRunnerImpl::Shutdown().
-    host_ = nullptr;
+  UtilityProcessHost::Options DefaultOptions() {
+    return UtilityProcessHost::Options()
+        .WithName(u"TestProcess")
+        .WithMetricsName(kTestProcessName)
+        .Pass();
   }
 
-  void SetExpectFailLaunch() {
-    DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  void AddFailedLaunchOptions(UtilityProcessHost::Options& options) {
     expect_failed_launch_ = true;
 
 #if BUILDFLAG(IS_WIN)
     // The Windows sandbox does not like the child process being a different
     // process, so launch unsandboxed for the purpose of this test.
-    host_->SetSandboxType(sandbox::mojom::Sandbox::kNoSandbox);
+    options.WithSandboxType(sandbox::mojom::Sandbox::kNoSandbox);
 #endif
     // Simulate a catastrophic launch failure for all child processes by
     // making the path to the process non-existent.
@@ -106,10 +103,9 @@
         base::FilePath(FILE_PATH_LITERAL("non_existent_path")));
   }
 
-  void SetElevated() {
-    DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  void AddElevatedOptions(UtilityProcessHost::Options& options) {
 #if BUILDFLAG(IS_WIN)
-    host_->SetSandboxType(
+    options.WithSandboxType(
         sandbox::mojom::Sandbox::kNoSandboxAndElevatedPrivileges);
 #else
     NOTREACHED();
@@ -118,18 +114,19 @@
 
   // After `service_` is bound, `run_test` is invoked, and then the RunLoop will
   // run.
-  void RunUtilityProcess(base::OnceClosure run_test) {
+  void RunUtilityProcess(UtilityProcessHost::Options options,
+                         base::OnceClosure run_test) {
     DCHECK_CURRENTLY_ON(BrowserThread::UI);
     base::RunLoop run_loop;
     done_closure_ =
         base::BindOnce(&UtilityProcessHostBrowserTest::DoneRunning,
                        base::Unretained(this), run_loop.QuitClosure());
 
-    EXPECT_TRUE(host_->Start());
-
-    host_->GetChildProcess()->BindServiceInterface(
+    options.WithBoundServiceInterfaceOnChildProcess(
         service_.BindNewPipeAndPassReceiver());
 
+    UtilityProcessHost::Start(std::move(options));
+
     std::move(run_test).Run();
     run_loop.Run();
   }
@@ -207,7 +204,6 @@
     GetUIThreadTaskRunner({})->PostTask(FROM_HERE, std::move(done_closure_));
   }
 
-  raw_ptr<UtilityProcessHost, AcrossTasksDanglingUntriaged> host_;
   mojo::Remote<mojom::TestService> service_;
   base::OnceClosure done_closure_;
   bool expect_crashed_ = false;
@@ -285,6 +281,7 @@
 
 IN_PROC_BROWSER_TEST_F(UtilityProcessHostBrowserTest, LaunchProcess) {
   RunUtilityProcess(
+      DefaultOptions(),
       base::BindOnce(&UtilityProcessHostBrowserTest::RunBasicPingPongTest,
                      base::Unretained(this)));
 }
@@ -301,18 +298,20 @@
 #endif
 IN_PROC_BROWSER_TEST_F(UtilityProcessHostBrowserTest,
                        MAYBE_FileDescriptorStore) {
+  UtilityProcessHost::Options options = DefaultOptions();
   // Tests whether base::FileDescriptorStore works in content by passing it a
   // file descriptor for a pipe on launch. This test ensures the process is
   // launched without a zygote.
 #if BUILDFLAG(USE_ZYGOTE)
-  host_->SetZygoteForTesting(nullptr);
+  options.WithZygoteForTesting(nullptr);
 #endif
 
   base::ScopedFD read_fd;
   base::ScopedFD write_fd;
   ASSERT_TRUE(base::CreatePipe(&read_fd, &write_fd));
-  host_->AddFileToPreload(mojom::kTestPipeKey, std::move(write_fd));
   RunUtilityProcess(
+      options.WithFileToPreload(mojom::kTestPipeKey, std::move(write_fd))
+          .Pass(),
       base::BindOnce(&UtilityProcessHostBrowserTest::RunFileDescriptorStoreTest,
                      base::Unretained(this), std::move(read_fd)));
 }
@@ -324,12 +323,13 @@
   // Tests whether base::FileDescriptorStore works in content by passing it a
   // file descriptor for a pipe on launch. This test ensures the process is
   // launched with the unsandboxed zygote.
-  host_->SetZygoteForTesting(GetUnsandboxedZygote());
-
   base::ScopedFD read_fd, write_fd;
   ASSERT_TRUE(base::CreatePipe(&read_fd, &write_fd));
-  host_->AddFileToPreload(mojom::kTestPipeKey, std::move(write_fd));
   RunUtilityProcess(
+      DefaultOptions()
+          .WithZygoteForTesting(GetUnsandboxedZygote())
+          .WithFileToPreload(mojom::kTestPipeKey, std::move(write_fd))
+          .Pass(),
       base::BindOnce(&UtilityProcessHostBrowserTest::RunFileDescriptorStoreTest,
                      base::Unretained(this), std::move(read_fd)));
 }
@@ -339,12 +339,13 @@
   // Tests whether base::FileDescriptorStore works in content by passing it a
   // file descriptor for a pipe on launch. This test ensures the process is
   // launched with the generic zygote.
-  host_->SetZygoteForTesting(GetGenericZygote());
-
   base::ScopedFD read_fd, write_fd;
   ASSERT_TRUE(base::CreatePipe(&read_fd, &write_fd));
-  host_->AddFileToPreload(mojom::kTestPipeKey, std::move(write_fd));
   RunUtilityProcess(
+      DefaultOptions()
+          .WithZygoteForTesting(GetGenericZygote())
+          .WithFileToPreload(mojom::kTestPipeKey, std::move(write_fd))
+          .Pass(),
       base::BindOnce(&UtilityProcessHostBrowserTest::RunFileDescriptorStoreTest,
                      base::Unretained(this), std::move(read_fd)));
 }
@@ -363,6 +364,7 @@
 IN_PROC_BROWSER_TEST_F(UtilityProcessHostBrowserTest,
                        MAYBE_LaunchProcessAndCrash) {
   RunUtilityProcess(
+      DefaultOptions(),
       base::BindOnce(&UtilityProcessHostBrowserTest::RunCrashImmediatelyTest,
                      base::Unretained(this)));
 }
@@ -376,10 +378,12 @@
 // See also ServiceProcessLauncherTest.FailToLaunchProcess.
 #if !BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_MAC)
 IN_PROC_BROWSER_TEST_F(UtilityProcessHostBrowserTest, FailToLaunchProcess) {
-  SetExpectFailLaunch();
+  UtilityProcessHost::Options options = DefaultOptions();
+  AddFailedLaunchOptions(options);
   // If the ping-pong test completes, the test will fail because that means the
   // process did not fail to launch.
   RunUtilityProcess(
+      std::move(options),
       base::BindOnce(&UtilityProcessHostBrowserTest::RunBasicPingPongTest,
                      base::Unretained(this)));
 }
@@ -387,8 +391,11 @@
 
 #if BUILDFLAG(IS_WIN)
 IN_PROC_BROWSER_TEST_F(UtilityProcessHostBrowserTest, LaunchElevatedProcess) {
-  SetElevated();
   RunUtilityProcess(
+      DefaultOptions()
+          .WithSandboxType(
+              sandbox::mojom::Sandbox::kNoSandboxAndElevatedPrivileges)
+          .Pass(),
       mojo::core::IsMojoIpczEnabled()
           ? base::BindOnce(
                 &UtilityProcessHostBrowserTest::RunSharedMemoryHandleTest,
@@ -400,8 +407,11 @@
 // Disabled because currently this causes a WER dialog to appear.
 IN_PROC_BROWSER_TEST_F(UtilityProcessHostBrowserTest,
                        DISABLED_LaunchElevatedProcessAndCrash) {
-  SetElevated();
   RunUtilityProcess(
+      DefaultOptions()
+          .WithSandboxType(
+              sandbox::mojom::Sandbox::kNoSandboxAndElevatedPrivileges)
+          .Pass(),
       base::BindOnce(&UtilityProcessHostBrowserTest::RunCrashImmediatelyTest,
                      base::Unretained(this)));
 }
@@ -429,8 +439,10 @@
 IN_PROC_BROWSER_TEST_F(NetworkServiceProcessIdentityTest, LaunchService) {
   // The process requirement is applied to the network service based on its
   // sandbox type.
-  host_->SetSandboxType(sandbox::mojom::Sandbox::kNetwork);
   RunUtilityProcess(
+      DefaultOptions()
+          .WithSandboxType(sandbox::mojom::Sandbox::kNetwork)
+          .Pass(),
       base::BindOnce(&UtilityProcessHostBrowserTest::RunBasicPingPongTest,
                      base::Unretained(this)));
 }
diff --git a/content/browser/service_host/utility_process_host_receiver_bindings.cc b/content/browser/service_host/utility_process_host_receiver_bindings.cc
index e405a62..9b1f9e4 100644
--- a/content/browser/service_host/utility_process_host_receiver_bindings.cc
+++ b/content/browser/service_host/utility_process_host_receiver_bindings.cc
@@ -31,7 +31,7 @@
   }
 #endif
 #if BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
-  if (allowed_gpu_) {
+  if (options_.allowed_gpu_) {
     // TODO(crbug.com/328099369) Remove once all clients get this directly.
     if (auto gpu_receiver = receiver.As<viz::mojom::Gpu>()) {
       gpu_client_ = content::CreateGpuClient(std::move(gpu_receiver));
diff --git a/content/browser/service_host/utility_process_sandbox_browsertest.cc b/content/browser/service_host/utility_process_sandbox_browsertest.cc
index ac83bb9..5305510 100644
--- a/content/browser/service_host/utility_process_sandbox_browsertest.cc
+++ b/content/browser/service_host/utility_process_sandbox_browsertest.cc
@@ -77,14 +77,15 @@
     done_closure_ =
         base::BindOnce(&UtilityProcessSandboxBrowserTest::DoneRunning,
                        base::Unretained(this), run_loop.QuitClosure());
-    UtilityProcessHost* host = new UtilityProcessHost();
-    host->SetSandboxType(GetParam());
-    host->SetName(u"SandboxTestProcess");
-    host->SetMetricsName(kTestProcessName);
-    EXPECT_TRUE(host->Start());
+    EXPECT_TRUE(UtilityProcessHost::Start(
+        UtilityProcessHost::Options()
+            .WithSandboxType(GetParam())
+            .WithName(u"SandboxTestProcess")
+            .WithMetricsName(kTestProcessName)
+            .WithBoundReceiverOnChildProcessForTesting(
+                service_.BindNewPipeAndPassReceiver())
+            .Pass()));
 
-    host->GetChildProcess()->BindReceiver(
-        service_.BindNewPipeAndPassReceiver());
     service_->GetSandboxStatus(
         base::BindOnce(&UtilityProcessSandboxBrowserTest::OnGotSandboxStatus,
                        base::Unretained(this)));
diff --git a/content/browser/site_instance_group.cc b/content/browser/site_instance_group.cc
index dc4c63f..d3aa7c4 100644
--- a/content/browser/site_instance_group.cc
+++ b/content/browser/site_instance_group.cc
@@ -42,6 +42,10 @@
   return weak_ptr_factory_.GetSafeRef();
 }
 
+base::WeakPtr<SiteInstanceGroup> SiteInstanceGroup::GetWeakPtr() {
+  return weak_ptr_factory_.GetWeakPtr();
+}
+
 base::WeakPtr<SiteInstanceGroup>
 SiteInstanceGroup::GetWeakPtrToAllowDangling() {
   return weak_ptr_factory_.GetWeakPtr();
diff --git a/content/browser/site_instance_group.h b/content/browser/site_instance_group.h
index ddac1f98..147d485 100644
--- a/content/browser/site_instance_group.h
+++ b/content/browser/site_instance_group.h
@@ -96,6 +96,7 @@
   SiteInstanceGroupId GetId() const;
 
   base::SafeRef<SiteInstanceGroup> GetSafeRef();
+  base::WeakPtr<SiteInstanceGroup> GetWeakPtr();
   // TODO(crbug.com/40258727): Remove this. Please don't use it.
   base::WeakPtr<SiteInstanceGroup> GetWeakPtrToAllowDangling();
 
diff --git a/content/browser/site_instance_group_browsertest.cc b/content/browser/site_instance_group_browsertest.cc
index c3e5bdc..8556c542 100644
--- a/content/browser/site_instance_group_browsertest.cc
+++ b/content/browser/site_instance_group_browsertest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/strings/stringprintf.h"
 #include "base/test/scoped_feature_list.h"
 #include "content/browser/process_lock.h"
 #include "content/browser/web_contents/web_contents_impl.h"
@@ -489,6 +490,167 @@
   EXPECT_EQ(a_instance->group(), data_instance->group());
 }
 
+// Tests for the default SiteInstanceGroup behaviour. This is parameterized to
+// also run in the legacy mode which puts unisolated sites in the default
+// SiteInstance.
+class DefaultSiteInstanceGroupTest
+    : public ContentBrowserTestBase,
+      public ::testing::WithParamInterface<bool> {
+ public:
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    // This feature only takes effect when there is no full site isolation.
+    command_line->RemoveSwitch(switches::kSitePerProcess);
+    command_line->AppendSwitch(switches::kDisableSiteIsolation);
+
+    if (IsDefaultSiteInstanceGroupEnabled()) {
+      feature_list_.InitAndEnableFeature(features::kDefaultSiteInstanceGroups);
+    } else {
+      feature_list_.InitAndDisableFeature(features::kDefaultSiteInstanceGroups);
+    }
+  }
+
+  static std::string DescribeParams(
+      const testing::TestParamInfo<ParamType>& info) {
+    return info.param ? "UseDefaultSiteInstanceGroups"
+                      : "UseDefaultSiteInstances";
+  }
+
+ protected:
+  bool IsDefaultSiteInstanceGroupEnabled() const { return GetParam(); }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+// Basic use case, where multiple unisolated sites are put in the default
+// SiteInstanceGroup, but have their own SiteInstances.
+IN_PROC_BROWSER_TEST_P(DefaultSiteInstanceGroupTest,
+                       DefaultSiteInstanceGroupProperties) {
+  // Navigate to a non-isolated site.
+  GURL url_c(embedded_test_server()->GetURL("c.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), url_c));
+
+  scoped_refptr<SiteInstanceImpl> c_instance =
+      main_frame_host()->GetSiteInstance();
+  EXPECT_FALSE(c_instance->RequiresDedicatedProcess());
+  EXPECT_TRUE(c_instance->GetProcess()->GetProcessLock().allows_any_site());
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_EQ(c_instance->group(),
+              c_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_TRUE(c_instance->IsDefaultSiteInstance());
+  }
+
+  // Navigate to a site with a cross-site iframe, both of which are not
+  // isolated.
+  GURL url(embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a(b)"));
+  EXPECT_TRUE(NavigateToURL(shell(), url));
+
+  scoped_refptr<SiteInstanceImpl> a_instance =
+      main_frame_host()->GetSiteInstance();
+  scoped_refptr<SiteInstanceImpl> b_instance = main_frame()
+                                                   ->child_at(0)
+                                                   ->render_manager()
+                                                   ->current_frame_host()
+                                                   ->GetSiteInstance();
+  EXPECT_TRUE(a_instance->GetProcess()->GetProcessLock().allows_any_site());
+  EXPECT_FALSE(a_instance->RequiresDedicatedProcess());
+  EXPECT_FALSE(b_instance->RequiresDedicatedProcess());
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    // A, B and C should all have their own SiteInstances, but all share the
+    // default SiteInstanceGroup.
+    EXPECT_NE(a_instance, b_instance);
+    EXPECT_NE(a_instance, c_instance);
+    EXPECT_NE(b_instance, c_instance);
+    EXPECT_EQ(a_instance->group(),
+              a_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+    EXPECT_EQ(a_instance->group(), b_instance->group());
+  } else {
+    EXPECT_TRUE(a_instance->IsDefaultSiteInstance());
+    EXPECT_EQ(a_instance, b_instance);
+  }
+
+  // Navigate to an isolated site that does not use the default
+  // SiteInstance/Group.
+  IsolateOriginsForTesting(embedded_test_server(), web_contents(), {"d.com"});
+  GURL url_d(embedded_test_server()->GetURL("d.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), url_d));
+
+  scoped_refptr<SiteInstanceImpl> d_instance =
+      main_frame_host()->GetSiteInstance();
+  EXPECT_TRUE(d_instance->RequiresDedicatedProcess());
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_NE(d_instance->group(),
+              d_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+    EXPECT_FALSE(d_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_FALSE(d_instance->IsDefaultSiteInstance());
+  }
+}
+
+IN_PROC_BROWSER_TEST_P(DefaultSiteInstanceGroupTest,
+                       ProcessHasAllowsAnySiteLock) {
+  // Test starts with a tab that has an unassigned SiteInstance. The associated
+  // SiteInstance should not yet have a site set. The process is locked with an
+  // allow-any-site lock.
+  scoped_refptr<SiteInstanceImpl> start_instance =
+      main_frame_host()->GetSiteInstance();
+  EXPECT_FALSE(start_instance->HasSite());
+  EXPECT_TRUE(start_instance->HasProcess());
+  RenderProcessHost* start_process = start_instance->GetProcess();
+  EXPECT_FALSE(start_process->GetProcessLock().is_locked_to_site());
+  EXPECT_FALSE(start_process->GetProcessLock().is_invalid());
+  EXPECT_EQ(start_process->GetProcessLock().lock_url(), GURL());
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_NE(start_instance->group(),
+              start_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_FALSE(start_instance->IsDefaultSiteInstance());
+  }
+
+  // Do a browser-initiated navigation to about:blank. Because it's an
+  // about:blank navigation without an initiator, it should stay in the previous
+  // SiteInstance which hasn't had a site set yet, and not 'use up' a
+  // SiteInstance.
+  EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
+  scoped_refptr<SiteInstanceImpl> blank_instance =
+      main_frame_host()->GetSiteInstance();
+  EXPECT_FALSE(start_instance->HasSite());
+  RenderProcessHost* blank_process = blank_instance->GetProcess();
+  EXPECT_EQ(start_process, blank_process);
+  EXPECT_EQ(start_instance, blank_instance);
+  EXPECT_FALSE(blank_process->GetProcessLock().is_locked_to_site());
+  EXPECT_FALSE(blank_process->GetProcessLock().is_invalid());
+  EXPECT_EQ(blank_process->GetProcessLock().lock_url().spec(), "");
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_NE(start_instance->group(),
+              start_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_FALSE(start_instance->IsDefaultSiteInstance());
+  }
+
+  // Now navigate to a 'real' site. Since the previous SiteInstance still did
+  // not have a site set, that will now be set as foo.com. With default
+  // SiteInstance/Group, all navigations should remain in the same process.
+  GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), foo_url));
+  scoped_refptr<SiteInstanceImpl> foo_instance =
+      main_frame_host()->GetSiteInstance();
+  EXPECT_TRUE(foo_instance->HasSite());
+  RenderProcessHost* foo_process = foo_instance->GetProcess();
+  EXPECT_TRUE(foo_process->GetProcessLock().allows_any_site());
+  EXPECT_FALSE(blank_process->GetProcessLock().is_invalid());
+  EXPECT_EQ(foo_process->GetProcessLock().lock_url().spec(), "");
+  EXPECT_EQ(foo_instance, start_instance);
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    EXPECT_EQ(foo_instance->group(),
+              foo_instance->DefaultSiteInstanceGroupForBrowsingInstance());
+  } else {
+    EXPECT_TRUE(foo_instance->IsDefaultSiteInstance());
+  }
+}
+
 INSTANTIATE_TEST_SUITE_P(All,
                          DataURLSiteInstanceGroupTest,
                          ::testing::Bool(),
@@ -497,4 +659,8 @@
                          DataURLSiteInstanceGroupTestWithoutSiteIsolation,
                          ::testing::Bool(),
                          &DataURLSiteInstanceGroupTest::DescribeParams);
+INSTANTIATE_TEST_SUITE_P(All,
+                         DefaultSiteInstanceGroupTest,
+                         ::testing::Bool(),
+                         &DefaultSiteInstanceGroupTest::DescribeParams);
 }  // namespace content
diff --git a/content/browser/site_instance_impl.cc b/content/browser/site_instance_impl.cc
index 8db7e1a..c88f607 100644
--- a/content/browser/site_instance_impl.cc
+++ b/content/browser/site_instance_impl.cc
@@ -22,6 +22,7 @@
 #include "content/browser/renderer_host/render_process_host_impl.h"
 #include "content/browser/site_instance_group.h"
 #include "content/browser/storage_partition_impl.h"
+#include "content/common/content_navigation_policy.h"
 #include "content/common/features.h"
 #include "content/public/browser/browser_or_resource_context.h"
 #include "content/public/browser/content_browser_client.h"
@@ -396,6 +397,7 @@
 }
 
 void SiteInstanceImpl::AddSiteInfoToDefault(const SiteInfo& site_info) {
+  DCHECK(!ShouldUseDefaultSiteInstanceGroup());
   DCHECK(IsDefaultSiteInstance());
   default_site_instance_state_->AddSiteInfo(site_info);
 }
@@ -469,8 +471,17 @@
       allocation_context.navigation_context->requires_new_process_for_coop =
           coop_reuse_process_failed_;
     }
-    SetProcessInternal(RenderProcessHostImpl::GetProcessHostForSiteInstance(
-        this, allocation_context));
+
+    // See if `this` can be placed in the default SiteInstanceGroup, otherwise
+    // create a process and associated SiteInstanceGroup.
+    if (CanPutSiteInstanceInDefaultGroup() &&
+        browsing_instance_->has_default_site_instance_group()) {
+      browsing_instance_->default_site_instance_group()->AddSiteInstance(this);
+      SetSiteInstanceGroup(browsing_instance_->default_site_instance_group());
+    } else {
+      SetProcessInternal(RenderProcessHostImpl::GetProcessHostForSiteInstance(
+          this, allocation_context));
+    }
   }
   DCHECK(site_instance_group_);
 
@@ -513,6 +524,13 @@
     return;
   }
 
+  // If `this` can go in the default SiteInstanceGroup and one exists, prefer
+  // that SiteInstanceGroup and process.
+  if (CanPutSiteInstanceInDefaultGroup() &&
+      browsing_instance()->has_default_site_instance_group()) {
+    return;
+  }
+
   // TODO(crbug.com/40676483): Don't try to reuse process if either of the
   // SiteInstances are cross-origin isolated (uses COOP/COEP).
   SetProcessInternal(existing_process);
@@ -525,6 +543,14 @@
     site_instance_group_->AddSiteInstance(this);
   }
 
+  // Check if the process created should become the default SiteInstanceGroup's
+  // process. If so, set `site_instance_group_` to be the default
+  // SiteInstanceGroup. We should only get here if a process needs to be created
+  // for the default SiteInstanceGroup.
+  if (CanPutSiteInstanceInDefaultGroup()) {
+    MaybeSetDefaultSiteInstanceGroup();
+  }
+
   LockProcessIfNeeded();
 
   // If we are using process-per-site, we need to register this process
@@ -684,11 +710,12 @@
 void SiteInstanceImpl::ConvertToDefaultOrSetSite(const UrlInfo& url_info) {
   DCHECK(!has_site_);
 
-  if (!browsing_instance_->has_default_site_instance()) {
+  if (!ShouldUseDefaultSiteInstanceGroup() &&
+      !browsing_instance_->has_default_site_instance()) {
     // We want to set a SiteInfo in this SiteInstance, from information in a
-    // UrlInfo. The WebExposedIsolationInfo must be compatible for this function
-    // to not violate WebExposedIsolationInfo isolation invariant within a
-    // BrowsingInstance.
+    // UrlInfo. The WebExposedIsolationInfo must be compatible for this
+    // function to not violate WebExposedIsolationInfo isolation invariant
+    // within a BrowsingInstance.
     DCHECK(WebExposedIsolationInfo::AreCompatible(
         url_info.web_exposed_isolation_info, GetWebExposedIsolationInfo()));
 
@@ -700,8 +727,8 @@
 
     const SiteInfo site_info =
         SiteInfo::Create(GetIsolationContext(), updated_url_info);
-    if (CanBePlacedInDefaultSiteInstance(GetIsolationContext(),
-                                         updated_url_info.url, site_info)) {
+    if (CanBePlacedInDefaultSiteInstanceOrGroup(
+            GetIsolationContext(), updated_url_info.url, site_info)) {
       SetSiteInfoToDefault(site_info.storage_partition_config());
       AddSiteInfoToDefault(site_info);
 
@@ -711,6 +738,31 @@
   }
 
   SetSite(url_info);
+
+  // If `this` should go in the default SiteInstanceGroup, it needs to be a
+  // regular SiteInstance with a site (unlike the default SiteInstance), so
+  // SetSite needs to be called first.
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    MaybeSetDefaultSiteInstanceGroup();
+  }
+}
+
+void SiteInstanceImpl::MaybeSetDefaultSiteInstanceGroup() {
+  CHECK(ShouldUseDefaultSiteInstanceGroup());
+  if (!browsing_instance_->has_default_site_instance_group() &&
+      CanBePlacedInDefaultSiteInstanceOrGroup(GetIsolationContext(),
+                                              GetSiteURL(), site_info_)) {
+    CHECK(HasProcess());
+    CHECK(has_group());
+    browsing_instance_->set_default_site_instance_group(
+        site_instance_group_->GetWeakPtr());
+  }
+}
+
+bool SiteInstanceImpl::CanPutSiteInstanceInDefaultGroup() {
+  return ShouldUseDefaultSiteInstanceGroup() &&
+         CanBePlacedInDefaultSiteInstanceOrGroup(GetIsolationContext(),
+                                                 GetSiteURL(), site_info_);
 }
 
 SiteInstanceProcessAssignment
@@ -1045,8 +1097,8 @@
     updated_url_info.web_exposed_isolation_info = GetWebExposedIsolationInfo();
 
     auto site_info = SiteInfo::Create(GetIsolationContext(), updated_url_info);
-    return CanBePlacedInDefaultSiteInstance(GetIsolationContext(), url,
-                                            site_info) &&
+    return CanBePlacedInDefaultSiteInstanceOrGroup(GetIsolationContext(), url,
+                                                   site_info) &&
            !browsing_instance_->HasSiteInstance(site_info);
   }
 
@@ -1370,9 +1422,10 @@
       site_info_.web_exposed_isolation_info();
 
   auto site_info = SiteInfo::Create(GetIsolationContext(), updated_url_info);
-  if (kCreateForURLAllowsDefaultSiteInstance &&
-      CanBePlacedInDefaultSiteInstance(GetIsolationContext(), url_info.url,
-                                       site_info)) {
+  if (!ShouldUseDefaultSiteInstanceGroup() &&
+      kCreateForURLAllowsDefaultSiteInstance &&
+      CanBePlacedInDefaultSiteInstanceOrGroup(GetIsolationContext(),
+                                              url_info.url, site_info)) {
     site_info = SiteInfo::CreateForDefaultSiteInstance(
         GetIsolationContext(), site_info.storage_partition_config(),
         GetWebExposedIsolationInfo());
@@ -1390,23 +1443,31 @@
 }
 
 // static
-bool SiteInstanceImpl::CanBePlacedInDefaultSiteInstance(
+bool SiteInstanceImpl::CanBePlacedInDefaultSiteInstanceOrGroup(
     const IsolationContext& isolation_context,
     const GURL& url,
     const SiteInfo& site_info) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
 
+  // Empty URLs, like the initial empty document, should not be placed in the
+  // default SiteInstance or group. The initial empty document's SiteInstance
+  // can be reused, including for navigations to isolated sites. Avoid the case
+  // where a SiteInstance or group set as the default can then become isolated.
+  if (url.is_empty()) {
+    return false;
+  }
+
   // Exclude "file://" URLs from the default SiteInstance to prevent the
-  // default SiteInstance process from accumulating file access grants that
-  // could be exploited by other non-isolated sites.
+  // default SiteInstance/Group process from accumulating file access grants
+  // that could be exploited by other non-isolated sites.
   if (url.SchemeIs(url::kFileScheme))
     return false;
 
-  // Don't use the default SiteInstance when SiteInstance doesn't assign a
+  // Don't use the default SiteInstance/Group when SiteInstance doesn't assign a
   // site URL for |url|, since in that case the SiteInstance should remain
   // unused, and a subsequent navigation should always be able to reuse it,
   // whether or not it's to a site requiring a dedicated process or to a site
-  // that will use the default SiteInstance.
+  // that will use the default SiteInstance/Group.
   if (!ShouldAssignSiteForURL(url))
     return false;
 
@@ -1634,12 +1695,18 @@
 }
 
 RenderProcessHost* SiteInstanceImpl::GetDefaultProcessForBrowsingInstance() {
-  if (SiteInstanceImpl* default_instance =
-          browsing_instance_->default_site_instance()) {
-    return default_instance->HasProcess() ? default_instance->GetProcess()
-                                          : nullptr;
+  if (ShouldUseDefaultSiteInstanceGroup()) {
+    return browsing_instance_->has_default_site_instance_group()
+               ? browsing_instance_->default_site_instance_group()->process()
+               : nullptr;
+  } else {
+    if (SiteInstanceImpl* default_instance =
+            browsing_instance_->default_site_instance()) {
+      return default_instance->HasProcess() ? default_instance->GetProcess()
+                                            : nullptr;
+    }
+    return nullptr;
   }
-  return nullptr;
 }
 
 void SiteInstanceImpl::SetProcessForTesting(RenderProcessHost* process) {
diff --git a/content/browser/site_instance_impl.h b/content/browser/site_instance_impl.h
index 802972bf..c0e49c74 100644
--- a/content/browser/site_instance_impl.h
+++ b/content/browser/site_instance_impl.h
@@ -350,6 +350,16 @@
   // logic is run.
   void ConvertToDefaultOrSetSite(const UrlInfo& url_info);
 
+  // If `browsing_instance_` does not have a default SiteInstanceGroup set and
+  // if `site_instance_group_` is eligible to become the default
+  // SiteInstanceGroup, this function makes `site_instance_group_` the new
+  // default SiteInstanceGroup. Otherwise, this is a no-op.
+  void MaybeSetDefaultSiteInstanceGroup();
+
+  // Checks if the default SiteInstanceGroup feature is enabled, and if the
+  // SiteInstance can be placed in the default SiteInstanceGroup.
+  bool CanPutSiteInstanceInDefaultGroup();
+
   // Returns whether SetSite() has been called.
   //
   // In some cases, the "site" is not set at SiteInstance creation time, and
@@ -545,6 +555,8 @@
   // where it is safe. It is not generally safe to change the process of a
   // SiteInstance, unless the RenderProcessHost itself is entirely destroyed and
   // a new one later replaces it.
+  // Before creating a process and calling this method, check if `this` can be
+  // placed in the default SiteInstanceGroup.
   void SetProcessInternal(RenderProcessHost* process);
 
   // Returns true if |original_url()| is the same site as
@@ -576,14 +588,14 @@
                          bool should_compare_effective_urls);
 
   // Returns true if |url| and its |site_url| can be placed inside a default
-  // SiteInstance.
+  // SiteInstance or default SiteInstanceGroup.
   //
   // Note: |url| and |site_info| must be consistent with each other. In contexts
   // where the caller only has |url| it can use
   // SiteInfo::Create() to generate |site_info|. This call is
   // intentionally not set as a default value to encourage the caller to reuse
   // a SiteInfo computation if they already have one.
-  static bool CanBePlacedInDefaultSiteInstance(
+  static bool CanBePlacedInDefaultSiteInstanceOrGroup(
       const IsolationContext& isolation_context,
       const GURL& url,
       const SiteInfo& site_info);
diff --git a/content/browser/storage_partition_impl.cc b/content/browser/storage_partition_impl.cc
index 6b6eec74..e55fc5e 100644
--- a/content/browser/storage_partition_impl.cc
+++ b/content/browser/storage_partition_impl.cc
@@ -2461,6 +2461,12 @@
       top_frame_origin, std::move(event_record));
 }
 
+#if BUILDFLAG(IS_MAC)
+bool StoragePartitionImpl::IsStorageServiceRemoteValid() const {
+  return GetStorageServiceRemoteStorage().is_bound();
+}
+#endif  // BUILDFLAG(IS_MAC)
+
 void StoragePartitionImpl::Clone(
     mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
         observer) {
diff --git a/content/browser/storage_partition_impl.h b/content/browser/storage_partition_impl.h
index 3b1355902..da85f80 100644
--- a/content/browser/storage_partition_impl.h
+++ b/content/browser/storage_partition_impl.h
@@ -407,6 +407,10 @@
     return shared_storage_header_observer_.get();
   }
 
+#if BUILDFLAG(IS_MAC)
+  bool IsStorageServiceRemoteValid() const;
+#endif  // BUILDFLAG(IS_MAC)
+
   // Can return nullptr while `this` is being destroyed.
   BrowserContext* browser_context() const;
 
diff --git a/content/browser/usb/web_usb_service_impl_unittest.cc b/content/browser/usb/web_usb_service_impl_unittest.cc
index 3c90d2a..6668547 100644
--- a/content/browser/usb/web_usb_service_impl_unittest.cc
+++ b/content/browser/usb/web_usb_service_impl_unittest.cc
@@ -14,6 +14,7 @@
 #include "base/barrier_closure.h"
 #include "base/functional/bind.h"
 #include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/gmock_callback_support.h"
 #include "base/test/test_future.h"
 #include "content/browser/service_worker/embedded_worker_test_helper.h"
diff --git a/content/browser/web_contents/partitioned_popins_controller_browsertest.cc b/content/browser/web_contents/partitioned_popins_controller_browsertest.cc
index ad8db42..4f357f6 100644
--- a/content/browser/web_contents/partitioned_popins_controller_browsertest.cc
+++ b/content/browser/web_contents/partitioned_popins_controller_browsertest.cc
@@ -7,6 +7,7 @@
 #include "base/check_op.h"
 #include "base/memory/weak_ptr.h"
 #include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/scoped_feature_list.h"
 #include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
diff --git a/content/browser/webid/federated_auth_request_impl.cc b/content/browser/webid/federated_auth_request_impl.cc
index b70e429..0798b89 100644
--- a/content/browser/webid/federated_auth_request_impl.cc
+++ b/content/browser/webid/federated_auth_request_impl.cc
@@ -21,6 +21,7 @@
 #include "base/metrics/histogram_macros.h"
 #include "base/rand_util.h"
 #include "base/strings/escape.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/to_string.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/time/time.h"
diff --git a/content/browser/webid/idp_network_request_manager.cc b/content/browser/webid/idp_network_request_manager.cc
index b44932f..78755491 100644
--- a/content/browser/webid/idp_network_request_manager.cc
+++ b/content/browser/webid/idp_network_request_manager.cc
@@ -11,6 +11,7 @@
 #include "base/json/json_writer.h"
 #include "base/strings/escape.h"
 #include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/to_string.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/task/sequenced_task_runner.h"
diff --git a/content/common/render_widget_host_ns_view.mojom b/content/common/render_widget_host_ns_view.mojom
index 1e300e82..bbcf43d7 100644
--- a/content/common/render_widget_host_ns_view.mojom
+++ b/content/common/render_widget_host_ns_view.mojom
@@ -162,18 +162,20 @@
   // Indicates whether or not the NSView is its NSWindow's first responder.
   OnFirstResponderChanged(bool is_first_responder);
 
-  // Indicates whether or not the NSView is its NSWindow's first responder.
-  OnWindowIsKeyChanged(bool is_key);
-
   // Indicates whether or not the NSView's NSWindow is key.
-  OnBoundsInWindowChanged(
-      gfx.mojom.Rect view_bounds_in_window_dip,
-      bool attached_to_window);
+  OnWindowIsKeyChanged(bool is_key);
 
   // Indicates the NSView's bounds in its NSWindow's DIP coordinate system (with
   // the origin at the upper-left corner), and indicate if the the NSView is
   // attached to an NSWindow (if it is not, then |view_bounds_in_window_dip|'s
   // origin is meaningless, but its size is still relevant).
+  OnBoundsInWindowChanged(
+      gfx.mojom.Rect view_bounds_in_window_dip,
+      bool attached_to_window);
+
+  // Indicates the NSView's NSWindow's frame in the global display::Screen
+  // DIP coordinate system (where the origin the upper-left corner of
+  // Screen::GetPrimaryDisplay).
   OnWindowFrameInScreenChanged(
       gfx.mojom.Rect window_frame_in_screen_dip);
 
diff --git a/content/public/browser/content_browser_client.cc b/content/public/browser/content_browser_client.cc
index eb036b9..2082523 100644
--- a/content/public/browser/content_browser_client.cc
+++ b/content/public/browser/content_browser_client.cc
@@ -192,11 +192,12 @@
   return true;
 }
 
-std::optional<ContentBrowserClient::SpareProcessRefusedByEmbedderReason>
-ContentBrowserClient::ShouldUseSpareRenderProcessHost(
+bool ContentBrowserClient::ShouldUseSpareRenderProcessHost(
     BrowserContext* browser_context,
-    const GURL& site_url) {
-  return std::nullopt;
+    const GURL& site_url,
+    std::optional<SpareProcessRefusedByEmbedderReason>& refused_reason) {
+  refused_reason = std::nullopt;
+  return true;
 }
 
 bool ContentBrowserClient::DoesSiteRequireDedicatedProcess(
diff --git a/content/public/browser/content_browser_client.h b/content/public/browser/content_browser_client.h
index 8c53f1a..3cff2ad 100644
--- a/content/public/browser/content_browser_client.h
+++ b/content/public/browser/content_browser_client.h
@@ -461,8 +461,8 @@
 
   // Returns whether a spare RenderProcessHost should be used for navigating to
   // the specified site URL. If the spare render process can be used, the
-  // function will return an empty value. Otherwise the detailed reason will be
-  // returned.
+  // function will return true with an empty refused_reason. Otherwise the
+  // function returns false and the detailed reason will be returned.
   //
   // Using the spare RenderProcessHost is advisable, because it can improve
   // performance of a navigation that requires a new process.  On the other
@@ -471,9 +471,10 @@
   // ContentBrowserClient::AppendExtraCommandLineSwitches and add some cmdline
   // switches at navigation time (and this won't work for the spare, because the
   // spare RenderProcessHost is launched ahead of time).
-  virtual std::optional<SpareProcessRefusedByEmbedderReason>
-  ShouldUseSpareRenderProcessHost(BrowserContext* browser_context,
-                                  const GURL& site_url);
+  virtual bool ShouldUseSpareRenderProcessHost(
+      BrowserContext* browser_context,
+      const GURL& site_url,
+      std::optional<SpareProcessRefusedByEmbedderReason>& refused_reason);
 
   // Returns true if site isolation should be enabled for |effective_site_url|.
   // This call allows the embedder to supplement the site isolation policy
diff --git a/content/public/test/keep_alive_url_loader_utils.cc b/content/public/test/keep_alive_url_loader_utils.cc
index 5855338..ae273cf 100644
--- a/content/public/test/keep_alive_url_loader_utils.cc
+++ b/content/public/test/keep_alive_url_loader_utils.cc
@@ -285,6 +285,33 @@
   impl_->get()->WaitForTotalOnCompleteProcessed(error_codes);
 }
 
+KeepAliveRequestUkmMatcher::CommonUkm::CommonUkm(
+    KeepAliveRequestTracker::RequestType request_type,
+    size_t category_id,
+    size_t num_redirects,
+    bool is_context_detached,
+    KeepAliveRequestTracker::RequestStageType end_stage,
+    std::optional<KeepAliveRequestTracker::RequestStageType> previous_stage,
+    const std::optional<base::UnguessableToken>& keepalive_token,
+    std::optional<int64_t> failed_error_code,
+    std::optional<int64_t> failed_extended_error_code,
+    std::optional<int64_t> completed_error_code,
+    std::optional<int64_t> completed_extended_error_code)
+    : request_type(request_type),
+      category_id(category_id),
+      num_redirects(num_redirects),
+      is_context_detached(is_context_detached),
+      end_stage(end_stage),
+      previous_stage(previous_stage),
+      keepalive_token(keepalive_token),
+      failed_error_code(failed_error_code),
+      failed_extended_error_code(failed_extended_error_code),
+      completed_error_code(completed_error_code),
+      completed_extended_error_code(completed_extended_error_code) {}
+
+KeepAliveRequestUkmMatcher::CommonUkm::CommonUkm(const CommonUkm& other) =
+    default;
+
 const ukm::mojom::UkmEntry* KeepAliveRequestUkmMatcher::GetUkmEntry() {
   auto entries = ukm_recorder().GetEntriesByName(UkmEvent::kEntryName);
   CHECK_EQ(entries.size(), 1u)
@@ -307,8 +334,10 @@
     KeepAliveRequestTracker::RequestStageType end_stage,
     std::optional<KeepAliveRequestTracker::RequestStageType> previous_stage,
     const std::optional<base::UnguessableToken>& keepalive_token,
-    std::optional<int64_t> error_code,
-    std::optional<int64_t> extended_error_code) {
+    std::optional<int64_t> failed_error_code,
+    std::optional<int64_t> failed_extended_error_code,
+    std::optional<int64_t> completed_error_code,
+    std::optional<int64_t> completed_extended_error_code) {
   EXPECT_TRUE(ukm_recorder().EntryHasMetric(entry, "Id.Low"));
   EXPECT_TRUE(ukm_recorder().EntryHasMetric(entry, "Id.High"));
   if (keepalive_token.has_value()) {
@@ -335,21 +364,39 @@
     EXPECT_FALSE(ukm_recorder().EntryHasMetric(entry, "PreviousStage"));
   }
 
-  if (error_code.has_value()) {
-    ukm_recorder().ExpectEntryMetric(entry, "CompletionStatus.ErrorCode",
-                                     static_cast<int64_t>(*error_code));
+  if (failed_error_code.has_value()) {
+    ukm_recorder().ExpectEntryMetric(entry, "RequestFailed.ErrorCode",
+                                     static_cast<int64_t>(*failed_error_code));
   } else {
     EXPECT_FALSE(
-        ukm_recorder().EntryHasMetric(entry, "CompletionStatus.ErrorCode"));
+        ukm_recorder().EntryHasMetric(entry, "RequestFailed.ErrorCode"));
   }
 
-  if (extended_error_code.has_value()) {
+  if (failed_extended_error_code.has_value()) {
     ukm_recorder().ExpectEntryMetric(
-        entry, "CompletionStatus.ExtendedErrorCode",
-        static_cast<int64_t>(*extended_error_code));
+        entry, "RequestFailed.ExtendedErrorCode",
+        static_cast<int64_t>(*failed_extended_error_code));
   } else {
     EXPECT_FALSE(ukm_recorder().EntryHasMetric(
-        entry, "CompletionStatus.ExtendedErrorCode"));
+        entry, "RequestFailed.ExtendedErrorCode"));
+  }
+
+  if (completed_error_code.has_value()) {
+    ukm_recorder().ExpectEntryMetric(
+        entry, "LoaderCompleted.ErrorCode",
+        static_cast<int64_t>(*completed_error_code));
+  } else {
+    EXPECT_FALSE(
+        ukm_recorder().EntryHasMetric(entry, "LoaderCompleted.ErrorCode"));
+  }
+
+  if (completed_extended_error_code.has_value()) {
+    ukm_recorder().ExpectEntryMetric(
+        entry, "LoaderCompleted.ExtendedErrorCode",
+        static_cast<int64_t>(*completed_extended_error_code));
+  } else {
+    EXPECT_FALSE(ukm_recorder().EntryHasMetric(
+        entry, "LoaderCompleted.ExtendedErrorCode"));
   }
 }
 
@@ -361,12 +408,16 @@
     KeepAliveRequestTracker::RequestStageType end_stage,
     std::optional<KeepAliveRequestTracker::RequestStageType> previous_stage,
     const std::optional<base::UnguessableToken>& keepalive_token,
-    std::optional<int64_t> error_code,
-    std::optional<int64_t> extended_error_code) {
+    std::optional<int64_t> failed_error_code,
+    std::optional<int64_t> failed_extended_error_code,
+    std::optional<int64_t> completed_error_code,
+    std::optional<int64_t> completed_extended_error_code) {
   const ukm::mojom::UkmEntry* entry = GetUkmEntry();
   ExpectCommonUkm(entry, request_type, category_id, num_redirects,
                   is_context_detached, end_stage, previous_stage,
-                  keepalive_token, error_code, extended_error_code);
+                  keepalive_token, failed_error_code,
+                  failed_extended_error_code, completed_error_code,
+                  completed_extended_error_code);
 }
 
 void KeepAliveRequestUkmMatcher::ExpectCommonUkms(
@@ -378,11 +429,12 @@
       << ukms.size();
 
   for (size_t i = 0; i < entries.size(); ++i) {
-    ExpectCommonUkm(entries[i], ukms[i].request_type, ukms[i].category_id,
-                    ukms[i].num_redirects, ukms[i].is_context_detached,
-                    ukms[i].end_stage, ukms[i].previous_stage,
-                    ukms[i].keepalive_token, ukms[i].error_code,
-                    ukms[i].extended_error_code);
+    ExpectCommonUkm(
+        entries[i], ukms[i].request_type, ukms[i].category_id,
+        ukms[i].num_redirects, ukms[i].is_context_detached, ukms[i].end_stage,
+        ukms[i].previous_stage, ukms[i].keepalive_token,
+        ukms[i].failed_error_code, ukms[i].failed_extended_error_code,
+        ukms[i].completed_error_code, ukms[i].completed_extended_error_code);
   }
 }
 
diff --git a/content/public/test/keep_alive_url_loader_utils.h b/content/public/test/keep_alive_url_loader_utils.h
index c13daca6..c6a037d 100644
--- a/content/public/test/keep_alive_url_loader_utils.h
+++ b/content/public/test/keep_alive_url_loader_utils.h
@@ -110,8 +110,26 @@
     std::optional<KeepAliveRequestTracker::RequestStageType> previous_stage =
         std::nullopt;
     std::optional<base::UnguessableToken> keepalive_token = std::nullopt;
-    std::optional<int64_t> error_code = std::nullopt;
-    std::optional<int64_t> extended_error_code = std::nullopt;
+    std::optional<int64_t> failed_error_code = std::nullopt;
+    std::optional<int64_t> failed_extended_error_code = std::nullopt;
+    std::optional<int64_t> completed_error_code = std::nullopt;
+    std::optional<int64_t> completed_extended_error_code = std::nullopt;
+
+    CommonUkm(
+        KeepAliveRequestTracker::RequestType request_type,
+        size_t category_id,
+        size_t num_redirects,
+        bool is_context_detached,
+        KeepAliveRequestTracker::RequestStageType end_stage,
+        std::optional<KeepAliveRequestTracker::RequestStageType>
+            previous_stage = std::nullopt,
+        const std::optional<base::UnguessableToken>& keepalive_token =
+            std::nullopt,
+        std::optional<int64_t> failed_error_code = std::nullopt,
+        std::optional<int64_t> failed_extended_error_code = std::nullopt,
+        std::optional<int64_t> completed_error_code = std::nullopt,
+        std::optional<int64_t> completed_extended_error_code = std::nullopt);
+    CommonUkm(const CommonUkm& other);
   };
 
   // Verifies all the common UKM metrics.
@@ -128,8 +146,10 @@
           std::nullopt,
       const std::optional<base::UnguessableToken>& keepalive_token =
           std::nullopt,
-      std::optional<int64_t> error_code = std::nullopt,
-      std::optional<int64_t> extended_error_code = std::nullopt);
+      std::optional<int64_t> failed_error_code = std::nullopt,
+      std::optional<int64_t> failed_extended_error_code = std::nullopt,
+      std::optional<int64_t> completed_error_code = std::nullopt,
+      std::optional<int64_t> completed_extended_error_code = std::nullopt);
   void ExpectCommonUkms(const std::vector<CommonUkm>& ukms);
 
   // Verifies that UKM TimeDelta.* listed in `time_sorted_metric_names` are all
@@ -155,8 +175,10 @@
           std::nullopt,
       const std::optional<base::UnguessableToken>& keepalive_token =
           std::nullopt,
-      std::optional<int64_t> error_code = std::nullopt,
-      std::optional<int64_t> extended_error_code = std::nullopt);
+      std::optional<int64_t> failed_error_code = std::nullopt,
+      std::optional<int64_t> failed_extended_error_code = std::nullopt,
+      std::optional<int64_t> completed_error_code = std::nullopt,
+      std::optional<int64_t> completed_extended_error_code = std::nullopt);
 };
 
 // `NavigationKeepAliveRequestUkmMatcher` provides common matchers and
diff --git a/content/public/test/prerender_test_util.cc b/content/public/test/prerender_test_util.cc
index 0f24e54..54a47f2 100644
--- a/content/public/test/prerender_test_util.cc
+++ b/content/public/test/prerender_test_util.cc
@@ -8,6 +8,7 @@
 
 #include "base/functional/callback_helpers.h"
 #include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/bind.h"
 #include "base/trace_event/typed_macros.h"
diff --git a/content/public/test/test_utils.cc b/content/public/test/test_utils.cc
index 635de19..e80f8ed 100644
--- a/content/public/test/test_utils.cc
+++ b/content/public/test/test_utils.cc
@@ -563,6 +563,8 @@
     // for the default SiteInstance). It is important not to call
     // SiteInfo::CreateForTesting here to avoid an infinite recursive call when
     // computing values for the SiteInfo.
+    // TODO(crbug.com/390571607): Make sure this test works as intended in
+    // default SiteInstanceGroup mode.
     GURL maybe_modified_url =
         SiteInfo::GetSiteForURLForTest(IsolationContext(browser_context),
                                        UrlInfo::CreateForTesting(pair.first),
diff --git a/content/shell/browser/shell_platform_delegate_ios.mm b/content/shell/browser/shell_platform_delegate_ios.mm
index 652a75be..e3e9ce53 100644
--- a/content/shell/browser/shell_platform_delegate_ios.mm
+++ b/content/shell/browser/shell_platform_delegate_ios.mm
@@ -126,6 +126,54 @@
                          alpha:1.0];
 }
 
+#if BUILDFLAG(IS_IOS_TVOS)
+// The following methods handle tvOS's focus engine by implementing the
+// following behavior:
+//
+// 1. The content view is focused and receives user input by default.
+// 2. Pressing the Menu button in the remote control switches focus to
+//    `_headerContentView` so that users can use the toolbar and the location
+//    bar.
+// 3. Pressing the Menu button again after that will switch to the home screen,
+//    and swiping down to focus the content view will reset the behavior
+//    described in 1).
+- (void)pressesBegan:(NSSet<UIPress*>*)presses
+           withEvent:(UIPressesEvent*)event {
+  for (UIPress* press in presses) {
+    if (press.type == UIPressTypeMenu) {
+      if (_shell->web_contents()->GetContentNativeView().Get().focused) {
+        _headerContentView.userInteractionEnabled = YES;
+        [self setNeedsFocusUpdate];
+        return;
+      }
+    }
+  }
+  [super pressesBegan:presses withEvent:event];
+}
+
+- (void)didUpdateFocusInContext:(UIFocusUpdateContext*)context
+       withAnimationCoordinator:(UIFocusAnimationCoordinator*)coordinator {
+  if (_shell) {
+    const UIView* native_web_contents_view =
+        _shell->web_contents()->GetContentNativeView().Get();
+    if (context.nextFocusedView == native_web_contents_view) {
+      _headerContentView.userInteractionEnabled = NO;
+      _shell->web_contents()->Focus();
+    }
+  }
+}
+
+- (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments {
+  // `userInteractionEnabled` is false when we create `_headerContentView` so
+  // that we focus on `_contentView` by default instead of the Back button in
+  // the toolbar.
+  // We set it to true when explicitly pressing the Back button on the remote
+  // control in order to focus the toolbar.
+  return _headerContentView.userInteractionEnabled ? @[ _headerContentView ]
+                                                   : @[ _contentView ];
+}
+#endif
+
 - (void)viewDidLoad {
   [super viewDidLoad];
 
@@ -168,6 +216,12 @@
   _headerBackgroundView.layoutMarginsRelativeArrangement = YES;
   _headerBackgroundView.preservesSuperviewLayoutMargins = YES;
 
+#if BUILDFLAG(IS_IOS_TVOS)
+  // On tvOS, make it impossible to focus `_headerContentView` by simply
+  // swiping up on the remote control since this behavior is not intuitive.
+  _headerContentView.userInteractionEnabled = NO;
+#endif
+
   _headerContentView.alignment = UIStackViewAlignmentCenter;
   _headerContentView.axis = UILayoutConstraintAxisHorizontal;
   _headerContentView.spacing = 16.0;
diff --git a/device/gamepad/gamepad_id_list.cc b/device/gamepad/gamepad_id_list.cc
index 869105c43..16885bc 100644
--- a/device/gamepad/gamepad_id_list.cc
+++ b/device/gamepad/gamepad_id_list.cc
@@ -613,6 +613,7 @@
     {{0x2dc8, 0x2830}, kXInputTypeNone},
     {{0x2dc8, 0x3000}, kXInputTypeNone},
     {{0x2dc8, 0x3001}, kXInputTypeNone},
+    {{0x2dc8, 0x301b}, kXInputTypeNone},
     {{0x2dc8, 0x3106}, kXInputTypeXbox360},
     {{0x2dc8, 0x3820}, kXInputTypeNone},
     {{0x2dc8, 0x9001}, kXInputTypeNone},
diff --git a/device/gamepad/gamepad_id_list.h b/device/gamepad/gamepad_id_list.h
index 191bd14..92b85c0 100644
--- a/device/gamepad/gamepad_id_list.h
+++ b/device/gamepad/gamepad_id_list.h
@@ -42,6 +42,7 @@
   // Fake IDs for devices which report as 0x0000 0x0000
   kPowerALicPro = 0x0000ff00,
   // ID values for supported devices.
+  k8BitDoProduct301b = 0x2dc8301b,
   k8BitDoProduct3106 = 0x2dc83106,
   kAcerProduct1304 = 0x05021304,
   kAcerProduct1305 = 0x05021305,
diff --git a/device/gamepad/gamepad_standard_mappings_mac.mm b/device/gamepad/gamepad_standard_mappings_mac.mm
index bd89f78..a7b33e84 100644
--- a/device/gamepad/gamepad_standard_mappings_mac.mm
+++ b/device/gamepad/gamepad_standard_mappings_mac.mm
@@ -139,6 +139,12 @@
   mapped->axes_length = AXIS_INDEX_COUNT;
 }
 
+void Mapper8BitDoBluetooth(const Gamepad& input, Gamepad* mapped) {
+  MapperXboxBluetooth(input, mapped);
+  mapped->buttons[BUTTON_INDEX_LEFT_TRIGGER] = AxisToButton(input.axes[4]);
+  mapped->buttons[BUTTON_INDEX_RIGHT_TRIGGER] = AxisToButton(input.axes[3]);
+}
+
 void MapperPlaystationSixAxis(const Gamepad& input, Gamepad* mapped) {
   *mapped = input;
   mapped->buttons[BUTTON_INDEX_PRIMARY] = input.buttons[14];
@@ -782,6 +788,8 @@
 } kAvailableMappings[] = {
     // PowerA Wireless Controller - Nintendo GameCube style
     {GamepadId::kPowerALicPro, MapperSwitchPro},
+    // 8BitDo Ultimate Wireless 2C (Bluetooth)
+    {GamepadId::k8BitDoProduct301b, Mapper8BitDoBluetooth},
     // Snakebyte iDroid:con
     {GamepadId::kBroadcomProduct8502, MapperSnakebyteIDroidCon},
     // DragonRise Generic USB
diff --git a/ios/chrome/browser/autofill/ui_bundled/autofill_ui_constants.h b/ios/chrome/browser/autofill/ui_bundled/autofill_ui_constants.h
index 691cff9..1f0adcc 100644
--- a/ios/chrome/browser/autofill/ui_bundled/autofill_ui_constants.h
+++ b/ios/chrome/browser/autofill/ui_bundled/autofill_ui_constants.h
@@ -13,6 +13,11 @@
 // the UI does.
 const base::TimeDelta kSelectSuggestionDelay = base::Milliseconds(500);
 
+// The delay between showing the confirmation and dismissing the progress
+// dialog.
+const base::TimeDelta kProgressDialogConfirmationDismissDelay =
+    base::Seconds(1);
+
 }  // namespace autofill_ui_constants
 
 #endif  // IOS_CHROME_BROWSER_AUTOFILL_UI_BUNDLED_AUTOFILL_UI_CONSTANTS_H_
diff --git a/ios/chrome/browser/autofill/ui_bundled/bottom_sheet/BUILD.gn b/ios/chrome/browser/autofill/ui_bundled/bottom_sheet/BUILD.gn
index fedcb52..0949bcd 100644
--- a/ios/chrome/browser/autofill/ui_bundled/bottom_sheet/BUILD.gn
+++ b/ios/chrome/browser/autofill/ui_bundled/bottom_sheet/BUILD.gn
@@ -165,6 +165,7 @@
     "//components/url_formatter",
     "//ios/chrome/app/strings",
     "//ios/chrome/browser/autofill/model:features",
+    "//ios/chrome/browser/autofill/ui_bundled:autofill_ui_constants",
     "//ios/chrome/browser/autofill/ui_bundled:eg_test_support+eg2",
     "//ios/chrome/browser/autofill/ui_bundled/authentication:card_unmask_authentication_selection_constants",
     "//ios/chrome/browser/autofill/ui_bundled/manual_fill:eg_test_support+eg2",
diff --git a/ios/chrome/browser/autofill/ui_bundled/bottom_sheet/payments_suggestion_bottom_sheet_egtest.mm b/ios/chrome/browser/autofill/ui_bundled/bottom_sheet/payments_suggestion_bottom_sheet_egtest.mm
index 3d8ab35..2a70828 100644
--- a/ios/chrome/browser/autofill/ui_bundled/bottom_sheet/payments_suggestion_bottom_sheet_egtest.mm
+++ b/ios/chrome/browser/autofill/ui_bundled/bottom_sheet/payments_suggestion_bottom_sheet_egtest.mm
@@ -16,6 +16,7 @@
 #import "components/url_formatter/elide_url.h"
 #import "ios/chrome/browser/autofill/model/features.h"
 #import "ios/chrome/browser/autofill/ui_bundled/autofill_app_interface.h"
+#import "ios/chrome/browser/autofill/ui_bundled/autofill_ui_constants.h"
 #import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_matchers.h"
 #import "ios/chrome/browser/metrics/model/metrics_app_interface.h"
 #import "ios/chrome/browser/settings/ui_bundled/settings_root_table_constants.h"
@@ -42,6 +43,11 @@
 const char kFormCardNumber[] = "CCNo";
 const char kFormCardExpirationMonth[] = "CCExpiresMonth";
 const char kFormCardExpirationYear[] = "CCExpiresYear";
+NSString* const kTriggeringRequestUrl =
+    @"https://payments.google.com/payments/apis-secure/creditcardservice/"
+    @"getrealpan?s7e_suffix=chromewallet";
+NSString* const kSuccessResponseNoAuthNeeded =
+    @"{ \"pan\": \"5411111111112109\" }";
 
 // Matcher for the credit card suggestion chip.
 id<GREYMatcher> KeyboardAccessoryCreditCardSuggestionChip() {
@@ -119,6 +125,14 @@
                  (testAttemptToOpenPaymentsBottomSheetWithoutCreditCardOnV3)]) {
     config.features_enabled.push_back(kAutofillPaymentsSheetV3Ios);
     config.features_enabled.push_back(kStatelessFormSuggestionController);
+  } else if ([self
+                 isRunningTest:@selector(testFillingFromKeyboardOnAutofocus)]) {
+    config.features_enabled.push_back(
+        autofill::features::kAutofillEnableFpanRiskBasedAuthentication);
+  } else if ([self isRunningTest:@selector
+                   (testUpdateBottomSheetOnAddServerCreditCard)]) {
+    config.features_enabled.push_back(
+        autofill::features::kAutofillEnableFpanRiskBasedAuthentication);
   }
   return config;
 }
@@ -462,6 +476,8 @@
 
   id<GREYMatcher> continueButton = WaitOnResponsiveContinueButton();
 
+  [AutofillAppInterface setUpFakeCreditCardServer];
+
   // Add a credit card to the Personal Data Manager.
   id<GREYMatcher> serverCreditCardEntry =
       grey_text([AutofillAppInterface saveMaskedCreditCard]);
@@ -491,9 +507,26 @@
 
   [[EarlGrey selectElementWithMatcher:continueButton] performAction:grey_tap()];
 
-  // Verify the CVC requester is visible.
-  [[EarlGrey selectElementWithMatcher:grey_text(@"Verification")]
-      assertWithMatcher:grey_notNil()];
+  // Wait for the progress dialog to appear.
+  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:
+                      chrome_test_util::StaticTextWithAccessibilityLabelId(
+                          IDS_AUTOFILL_CARD_UNMASK_PROGRESS_DIALOG_TITLE)];
+  // Fake the successful server response that triggers Dismiss.
+  [AutofillAppInterface setPaymentsResponse:kSuccessResponseNoAuthNeeded
+                                 forRequest:kTriggeringRequestUrl
+                              withErrorCode:net::HTTP_OK];
+  // This delay is the autodismiss delay (1 second) + extra time to avoid
+  // flakiness on the simulators (2 seconds).
+  const base::TimeDelta total_delay_for_dismiss =
+      autofill_ui_constants::kProgressDialogConfirmationDismissDelay +
+      base::Seconds(2);
+
+  // Wait for the dialog to disappear after the delay.
+  [ChromeEarlGrey
+      waitForUIElementToDisappearWithMatcher:
+          chrome_test_util::StaticTextWithAccessibilityLabelId(
+              IDS_AUTOFILL_CARD_UNMASK_PROGRESS_DIALOG_TITLE)
+                                     timeout:total_delay_for_dismiss];
 
   GREYAssertNil(
       [MetricsAppInterface
@@ -503,9 +536,6 @@
                              @"Autofill.TouchToFill.CreditCard.SelectedIndex"],
       @"Unexpected histogram error for touch to fill credit card selected "
       @"index");
-
-  // TODO(crbug.com/40577448): Figure out a way to enter CVC and get the
-  // unlocked card result.
 }
 
 // Tests that accessing a long press menu does not disable the bottom sheet.
@@ -798,6 +828,8 @@
   // for this test case.
   [AutofillAppInterface clearCreditCardStore];
 
+  [AutofillAppInterface setUpFakeCreditCardServer];
+
   // Add the server credit card. Before loading the page so it can be in the
   // autofill suggestion upon autofocusing the credit card field.
   [AutofillAppInterface saveMaskedCreditCard];
diff --git a/ios/chrome/browser/autofill/ui_bundled/progress_dialog/BUILD.gn b/ios/chrome/browser/autofill/ui_bundled/progress_dialog/BUILD.gn
index 3faac1e..499e33d 100644
--- a/ios/chrome/browser/autofill/ui_bundled/progress_dialog/BUILD.gn
+++ b/ios/chrome/browser/autofill/ui_bundled/progress_dialog/BUILD.gn
@@ -16,6 +16,7 @@
     "//components/strings",
     "//ios/chrome/browser/alert_view/ui_bundled",
     "//ios/chrome/browser/autofill/model:model_internal",
+    "//ios/chrome/browser/autofill/ui_bundled:autofill_ui_constants",
     "//ios/chrome/browser/autofill/ui_bundled:coordinator",
     "//ios/chrome/browser/shared/coordinator/chrome_coordinator",
     "//ios/chrome/browser/shared/model/browser",
diff --git a/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_egtest.mm b/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_egtest.mm
index c598ab7..c7de6a4 100644
--- a/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_egtest.mm
+++ b/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_egtest.mm
@@ -36,13 +36,6 @@
 
 }  // namespace
 
-// Matcher for the "Cancel" button.
-id<GREYMatcher> CancelButton() {
-  return grey_allOf(
-      chrome_test_util::CancelButton(),
-      grey_not(grey_accessibilityTrait(UIAccessibilityTraitNotEnabled)), nil);
-}
-
 @interface AutofillProgressDialogDismissEGTest : ChromeTestCase {
   NSString* _enrolledCardNameAndLastFour;
 }
@@ -126,13 +119,18 @@
                                  forRequest:kTriggeringRequestUrl
                               withErrorCode:net::HTTP_OK];
 
-  // Wait for the dialog to disappear after the delay. This delay is the
-  // autodismiss delay (1 second) + extra time to avoid flakiness on the
-  // simulators (2 seconds).
-  [ChromeEarlGrey waitForUIElementToDisappearWithMatcher:
-                      chrome_test_util::StaticTextWithAccessibilityLabelId(
-                          IDS_AUTOFILL_CARD_UNMASK_PROGRESS_DIALOG_TITLE)
-                                                 timeout:base::Seconds(3)];
+  // This delay is the autodismiss delay (1 second) + extra time to avoid
+  // flakiness on the simulators (2 seconds).
+  const base::TimeDelta total_delay_for_dismiss =
+      autofill_ui_constants::kProgressDialogConfirmationDismissDelay +
+      base::Seconds(2);
+
+  // Wait for the dialog to disappear after the delay.
+  [ChromeEarlGrey
+      waitForUIElementToDisappearWithMatcher:
+          chrome_test_util::StaticTextWithAccessibilityLabelId(
+              IDS_AUTOFILL_CARD_UNMASK_PROGRESS_DIALOG_TITLE)
+                                     timeout:total_delay_for_dismiss];
 }
 
 @end
diff --git a/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator.mm b/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator.mm
index 370938b..d243e45d 100644
--- a/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator.mm
+++ b/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator.mm
@@ -17,16 +17,11 @@
 #import "components/strings/grit/components_strings.h"
 #import "ios/chrome/browser/alert_view/ui_bundled/alert_action.h"
 #import "ios/chrome/browser/alert_view/ui_bundled/alert_consumer.h"
+#import "ios/chrome/browser/autofill/ui_bundled/autofill_ui_constants.h"
 #import "ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator_delegate.h"
 #import "ios/chrome/browser/shared/ui/symbols/symbols.h"
 #import "ui/base/l10n/l10n_util.h"
 
-namespace {
-// The delay between showing the confirmation and dismissing the progress
-// dialog.
-constexpr base::TimeDelta kConfirmationDismissDelay = base::Seconds(1);
-}  // namespace
-
 AutofillProgressDialogMediator::AutofillProgressDialogMediator(
     autofill::AutofillProgressDialogControllerImpl* model_controller,
     id<AutofillProgressDialogMediatorDelegate> delegate)
@@ -64,7 +59,8 @@
                      weak_ptr_factory_.GetWeakPtr());
 
   base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
-      FROM_HERE, std::move(closure), kConfirmationDismissDelay);
+      FROM_HERE, std::move(closure),
+      autofill_ui_constants::kProgressDialogConfirmationDismissDelay);
 }
 
 void AutofillProgressDialogMediator::InvalidateControllerForCallbacks() {
diff --git a/ios/chrome/browser/flags/about_flags.mm b/ios/chrome/browser/flags/about_flags.mm
index bef4eaed8..0d983729 100644
--- a/ios/chrome/browser/flags/about_flags.mm
+++ b/ios/chrome/browser/flags/about_flags.mm
@@ -2688,6 +2688,15 @@
      flags_ui::kOsIos,
      FEATURE_VALUE_TYPE(
          autofill::features::kAutofillEnableFpanRiskBasedAuthentication)},
+    {"autofill-enable-multiple-request-in-virtual-card-downstream-enrollment",
+     flag_descriptions::
+         kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollmentName,
+     flag_descriptions::
+         kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollmentDescription,
+     flags_ui::kOsIos,
+     FEATURE_VALUE_TYPE(
+         autofill::features::
+             kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollment)},
 };
 
 bool SkipConditionalFeatureEntry(const flags_ui::FeatureEntry& entry) {
diff --git a/ios/chrome/browser/flags/ios_chrome_flag_descriptions.cc b/ios/chrome/browser/flags/ios_chrome_flag_descriptions.cc
index 0828f56..bd2f4c5 100644
--- a/ios/chrome/browser/flags/ios_chrome_flag_descriptions.cc
+++ b/ios/chrome/browser/flags/ios_chrome_flag_descriptions.cc
@@ -102,6 +102,16 @@
     "all credit card form types and address form events will log to all "
     "address form types.";
 
+const char
+    kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollmentName[] =
+        "Enable multiple server request support for virtual card downstream "
+        "enrollment";
+const char
+    kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollmentDescription
+        [] = "When enabled, Chrome will be able to send preflight call for "
+             "enrollment earlier in the flow with the multiple server request "
+             "support.";
+
 const char kAutofillEnableFpanRiskBasedAuthenticationName[] =
     "Enable risk-based authentication for FPAN retrieval";
 const char kAutofillEnableFpanRiskBasedAuthenticationDescription[] =
diff --git a/ios/chrome/browser/flags/ios_chrome_flag_descriptions.h b/ios/chrome/browser/flags/ios_chrome_flag_descriptions.h
index efc1af6..4a6320f 100644
--- a/ios/chrome/browser/flags/ios_chrome_flag_descriptions.h
+++ b/ios/chrome/browser/flags/ios_chrome_flag_descriptions.h
@@ -72,6 +72,12 @@
 extern const char kAutofillEnableLogFormEventsToAllParsedFormTypesName[];
 extern const char kAutofillEnableLogFormEventsToAllParsedFormTypesDescription[];
 
+extern const char
+    kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollmentName[];
+extern const char
+    kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollmentDescription
+        [];
+
 extern const char kAutofillEnablePrefetchingRiskDataForRetrievalName[];
 extern const char kAutofillEnablePrefetchingRiskDataForRetrievalDescription[];
 
diff --git a/ios/chrome/browser/home_customization/coordinator/BUILD.gn b/ios/chrome/browser/home_customization/coordinator/BUILD.gn
index 26e1bb5..f27ae3f 100644
--- a/ios/chrome/browser/home_customization/coordinator/BUILD.gn
+++ b/ios/chrome/browser/home_customization/coordinator/BUILD.gn
@@ -24,6 +24,7 @@
     "//base",
     "//base:i18n",
     "//components/commerce/core:feature_list",
+    "//components/image_fetcher/core",
     "//components/prefs",
     "//ios/chrome/app/strings",
     "//ios/chrome/browser/content_suggestions/ui_bundled/set_up_list:utils",
@@ -32,6 +33,7 @@
     "//ios/chrome/browser/home_customization/model",
     "//ios/chrome/browser/home_customization/ui",
     "//ios/chrome/browser/home_customization/utils",
+    "//ios/chrome/browser/image_fetcher/model",
     "//ios/chrome/browser/ntp/ui_bundled:logo",
     "//ios/chrome/browser/parcel_tracking:features",
     "//ios/chrome/browser/shared/coordinator/alert",
@@ -71,6 +73,7 @@
     "//ios/chrome/browser/discover_feed/model:discover_feed_visibility_browser_agent",
     "//ios/chrome/browser/home_customization/ui",
     "//ios/chrome/browser/home_customization/utils",
+    "//ios/chrome/browser/image_fetcher/model",
     "//ios/chrome/browser/shared/model/browser/test:test_support",
     "//ios/chrome/browser/shared/model/prefs:browser_prefs",
     "//ios/chrome/browser/shared/model/prefs:pref_names",
diff --git a/ios/chrome/browser/home_customization/coordinator/DEPS b/ios/chrome/browser/home_customization/coordinator/DEPS
index 6d8f78b3..3ef772d 100644
--- a/ios/chrome/browser/home_customization/coordinator/DEPS
+++ b/ios/chrome/browser/home_customization/coordinator/DEPS
@@ -5,5 +5,6 @@
   "+ios/chrome/browser/parcel_tracking",
   "+ios/chrome/browser/content_suggestions/ui_bundled/set_up_list",
   "+ios/chrome/browser/ntp/ui_bundled",
-  "+third_party/material_color_utilities/src/cpp"
+  "+third_party/material_color_utilities/src/cpp",
+  "+ios/chrome/browser/image_fetcher",
 ]
diff --git a/ios/chrome/browser/home_customization/coordinator/home_customization_background_picker_action_sheet_coordinator.mm b/ios/chrome/browser/home_customization/coordinator/home_customization_background_picker_action_sheet_coordinator.mm
index c24960d..61f334a 100644
--- a/ios/chrome/browser/home_customization/coordinator/home_customization_background_picker_action_sheet_coordinator.mm
+++ b/ios/chrome/browser/home_customization/coordinator/home_customization_background_picker_action_sheet_coordinator.mm
@@ -4,6 +4,7 @@
 
 #import "ios/chrome/browser/home_customization/coordinator/home_customization_background_picker_action_sheet_coordinator.h"
 
+#import "components/image_fetcher/core/image_fetcher_service.h"
 #import "ios/chrome/browser/home_customization/coordinator/home_customization_background_color_picker_mediator.h"
 #import "ios/chrome/browser/home_customization/coordinator/home_customization_background_photo_picker_coordinator.h"
 #import "ios/chrome/browser/home_customization/coordinator/home_customization_background_preset_gallery_picker_mediator.h"
@@ -11,6 +12,7 @@
 #import "ios/chrome/browser/home_customization/ui/home_customization_background_photo_library_picker_view_controller.h"
 #import "ios/chrome/browser/home_customization/ui/home_customization_background_preset_gallery_picker_view_controller.h"
 #import "ios/chrome/browser/home_customization/ui/home_customization_logo_vendor_provider.h"
+#import "ios/chrome/browser/image_fetcher/model/image_fetcher_service_factory.h"
 #import "ios/chrome/browser/shared/model/browser/browser.h"
 #import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
 #import "ios/chrome/grit/ios_strings.h"
@@ -47,11 +49,14 @@
 
 - (void)start {
   __weak __typeof(self) weakSelf = self;
+  image_fetcher::ImageFetcherService* imageFetcherService =
+      ImageFetcherServiceFactory::GetForProfile(self.browser->GetProfile());
+
   _backgroundColorPickerMediator =
       [[HomeCustomizationBackgroundColorPickerMediator alloc] init];
-
   _backgroundPresetGalleryPickerMediator =
-      [[HomeCustomizationBackgroundPresetGalleryPickerMediator alloc] init];
+      [[HomeCustomizationBackgroundPresetGalleryPickerMediator alloc]
+          initWithImageFetcherService:imageFetcherService];
 
   [self
       addItemWithTitle:
diff --git a/ios/chrome/browser/home_customization/coordinator/home_customization_background_preset_gallery_picker_mediator.h b/ios/chrome/browser/home_customization/coordinator/home_customization_background_preset_gallery_picker_mediator.h
index 240f6b7d..84c1bc0 100644
--- a/ios/chrome/browser/home_customization/coordinator/home_customization_background_preset_gallery_picker_mediator.h
+++ b/ios/chrome/browser/home_customization/coordinator/home_customization_background_preset_gallery_picker_mediator.h
@@ -9,6 +9,10 @@
 
 #import "ios/chrome/browser/home_customization/ui/home_customization_background_preset_gallery_picker_mutator.h"
 
+namespace image_fetcher {
+class ImageFetcherService;
+}
+
 @protocol HomeCustomizationBackgroundPresetGalleryPickerConsumer;
 
 // A mediator that generates and configures background presets for the Home
@@ -21,6 +25,11 @@
     id<HomeCustomizationBackgroundPresetGalleryPickerConsumer>
         consumer;
 
+// Initializes a new instance of the background preset gallery picker mediator
+// with the provided image fetcher service.
+- (instancetype)initWithImageFetcherService:
+    (image_fetcher::ImageFetcherService*)imageFetcherService;
+
 // Provide a collection of background configurations to the consumer.
 - (void)configureBackgroundConfigurations;
 
diff --git a/ios/chrome/browser/home_customization/coordinator/home_customization_background_preset_gallery_picker_mediator.mm b/ios/chrome/browser/home_customization/coordinator/home_customization_background_preset_gallery_picker_mediator.mm
index ea8cc1fc..c9d1ebe 100644
--- a/ios/chrome/browser/home_customization/coordinator/home_customization_background_preset_gallery_picker_mediator.mm
+++ b/ios/chrome/browser/home_customization/coordinator/home_customization_background_preset_gallery_picker_mediator.mm
@@ -6,12 +6,32 @@
 
 #import <Foundation/Foundation.h>
 
+#import "components/image_fetcher/core/image_fetcher.h"
+#import "components/image_fetcher/core/image_fetcher_service.h"
 #import "ios/chrome/browser/home_customization/model/background_collection_configuration.h"
 #import "ios/chrome/browser/home_customization/model/background_customization_configuration.h"
 #import "ios/chrome/browser/home_customization/ui/home_customization_background_preset_gallery_picker_consumer.h"
+#import "ui/gfx/image/image.h"
+#import "url/gurl.h"
+
+@interface HomeCustomizationBackgroundPresetGalleryPickerMediator () {
+  // The image fetcher used to download individual background preset images.
+  raw_ptr<image_fetcher::ImageFetcher> _imageFetcher;
+}
+@end
 
 @implementation HomeCustomizationBackgroundPresetGalleryPickerMediator
 
+- (instancetype)initWithImageFetcherService:
+    (image_fetcher::ImageFetcherService*)imageFetcherService {
+  self = [super init];
+  if (self) {
+    _imageFetcher = imageFetcherService->GetImageFetcher(
+        image_fetcher::ImageFetcherConfig::kDiskCacheOnly);
+  }
+  return self;
+}
+
 - (void)configureBackgroundConfigurations {
   NSMutableArray<BackgroundCollectionConfiguration*>* configurations =
       [NSMutableArray array];
@@ -74,9 +94,32 @@
                               selectedBackgroundId:selectedBackgroundId];
 }
 
+#pragma mark - HomeCustomizationBackgroundPresetGalleryPickerMutator
+
 - (void)applyBackgroundForConfiguration:
     (BackgroundCustomizationConfiguration*)backgroundConfiguration {
   // TODO(crbug.com/408243803): apply NTP background configuration to NTP.
 }
 
+- (void)fetchBackgroundCustomizationThumbnailURLImage:(GURL)thumbnailURL
+                                           completion:
+                                               (void (^)(UIImage*))completion {
+  CHECK(!thumbnailURL.is_empty());
+  CHECK(thumbnailURL.is_valid());
+
+  _imageFetcher->FetchImage(
+      thumbnailURL,
+      base::BindOnce(^(const gfx::Image& image,
+                       const image_fetcher::RequestMetadata& metadata) {
+        if (!image.IsEmpty()) {
+          UIImage* uiImage = image.ToUIImage();
+          if (completion) {
+            completion(uiImage);
+          }
+        }
+      }),
+      // TODO (crbug.com/417234848): Add annotation.
+      image_fetcher::ImageFetcherParams(NO_TRAFFIC_ANNOTATION_YET, "Test"));
+}
+
 @end
diff --git a/ios/chrome/browser/home_customization/coordinator/home_customization_coordinator.mm b/ios/chrome/browser/home_customization/coordinator/home_customization_coordinator.mm
index 80ae099e..399b850 100644
--- a/ios/chrome/browser/home_customization/coordinator/home_customization_coordinator.mm
+++ b/ios/chrome/browser/home_customization/coordinator/home_customization_coordinator.mm
@@ -4,6 +4,7 @@
 
 #import "ios/chrome/browser/home_customization/coordinator/home_customization_coordinator.h"
 
+#import "components/image_fetcher/core/image_fetcher_service.h"
 #import "ios/chrome/browser/discover_feed/model/discover_feed_visibility_browser_agent.h"
 #import "ios/chrome/browser/home_customization/coordinator/home_customization_background_picker_action_sheet_coordinator.h"
 #import "ios/chrome/browser/home_customization/coordinator/home_customization_delegate.h"
@@ -15,6 +16,7 @@
 #import "ios/chrome/browser/home_customization/ui/home_customization_magic_stack_view_controller.h"
 #import "ios/chrome/browser/home_customization/ui/home_customization_main_view_controller.h"
 #import "ios/chrome/browser/home_customization/utils/home_customization_constants.h"
+#import "ios/chrome/browser/image_fetcher/model/image_fetcher_service_factory.h"
 #import "ios/chrome/browser/ntp/ui_bundled/logo_vendor.h"
 #import "ios/chrome/browser/shared/coordinator/alert/action_sheet_coordinator.h"
 #import "ios/chrome/browser/shared/model/browser/browser.h"
@@ -72,10 +74,14 @@
 #pragma mark - ChromeCoordinator
 
 - (void)start {
+  image_fetcher::ImageFetcherService* imageFetcherService =
+      ImageFetcherServiceFactory::GetForProfile(self.browser->GetProfile());
+
   _mediator = [[HomeCustomizationMediator alloc]
                      initWithPrefService:self.profile->GetPrefs()
       discoverFeedVisibilityBrowserAgent:DiscoverFeedVisibilityBrowserAgent::
-                                             FromBrowser(self.browser)];
+                                             FromBrowser(self.browser)
+                     imageFetcherService:imageFetcherService];
   _mediator.navigationDelegate = self;
 
   // The Customization menu consists of a stack of presenting view controllers.
diff --git a/ios/chrome/browser/home_customization/coordinator/home_customization_mediator.h b/ios/chrome/browser/home_customization/coordinator/home_customization_mediator.h
index f843968..3f844cf 100644
--- a/ios/chrome/browser/home_customization/coordinator/home_customization_mediator.h
+++ b/ios/chrome/browser/home_customization/coordinator/home_customization_mediator.h
@@ -16,6 +16,10 @@
 @protocol HomeCustomizationNavigationDelegate;
 class PrefService;
 
+namespace image_fetcher {
+class ImageFetcherService;
+}
+
 // The mediator for the Home surface's customization menu.
 @interface HomeCustomizationMediator : NSObject <HomeCustomizationMutator>
 
@@ -23,6 +27,8 @@
 - (instancetype)initWithPrefService:(PrefService*)prefService
     discoverFeedVisibilityBrowserAgent:
         (DiscoverFeedVisibilityBrowserAgent*)discoverFeedVisibilityBrowserAgent
+                   imageFetcherService:
+                       (image_fetcher::ImageFetcherService*)imageFetcherService
     NS_DESIGNATED_INITIALIZER;
 
 - (instancetype)init NS_UNAVAILABLE;
diff --git a/ios/chrome/browser/home_customization/coordinator/home_customization_mediator.mm b/ios/chrome/browser/home_customization/coordinator/home_customization_mediator.mm
index 9701bab0..c15f2dc 100644
--- a/ios/chrome/browser/home_customization/coordinator/home_customization_mediator.mm
+++ b/ios/chrome/browser/home_customization/coordinator/home_customization_mediator.mm
@@ -9,6 +9,8 @@
 #import "base/strings/sys_string_conversions.h"
 #import "base/strings/utf_string_conversions.h"
 #import "components/commerce/core/commerce_feature_list.h"
+#import "components/image_fetcher/core/image_fetcher.h"
+#import "components/image_fetcher/core/image_fetcher_service.h"
 #import "components/prefs/pref_service.h"
 #import "ios/chrome/browser/content_suggestions/ui_bundled/set_up_list/utils.h"
 #import "ios/chrome/browser/discover_feed/model/discover_feed_visibility_browser_agent.h"
@@ -24,6 +26,7 @@
 #import "ios/chrome/browser/parcel_tracking/features.h"
 #import "ios/chrome/browser/shared/model/prefs/pref_names.h"
 #import "ios/chrome/browser/shared/public/features/features.h"
+#import "ui/gfx/image/image.h"
 #import "url/gurl.h"
 
 @implementation HomeCustomizationMediator {
@@ -32,15 +35,21 @@
   // Browser agent to be notified of Discover eligibility.
   raw_ptr<DiscoverFeedVisibilityBrowserAgent>
       _discoverFeedVisibilityBrowserAgent;
+  // The image fetcher used to download individual background images.
+  raw_ptr<image_fetcher::ImageFetcher> _imageFetcher;
 }
 
 - (instancetype)initWithPrefService:(PrefService*)prefService
-    discoverFeedVisibilityBrowserAgent:(DiscoverFeedVisibilityBrowserAgent*)
-                                           discoverFeedVisibilityBrowserAgent {
+    discoverFeedVisibilityBrowserAgent:
+        (DiscoverFeedVisibilityBrowserAgent*)discoverFeedVisibilityBrowserAgent
+                   imageFetcherService:(image_fetcher::ImageFetcherService*)
+                                           imageFetcherService {
   self = [super init];
   if (self) {
     _prefService = prefService;
     _discoverFeedVisibilityBrowserAgent = discoverFeedVisibilityBrowserAgent;
+    _imageFetcher = imageFetcherService->GetImageFetcher(
+        image_fetcher::ImageFetcherConfig::kDiskCacheOnly);
   }
   return self;
 }
@@ -261,4 +270,25 @@
   // TODO(crbug.com/408243803): apply NTP background configuration to NTP.
 }
 
+- (void)fetchBackgroundCustomizationThumbnailURLImage:(GURL)thumbnailURL
+                                           completion:
+                                               (void (^)(UIImage*))completion {
+  CHECK(!thumbnailURL.is_empty());
+  CHECK(thumbnailURL.is_valid());
+
+  _imageFetcher->FetchImage(
+      thumbnailURL,
+      base::BindOnce(^(const gfx::Image& image,
+                       const image_fetcher::RequestMetadata& metadata) {
+        if (!image.IsEmpty()) {
+          UIImage* uiImage = image.ToUIImage();
+          if (completion) {
+            completion(uiImage);
+          }
+        }
+      }),
+      // TODO (crbug.com/417234848): Add annotation.
+      image_fetcher::ImageFetcherParams(NO_TRAFFIC_ANNOTATION_YET, "Test"));
+}
+
 @end
diff --git a/ios/chrome/browser/home_customization/coordinator/home_customization_mediator_unittest.mm b/ios/chrome/browser/home_customization/coordinator/home_customization_mediator_unittest.mm
index 1da80c5..8c966c1 100644
--- a/ios/chrome/browser/home_customization/coordinator/home_customization_mediator_unittest.mm
+++ b/ios/chrome/browser/home_customization/coordinator/home_customization_mediator_unittest.mm
@@ -11,6 +11,7 @@
 #import "ios/chrome/browser/discover_feed/model/discover_feed_visibility_browser_agent.h"
 #import "ios/chrome/browser/home_customization/ui/home_customization_main_consumer.h"
 #import "ios/chrome/browser/home_customization/utils/home_customization_constants.h"
+#import "ios/chrome/browser/image_fetcher/model/image_fetcher_service_factory.h"
 #import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
 #import "ios/chrome/browser/shared/model/prefs/browser_prefs.h"
 #import "ios/chrome/browser/shared/model/prefs/pref_names.h"
@@ -62,10 +63,14 @@
     pref_service_ = profile_->GetPrefs();
     discover_feed_visibility_browser_agent_ =
         DiscoverFeedVisibilityBrowserAgent::FromBrowser(browser);
-    mediator_ =
-        [[HomeCustomizationMediator alloc] initWithPrefService:pref_service_
-                            discoverFeedVisibilityBrowserAgent:
-                                discover_feed_visibility_browser_agent_];
+    imageFetcherService_ =
+        ImageFetcherServiceFactory::GetForProfile(browser->GetProfile());
+
+    mediator_ = [[HomeCustomizationMediator alloc]
+                       initWithPrefService:pref_service_
+        discoverFeedVisibilityBrowserAgent:
+            discover_feed_visibility_browser_agent_
+                       imageFetcherService:imageFetcherService_];
   }
 
  protected:
@@ -75,6 +80,7 @@
       discover_feed_visibility_browser_agent_;
   raw_ptr<PrefService> pref_service_;
   std::unique_ptr<TestProfileIOS> profile_;
+  raw_ptr<image_fetcher::ImageFetcherService> imageFetcherService_;
 };
 
 // Tests that the mediator populates the main page data for its consumer based
diff --git a/ios/chrome/browser/home_customization/ui/BUILD.gn b/ios/chrome/browser/home_customization/ui/BUILD.gn
index d43e099..ceb1a197 100644
--- a/ios/chrome/browser/home_customization/ui/BUILD.gn
+++ b/ios/chrome/browser/home_customization/ui/BUILD.gn
@@ -62,6 +62,7 @@
     "//ios/chrome/common/ui/util:dynamic_type_util",
     "//ios/public/provider/chrome/browser/ui_utils:ui_utils_api",
     "//ui/base",
+    "//url",
   ]
   frameworks = [ "UIKit.framework" ]
 }
diff --git a/ios/chrome/browser/home_customization/ui/home_customization_background_cell.h b/ios/chrome/browser/home_customization/ui/home_customization_background_cell.h
index 623cffc..bac48c9 100644
--- a/ios/chrome/browser/home_customization/ui/home_customization_background_cell.h
+++ b/ios/chrome/browser/home_customization/ui/home_customization_background_cell.h
@@ -26,6 +26,9 @@
             (BackgroundCustomizationConfiguration*)backgroundConfiguration
                            logoVendor:(id<LogoVendor>)logoVendor;
 
+// Updates the background image displayed behind the cell’s content.
+- (void)updateBackgroundImage:(UIImage*)image;
+
 @end
 
 #endif  // IOS_CHROME_BROWSER_HOME_CUSTOMIZATION_UI_HOME_CUSTOMIZATION_BACKGROUND_CELL_H_
diff --git a/ios/chrome/browser/home_customization/ui/home_customization_background_cell.mm b/ios/chrome/browser/home_customization/ui/home_customization_background_cell.mm
index 4cd277a..05a8848 100644
--- a/ios/chrome/browser/home_customization/ui/home_customization_background_cell.mm
+++ b/ios/chrome/browser/home_customization/ui/home_customization_background_cell.mm
@@ -57,6 +57,9 @@
 @implementation HomeCustomizationBackgroundCell {
   // Associated background configuration.
   BackgroundCustomizationConfiguration* _backgroundConfiguration;
+
+  // The background image of the cell.
+  UIImageView* _backgroundImageView;
 }
 
 - (instancetype)initWithFrame:(CGRect)frame {
@@ -81,6 +84,15 @@
     self.innerContentView.layer.cornerRadius = kContentViewCornerRadius;
     self.innerContentView.layer.masksToBounds = YES;
     self.innerContentView.axis = UILayoutConstraintAxisVertical;
+
+    // Adds the empty background image.
+    _backgroundImageView = [[UIImageView alloc] init];
+    _backgroundImageView.contentMode = UIViewContentModeScaleAspectFill;
+    _backgroundImageView.clipsToBounds = YES;
+    _backgroundImageView.translatesAutoresizingMaskIntoConstraints = NO;
+    [self.innerContentView addSubview:_backgroundImageView];
+    AddSameConstraints(_backgroundImageView, self.innerContentView);
+
     [self.borderWrapperView addSubview:self.innerContentView];
 
     // Constraints for positioning the border wrapper view inside the cell.
@@ -126,6 +138,10 @@
   ]];
 }
 
+- (void)updateBackgroundImage:(UIImage*)image {
+  [_backgroundImageView setImage:image];
+}
+
 #pragma mark - Setters
 
 - (void)setSelected:(BOOL)selected {
diff --git a/ios/chrome/browser/home_customization/ui/home_customization_background_preset_gallery_picker_mutator.h b/ios/chrome/browser/home_customization/ui/home_customization_background_preset_gallery_picker_mutator.h
index ac2f2af..82ffce71 100644
--- a/ios/chrome/browser/home_customization/ui/home_customization_background_preset_gallery_picker_mutator.h
+++ b/ios/chrome/browser/home_customization/ui/home_customization_background_preset_gallery_picker_mutator.h
@@ -16,6 +16,14 @@
 - (void)applyBackgroundForConfiguration:
     (BackgroundCustomizationConfiguration*)backgroundConfiguration;
 
+// Downloads and returns a thumbnail image from the given GURL. The image is
+// returned asynchronously through the `completion` block. The method is
+// intended to be used for background customization thumbnails, such as loading
+// preview images for a collection view cell when it becomes visible.
+- (void)fetchBackgroundCustomizationThumbnailURLImage:(GURL)thumbnailURL
+                                           completion:
+                                               (void (^)(UIImage*))completion;
+
 @end
 
 #endif  // IOS_CHROME_BROWSER_HOME_CUSTOMIZATION_UI_HOME_CUSTOMIZATION_BACKGROUND_PRESET_GALLERY_PICKER_MUTATOR_H_
diff --git a/ios/chrome/browser/home_customization/ui/home_customization_background_preset_gallery_picker_view_controller.mm b/ios/chrome/browser/home_customization/ui/home_customization_background_preset_gallery_picker_view_controller.mm
index 6041ce3..f913007 100644
--- a/ios/chrome/browser/home_customization/ui/home_customization_background_preset_gallery_picker_view_controller.mm
+++ b/ios/chrome/browser/home_customization/ui/home_customization_background_preset_gallery_picker_view_controller.mm
@@ -20,6 +20,7 @@
 #import "ios/chrome/common/ui/util/constraints_ui_util.h"
 #import "ios/chrome/grit/ios_strings.h"
 #import "ui/base/l10n/l10n_util_mac.h"
+#import "url/gurl.h"
 
 // Define constants within the namespace
 namespace {
@@ -181,6 +182,24 @@
   [self.mutator applyBackgroundForConfiguration:backgroundConfiguration];
 }
 
+- (void)collectionView:(UICollectionView*)collectionView
+       willDisplayCell:(HomeCustomizationBackgroundCell*)cell
+    forItemAtIndexPath:(NSIndexPath*)indexPath {
+  NSString* itemIdentifier =
+      [_diffableDataSource itemIdentifierForIndexPath:indexPath];
+  BackgroundCustomizationConfiguration* backgroundConfiguration =
+      _backgroundCustomizationConfigurationMap[itemIdentifier];
+
+  if (!backgroundConfiguration.thumbnailURL.is_empty()) {
+    [self.mutator
+        fetchBackgroundCustomizationThumbnailURLImage:backgroundConfiguration
+                                                          .thumbnailURL
+                                           completion:^(UIImage* image) {
+                                             [cell updateBackgroundImage:image];
+                                           }];
+  }
+}
+
 #pragma mark - HomeCustomizationViewControllerProtocol
 
 - (NSCollectionLayoutSection*)
diff --git a/ios/chrome/browser/home_customization/ui/home_customization_main_view_controller.mm b/ios/chrome/browser/home_customization/ui/home_customization_main_view_controller.mm
index 56538b9e1..80bd555 100644
--- a/ios/chrome/browser/home_customization/ui/home_customization_main_view_controller.mm
+++ b/ios/chrome/browser/home_customization/ui/home_customization_main_view_controller.mm
@@ -224,6 +224,34 @@
   [self.mutator applyBackgroundForConfiguration:backgroundConfiguration];
 }
 
+- (void)collectionView:(UICollectionView*)collectionView
+       willDisplayCell:(UICollectionViewCell*)cell
+    forItemAtIndexPath:(NSIndexPath*)indexPath {
+  CustomizationSection* section =
+      [self.diffableDataSource snapshot].sectionIdentifiers[indexPath.section];
+  NSString* itemIdentifier =
+      [_diffableDataSource itemIdentifierForIndexPath:indexPath];
+  if (![section isEqualToString:kCustomizationSectionBackground] ||
+      ![cell isKindOfClass:[HomeCustomizationBackgroundCell class]]) {
+    return;
+  }
+
+  BackgroundCustomizationConfiguration* backgroundConfiguration =
+      _backgroundCustomizationConfigurationMap[itemIdentifier];
+
+  if (backgroundConfiguration &&
+      !backgroundConfiguration.thumbnailURL.is_empty()) {
+    [self.mutator
+        fetchBackgroundCustomizationThumbnailURLImage:backgroundConfiguration
+                                                          .thumbnailURL
+                                           completion:^(UIImage* image) {
+                                             [(HomeCustomizationBackgroundCell*)
+                                                     cell
+                                                 updateBackgroundImage:image];
+                                           }];
+  }
+}
+
 #pragma mark - HomeCustomizationMainConsumer
 
 - (void)populateToggles:(std::map<CustomizationToggleType, BOOL>)toggleMap {
diff --git a/ios/chrome/browser/home_customization/ui/home_customization_mutator.h b/ios/chrome/browser/home_customization/ui/home_customization_mutator.h
index 1a80e51e..ff15a4b 100644
--- a/ios/chrome/browser/home_customization/ui/home_customization_mutator.h
+++ b/ios/chrome/browser/home_customization/ui/home_customization_mutator.h
@@ -30,6 +30,13 @@
 - (void)applyBackgroundForConfiguration:
     (BackgroundCustomizationConfiguration*)backgroundConfiguration;
 
+// Downloads and returns a thumbnail image from the given GURL. The image is
+// returned asynchronously through the `completion` block. The method is
+// intended to be used for background customization thumbnails, such as loading
+// preview images for a collection view cell when it becomes visible.
+- (void)fetchBackgroundCustomizationThumbnailURLImage:(GURL)thumbnailURL
+                                           completion:
+                                               (void (^)(UIImage*))completion;
 @end
 
 #endif  // IOS_CHROME_BROWSER_HOME_CUSTOMIZATION_UI_HOME_CUSTOMIZATION_MUTATOR_H_
diff --git a/ios/chrome/browser/passwords/model/BUILD.gn b/ios/chrome/browser/passwords/model/BUILD.gn
index 7a50044..ac5fda2 100644
--- a/ios/chrome/browser/passwords/model/BUILD.gn
+++ b/ios/chrome/browser/passwords/model/BUILD.gn
@@ -91,6 +91,7 @@
     "//ios/chrome/browser/safe_browsing/model",
     "//ios/chrome/browser/shared/model/application_context",
     "//ios/chrome/browser/shared/model/browser",
+    "//ios/chrome/browser/shared/model/prefs:pref_names",
     "//ios/chrome/browser/shared/model/profile",
     "//ios/chrome/browser/shared/model/profile:profile_keyed_service_factory",
     "//ios/chrome/browser/shared/public/commands",
diff --git a/ios/chrome/browser/passwords/model/ios_chrome_password_manager_client.mm b/ios/chrome/browser/passwords/model/ios_chrome_password_manager_client.mm
index fd02049..338bb04f 100644
--- a/ios/chrome/browser/passwords/model/ios_chrome_password_manager_client.mm
+++ b/ios/chrome/browser/passwords/model/ios_chrome_password_manager_client.mm
@@ -41,6 +41,7 @@
 #import "ios/chrome/browser/safe_browsing/model/chrome_password_protection_service.h"
 #import "ios/chrome/browser/safe_browsing/model/chrome_password_protection_service_factory.h"
 #import "ios/chrome/browser/shared/model/application_context/application_context.h"
+#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
 #import "ios/chrome/browser/shared/model/profile/profile_ios.h"
 #import "ios/chrome/browser/shared/public/commands/credential_provider_promo_commands.h"
 #import "ios/chrome/browser/shared/public/features/features.h"
@@ -225,6 +226,8 @@
   [bridge_
       showCredentialProviderPromo:CredentialProviderPromoTrigger::
                                       SuccessfulLoginUsingExistingPassword];
+  GetLocalStatePrefs()->SetTime(prefs::kIosSuccessfulLoginWithExistingPassword,
+                                base::Time::Now());
 }
 
 bool IOSChromePasswordManagerClient::IsPasswordChangeOngoing() {
diff --git a/ios/chrome/browser/passwords/model/ios_chrome_password_manager_client_unittest.mm b/ios/chrome/browser/passwords/model/ios_chrome_password_manager_client_unittest.mm
index 9acd2665..5565e3b 100644
--- a/ios/chrome/browser/passwords/model/ios_chrome_password_manager_client_unittest.mm
+++ b/ios/chrome/browser/passwords/model/ios_chrome_password_manager_client_unittest.mm
@@ -33,6 +33,7 @@
 #import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
 #import "ios/chrome/browser/shared/public/commands/credential_provider_promo_commands.h"
 #import "ios/chrome/browser/web/model/chrome_web_client.h"
+#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
 #import "ios/web/public/browser_state.h"
 #import "ios/web/public/test/scoped_testing_web_client.h"
 #import "ios/web/public/test/web_state_test_util.h"
@@ -123,6 +124,7 @@
 
   web::WebState* web_state() { return web_state_.get(); }
 
+  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
   web::ScopedTestingWebClient web_client_;
   web::WebTaskEnvironment task_environment_;
   std::unique_ptr<TestProfileIOS> profile_;
diff --git a/ios/chrome/browser/shared/model/prefs/browser_prefs.mm b/ios/chrome/browser/shared/model/prefs/browser_prefs.mm
index 3208858..c6e51b8d 100644
--- a/ios/chrome/browser/shared/model/prefs/browser_prefs.mm
+++ b/ios/chrome/browser/shared/model/prefs/browser_prefs.mm
@@ -516,6 +516,9 @@
   registry->RegisterBooleanPref(prefs::kIosCredentialProviderPromoPolicyEnabled,
                                 true);
 
+  registry->RegisterTimePref(prefs::kIosSuccessfulLoginWithExistingPassword,
+                             base::Time());
+
   registry->RegisterTimePref(prefs::kIosDefaultBrowserBlueDotPromoFirstDisplay,
                              base::Time());
 
diff --git a/ios/chrome/browser/shared/model/prefs/pref_names.h b/ios/chrome/browser/shared/model/prefs/pref_names.h
index dfc5b8cf..c5232aaf 100644
--- a/ios/chrome/browser/shared/model/prefs/pref_names.h
+++ b/ios/chrome/browser/shared/model/prefs/pref_names.h
@@ -211,6 +211,11 @@
 inline constexpr char kIosCredentialProviderPromoDisplayTime[] =
     "ios.credential_provider_promo_display_time";
 
+// The timestamp of the last time the user had a successful login with an
+// existing saved password.
+inline constexpr char kIosSuccessfulLoginWithExistingPassword[] =
+    "ios.successful_login_with_existing_password";
+
 // Boolean that is true when the CredentialProviderPromoEnabled policy is
 // enabled.
 inline constexpr char kIosCredentialProviderPromoPolicyEnabled[] =
diff --git a/ios/chrome/browser/tips_notifications/model/tips_notification_client.mm b/ios/chrome/browser/tips_notifications/model/tips_notification_client.mm
index 6490803a9..7b9cfd00 100644
--- a/ios/chrome/browser/tips_notifications/model/tips_notification_client.mm
+++ b/ios/chrome/browser/tips_notifications/model/tips_notification_client.mm
@@ -61,7 +61,10 @@
 // The amount of time used to determine if Lens was opened recently.
 const base::TimeDelta kLensOpenedRecency = base::Days(30);
 // The amount of time used to determine if the CPE promo was displayed recently.
-const base::TimeDelta kCPEPromoRecency = base::Days(30);
+const base::TimeDelta kCPEPromoRecency = base::Days(7);
+// The amount of time used to determine if the user successfully logged in
+// recently.
+const base::TimeDelta kSuccessfullLoginRecency = base::Days(30);
 // The amount of time used to determine if the user should be classified.
 const base::TimeDelta kClassifyUserRecency = base::Hours(2);
 
@@ -591,9 +594,9 @@
   if (IsRecent(promo_display_time, kCPEPromoRecency)) {
     return false;
   }
-  // TODO(crbug.com/417940156): Refine CPE trigger criteria to include:
-  //   * have used autofill in the last 30 days.
-  return true;
+  base::Time login_time =
+      local_state_->GetTime(prefs::kIosSuccessfulLoginWithExistingPassword);
+  return IsRecent(login_time, kSuccessfullLoginRecency);
 }
 
 bool TipsNotificationClient::IsSceneLevelForegroundActive() {
diff --git a/ios/chrome/browser/tips_notifications/model/tips_notification_client_unittest.mm b/ios/chrome/browser/tips_notifications/model/tips_notification_client_unittest.mm
index 6505763..f5e02b34 100644
--- a/ios/chrome/browser/tips_notifications/model/tips_notification_client_unittest.mm
+++ b/ios/chrome/browser/tips_notifications/model/tips_notification_client_unittest.mm
@@ -779,7 +779,7 @@
 }
 
 // Tests that the client can register a CPE Promo notification, only when the
-// CPE promo was displayed more than 30 days ago.
+// CPE promo was displayed more than 7 days ago.
 TEST_F(TipsNotificationClientTest, CPERequest) {
   base::test::ScopedFeatureList feature_list;
   feature_list.InitAndEnableFeature(kIOSExpandedTips);
@@ -797,6 +797,8 @@
   });
   PrefService* local_state = GetApplicationContext()->GetLocalState();
   local_state->SetTime(prefs::kIosCredentialProviderPromoDisplayTime,
+                       base::Time::Now() - base::Days(6));
+  local_state->SetTime(prefs::kIosSuccessfulLoginWithExistingPassword,
                        base::Time::Now() - base::Days(29));
 
   // A notification should not be requested yet because promo display time is
@@ -809,7 +811,7 @@
 
   // Simulate that the CPE promo was displayed more than 30 days ago.
   local_state->SetTime(prefs::kIosCredentialProviderPromoDisplayTime,
-                       base::Time::Now() - base::Days(31));
+                       base::Time::Now() - base::Days(8));
   SetupMockNotificationCenter();
   StubGetPendingRequests(nil);
   ExpectNotificationRequest(TipsNotificationType::kCPE);
diff --git a/media/base/cdm_capability.cc b/media/base/cdm_capability.cc
index 93dad433..9a899f2 100644
--- a/media/base/cdm_capability.cc
+++ b/media/base/cdm_capability.cc
@@ -79,20 +79,12 @@
       return "kDisconnectionError";
     case CdmCapabilityQueryStatus::kMediaFoundationGetCdmFactoryFailed:
       return "kMediaFoundationGetCdmFactoryFailed. For the actual error code, "
-             "please check out "
-             "about://histograms/"
-             "#Media.EME.{KeySystem}.CdmCapabilityQueryStatus." +
-             std::string(kMediaFoundationGetCdmFactoryHresultUmaPostfix) +
-             " where KeySystem is a key "
-             "system.";
+             "please check out about://histograms/#" +
+             std::string(kMediaFoundationGetCdmFactoryHresultUmaPostfix);
     case CdmCapabilityQueryStatus::kCreateDummyMediaFoundationCdmFailed:
       return "kCreateDummyMediaFoundationCdmFailed. For the actual error code, "
-             "please check out "
-             "about://histograms/"
-             "#Media.EME.{KeySystem}.CdmCapabilityQueryStatus." +
-             std::string(kCreateDummyMediaFoundationCdmHresultUmaPostfix) +
-             " where KeySystem is a key "
-             "system.";
+             "please check out about://histograms/#" +
+             std::string(kCreateDummyMediaFoundationCdmHresultUmaPostfix);
     case CdmCapabilityQueryStatus::kUnexpectedEmptyCapability:
       return "kUnexpectedEmptyCapability";
     case CdmCapabilityQueryStatus::kNoMediaDrmSupport:
diff --git a/media/gpu/vaapi/vaapi_video_encode_accelerator_unittest.cc b/media/gpu/vaapi/vaapi_video_encode_accelerator_unittest.cc
index 1eb4ccd..6c5703c 100644
--- a/media/gpu/vaapi/vaapi_video_encode_accelerator_unittest.cc
+++ b/media/gpu/vaapi/vaapi_video_encode_accelerator_unittest.cc
@@ -12,6 +12,7 @@
 #include "base/bits.h"
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/gmock_callback_support.h"
 #include "base/test/task_environment.h"
 #include "build/build_config.h"
diff --git a/media/mojo/services/mojo_video_encode_accelerator_service.cc b/media/mojo/services/mojo_video_encode_accelerator_service.cc
index 0449902..74ee2b3 100644
--- a/media/mojo/services/mojo_video_encode_accelerator_service.cc
+++ b/media/mojo/services/mojo_video_encode_accelerator_service.cc
@@ -8,6 +8,7 @@
 #include <utility>
 
 #include "base/logging.h"
+#include "base/strings/stringprintf.h"
 #include "base/task/bind_post_task.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/trace_event/trace_event.h"
diff --git a/mojo/golden/generate.py b/mojo/golden/generate.py
index 6c6361c..b19c5c9e 100755
--- a/mojo/golden/generate.py
+++ b/mojo/golden/generate.py
@@ -17,7 +17,6 @@
 _GENERATOR_SCRIPT = os.path.join(
     _SCRIPT_DIR, '../public/tools/bindings/mojom_bindings_generator.py')
 
-
 def removesuffix(string, suffix):
     if not suffix or not string.endswith(suffix):
         return string
@@ -49,10 +48,16 @@
         ],
                        check=True)
 
-        for lang in ['typescript', 'c++']:
+        for lang in ['typescript', 'c++', 'java']:
+          language_flags = []
+
           lang_tmp_output = f'{tmp_bindings_dir}/{lang}'
           lang_output = f'{output_dir}/{lang}'
 
+          if lang == 'java':
+            language_flags += ['--java_output_directory=' + lang_tmp_output]
+
+
           # Paths to module files relative to the bindings output directory.
           mojom_modules = (os.path.join('../../modules',
                             removesuffix(module_filename, '-module'))
@@ -62,12 +67,13 @@
               '--bytecode_path', tmp_bytecode_dir, '--generators', lang,
               # typemap is hardcoded for now.
               '--typemap', f'{input_dir}/typemap.json',
-              *mojom_modules
-          ],
+              *language_flags, *mojom_modules],
                          check=True)
           # Append '.golden' file extension to avoid presubmit checks.
-          for entry in os.scandir(lang_tmp_output):
-              os.rename(entry.path, entry.path + '.golden')
+          for root, dirs, files in os.walk(lang_tmp_output):
+              for file in files:
+                path = root + '/'.join(dirs) + '/' + file
+                os.rename(path, path + '.golden')
           shutil.copytree(lang_tmp_output, lang_output, dirs_exist_ok=True)
 
 
diff --git a/mojo/golden/generated/c++/results.test-mojom.cc.golden b/mojo/golden/generated/c++/results.test-mojom.cc.golden
index 5470928..25faeff 100644
--- a/mojo/golden/generated/c++/results.test-mojom.cc.golden
+++ b/mojo/golden/generated/c++/results.test-mojom.cc.golden
@@ -734,15 +734,18 @@
   mojo::internal::MessageFragment<decltype(params->result)>
       result_fragment(params.message());
   result_fragment.Claim(&params->result);
+  
   mojo::internal::Serialize<::golden::ResultInterface_SyncMethod_ResponseParam_ResultDataView>(
-      in_result,
-      result_fragment,
-      true);
+    in_result,
+    result_fragment,
+    true);
 
-  MOJO_INTERNAL_DLOG_SERIALIZATION_WARNING(
-      params->result.is_null(),
-      mojo::internal::VALIDATION_ERROR_UNEXPECTED_NULL_POINTER,
-      "null result in ");
+  
+  MOJO_INTERNAL_CHECK_SERIALIZATION(
+    mojo::internal::SendValidation::kDefault,
+    !(params->result.is_null()),
+    mojo::internal::VALIDATION_ERROR_UNEXPECTED_NULL_POINTER,
+    "null result in ");
 
 #if defined(ENABLE_IPC_FUZZER)
   message.set_interface_name(ResultInterface::Name_);
diff --git a/mojo/golden/generated/java/org/chromium/golden/BasicStruct.java.golden b/mojo/golden/generated/java/org/chromium/golden/BasicStruct.java.golden
new file mode 100644
index 0000000..ed434ad
--- /dev/null
+++ b/mojo/golden/generated/java/org/chromium/golden/BasicStruct.java.golden
@@ -0,0 +1,81 @@
+// BasicStruct.java is auto generated by mojom_bindings_generator.py, do not edit
+
+
+// Copyright 2014 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 autogenerated by:
+//     mojo/public/tools/bindings/mojom_bindings_generator.py
+// For:
+//     basic_struct.test-mojom
+//
+
+package org.chromium.golden;
+
+import androidx.annotation.IntDef;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+
+@NullMarked
+@SuppressWarnings("NullAway")
+public final class BasicStruct extends org.chromium.mojo.bindings.Struct {
+
+    private static final int STRUCT_SIZE = 16;
+    private static final org.chromium.mojo.bindings.DataHeader[] VERSION_ARRAY = new org.chromium.mojo.bindings.DataHeader[] {new org.chromium.mojo.bindings.DataHeader(16, 0)};
+    private static final org.chromium.mojo.bindings.DataHeader DEFAULT_STRUCT_INFO = VERSION_ARRAY[0];
+    public boolean myBool;
+
+    private BasicStruct(int version) {
+        super(STRUCT_SIZE, version);
+    }
+
+    public BasicStruct() {
+        this(0);
+    }
+
+    public static BasicStruct deserialize(org.chromium.mojo.bindings.Message message) {
+        return decode(new org.chromium.mojo.bindings.Decoder(message));
+    }
+
+    /**
+     * Similar to the method above, but deserializes from a |ByteBuffer| instance.
+     *
+     * @throws org.chromium.mojo.bindings.DeserializationException on deserialization failure.
+     */
+    public static BasicStruct deserialize(java.nio.ByteBuffer data) {
+        return deserialize(new org.chromium.mojo.bindings.Message(
+                data, new java.util.ArrayList<org.chromium.mojo.system.Handle>()));
+    }
+
+    @SuppressWarnings("unchecked")
+    public static BasicStruct decode(org.chromium.mojo.bindings.@Nullable Decoder decoder0) {
+        if (decoder0 == null) {
+            return null;
+        }
+        decoder0.increaseStackDepth();
+        BasicStruct result;
+        try {
+            org.chromium.mojo.bindings.DataHeader mainDataHeader = decoder0.readAndValidateDataHeader(VERSION_ARRAY);
+            final int elementsOrVersion = mainDataHeader.elementsOrVersion;
+            result = new BasicStruct(elementsOrVersion);
+                {
+                    
+                result.myBool = decoder0.readBoolean(8, 0);
+                }
+
+        } finally {
+            decoder0.decreaseStackDepth();
+        }
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected final void encode(org.chromium.mojo.bindings.Encoder encoder) {
+        org.chromium.mojo.bindings.Encoder encoder0 = encoder.getEncoderAtDataOffset(DEFAULT_STRUCT_INFO);
+        
+        encoder0.encode(this.myBool, 8, 0);
+    }
+}
\ No newline at end of file
diff --git a/mojo/golden/generated/java/org/chromium/golden/IFace.java.golden b/mojo/golden/generated/java/org/chromium/golden/IFace.java.golden
new file mode 100644
index 0000000..60838345
--- /dev/null
+++ b/mojo/golden/generated/java/org/chromium/golden/IFace.java.golden
@@ -0,0 +1,40 @@
+// IFace.java is auto generated by mojom_bindings_generator.py, do not edit
+
+
+// Copyright 2014 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 autogenerated by:
+//     mojo/public/tools/bindings/mojom_bindings_generator.py
+// For:
+//     interface.test-mojom
+//
+
+package org.chromium.golden;
+
+import androidx.annotation.IntDef;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+
+public interface IFace extends org.chromium.mojo.bindings.Interface {
+
+
+
+    public interface Proxy extends IFace, org.chromium.mojo.bindings.Interface.Proxy {
+    }
+
+    Manager<IFace, IFace.Proxy> MANAGER = IFace_Internal.MANAGER;
+
+    void method(
+boolean param, 
+Method_Response callback);
+
+    interface Method_Response {
+      public void call(
+          String result);
+    }
+
+
+}
diff --git a/mojo/golden/generated/java/org/chromium/golden/IFaceWithTypemap.java.golden b/mojo/golden/generated/java/org/chromium/golden/IFaceWithTypemap.java.golden
new file mode 100644
index 0000000..b11654e
--- /dev/null
+++ b/mojo/golden/generated/java/org/chromium/golden/IFaceWithTypemap.java.golden
@@ -0,0 +1,40 @@
+// IFaceWithTypemap.java is auto generated by mojom_bindings_generator.py, do not edit
+
+
+// Copyright 2014 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 autogenerated by:
+//     mojo/public/tools/bindings/mojom_bindings_generator.py
+// For:
+//     typemap.test-mojom
+//
+
+package org.chromium.golden;
+
+import androidx.annotation.IntDef;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+
+public interface IFaceWithTypemap extends org.chromium.mojo.bindings.Interface {
+
+
+
+    public interface Proxy extends IFaceWithTypemap, org.chromium.mojo.bindings.Interface.Proxy {
+    }
+
+    Manager<IFaceWithTypemap, IFaceWithTypemap.Proxy> MANAGER = IFaceWithTypemap_Internal.MANAGER;
+
+    void echo(
+Typemapped param, 
+Echo_Response callback);
+
+    interface Echo_Response {
+      public void call(
+          Typemapped out);
+    }
+
+
+}
diff --git a/mojo/golden/generated/java/org/chromium/golden/IFaceWithTypemap_Internal.java.golden b/mojo/golden/generated/java/org/chromium/golden/IFaceWithTypemap_Internal.java.golden
new file mode 100644
index 0000000..da9c345
--- /dev/null
+++ b/mojo/golden/generated/java/org/chromium/golden/IFaceWithTypemap_Internal.java.golden
@@ -0,0 +1,361 @@
+// IFaceWithTypemap_Internal.java is auto generated by mojom_bindings_generator.py, do not edit
+
+
+// Copyright 2014 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 autogenerated by:
+//     mojo/public/tools/bindings/mojom_bindings_generator.py
+// For:
+//     typemap.test-mojom
+//
+
+package org.chromium.golden;
+
+import androidx.annotation.IntDef;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+
+class IFaceWithTypemap_Internal {
+
+    public static final org.chromium.mojo.bindings.Interface.Manager<IFaceWithTypemap, IFaceWithTypemap.Proxy> MANAGER =
+            new org.chromium.mojo.bindings.Interface.Manager<IFaceWithTypemap, IFaceWithTypemap.Proxy>() {
+
+        @Override
+        public String getName() {
+            return "golden.IFaceWithTypemap";
+        }
+
+        @Override
+        public int getVersion() {
+          return 0;
+        }
+
+        @Override
+        public Proxy buildProxy(org.chromium.mojo.system.Core core,
+                                org.chromium.mojo.bindings.MessageReceiverWithResponder messageReceiver) {
+            return new Proxy(core, messageReceiver);
+        }
+
+        @Override
+        public Stub buildStub(org.chromium.mojo.system.Core core, IFaceWithTypemap impl) {
+            return new Stub(core, impl);
+        }
+
+        @Override
+        public IFaceWithTypemap[] buildArray(int size) {
+          return new IFaceWithTypemap[size];
+        }
+    };
+
+
+    private static final int ECHO_ORDINAL = 0;
+
+
+    static final class Proxy extends org.chromium.mojo.bindings.Interface.AbstractProxy implements IFaceWithTypemap.Proxy {
+
+        Proxy(org.chromium.mojo.system.Core core,
+              org.chromium.mojo.bindings.MessageReceiverWithResponder messageReceiver) {
+            super(core, messageReceiver);
+        }
+
+
+        @Override
+        public void echo(
+Typemapped param, 
+Echo_Response callback) {
+
+            IFaceWithTypemapEchoParams _message = new IFaceWithTypemapEchoParams();
+
+            _message.param = param;
+
+
+            getProxyHandler().getMessageReceiver().acceptWithResponder(
+                    _message.serializeWithHeader(
+                            getProxyHandler().getCore(),
+                            new org.chromium.mojo.bindings.MessageHeader(
+                                    ECHO_ORDINAL,
+                                    org.chromium.mojo.bindings.MessageHeader.MESSAGE_EXPECTS_RESPONSE_FLAG,
+                                    0)),
+                    new IFaceWithTypemapEchoResponseParamsForwardToCallback(callback));
+
+        }
+
+
+    }
+
+    static final class Stub extends org.chromium.mojo.bindings.Interface.Stub<IFaceWithTypemap> {
+
+        Stub(org.chromium.mojo.system.Core core, IFaceWithTypemap impl) {
+            super(core, impl);
+        }
+
+        @Override
+        public boolean accept(org.chromium.mojo.bindings.Message message) {
+            try {
+                org.chromium.mojo.bindings.ServiceMessage messageWithHeader =
+                        message.asServiceMessage();
+                org.chromium.mojo.bindings.MessageHeader header = messageWithHeader.getHeader();
+                int flags = org.chromium.mojo.bindings.MessageHeader.NO_FLAG;
+                if (header.hasFlag(org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG)) {
+                    flags = flags | org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG;
+                }
+                if (!header.validateHeader(flags)) {
+                    return false;
+                }
+                switch(header.getType()) {
+
+                    case org.chromium.mojo.bindings.interfacecontrol.InterfaceControlMessagesConstants.RUN_OR_CLOSE_PIPE_MESSAGE_ID:
+                        return org.chromium.mojo.bindings.InterfaceControlMessagesHelper.handleRunOrClosePipe(
+                                IFaceWithTypemap_Internal.MANAGER, messageWithHeader);
+
+
+
+
+                    default:
+                        return false;
+                }
+            } catch (org.chromium.mojo.bindings.DeserializationException e) {
+                System.err.println(e.toString());
+                return false;
+            }
+        }
+
+        @Override
+        public boolean acceptWithResponder(org.chromium.mojo.bindings.Message message, org.chromium.mojo.bindings.MessageReceiver receiver) {
+            try {
+                org.chromium.mojo.bindings.ServiceMessage messageWithHeader =
+                        message.asServiceMessage();
+                org.chromium.mojo.bindings.MessageHeader header = messageWithHeader.getHeader();
+                int flags = org.chromium.mojo.bindings.MessageHeader.MESSAGE_EXPECTS_RESPONSE_FLAG;
+                if (header.hasFlag(org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG)) {
+                    flags = flags | org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG;
+                }
+                if (!header.validateHeader(flags)) {
+                    return false;
+                }
+                switch(header.getType()) {
+
+                    case org.chromium.mojo.bindings.interfacecontrol.InterfaceControlMessagesConstants.RUN_MESSAGE_ID:
+                        return org.chromium.mojo.bindings.InterfaceControlMessagesHelper.handleRun(
+                                getCore(), IFaceWithTypemap_Internal.MANAGER, messageWithHeader, receiver);
+
+
+
+
+
+
+
+                    case ECHO_ORDINAL: {
+
+                        IFaceWithTypemapEchoParams data =
+                                IFaceWithTypemapEchoParams.deserialize(messageWithHeader.getPayload());
+
+                        getImpl().echo(data.param, new IFaceWithTypemapEchoResponseParamsProxyToResponder(getCore(), receiver, header.getRequestId()));
+                        return true;
+                    }
+
+
+                    default:
+                        return false;
+                }
+            } catch (org.chromium.mojo.bindings.DeserializationException e) {
+                System.err.println(e.toString());
+                return false;
+            }
+        }
+    }
+
+
+    
+    static final class IFaceWithTypemapEchoParams extends org.chromium.mojo.bindings.Struct {
+
+        private static final int STRUCT_SIZE = 16;
+        private static final org.chromium.mojo.bindings.DataHeader[] VERSION_ARRAY = new org.chromium.mojo.bindings.DataHeader[] {new org.chromium.mojo.bindings.DataHeader(16, 0)};
+        private static final org.chromium.mojo.bindings.DataHeader DEFAULT_STRUCT_INFO = VERSION_ARRAY[0];
+        public Typemapped param;
+
+        private IFaceWithTypemapEchoParams(int version) {
+            super(STRUCT_SIZE, version);
+        }
+
+        public IFaceWithTypemapEchoParams() {
+            this(0);
+        }
+
+        public static IFaceWithTypemapEchoParams deserialize(org.chromium.mojo.bindings.Message message) {
+            return decode(new org.chromium.mojo.bindings.Decoder(message));
+        }
+
+        /**
+         * Similar to the method above, but deserializes from a |ByteBuffer| instance.
+         *
+         * @throws org.chromium.mojo.bindings.DeserializationException on deserialization failure.
+         */
+        public static IFaceWithTypemapEchoParams deserialize(java.nio.ByteBuffer data) {
+            return deserialize(new org.chromium.mojo.bindings.Message(
+                    data, new java.util.ArrayList<org.chromium.mojo.system.Handle>()));
+        }
+
+        @SuppressWarnings("unchecked")
+        public static IFaceWithTypemapEchoParams decode(org.chromium.mojo.bindings.@Nullable Decoder decoder0) {
+            if (decoder0 == null) {
+                return null;
+            }
+            decoder0.increaseStackDepth();
+            IFaceWithTypemapEchoParams result;
+            try {
+                org.chromium.mojo.bindings.DataHeader mainDataHeader = decoder0.readAndValidateDataHeader(VERSION_ARRAY);
+                final int elementsOrVersion = mainDataHeader.elementsOrVersion;
+                result = new IFaceWithTypemapEchoParams(elementsOrVersion);
+                    {
+                        
+                    org.chromium.mojo.bindings.Decoder decoder1 = decoder0.readPointer(8, false);
+                    result.param = Typemapped.decode(decoder1);
+                    }
+
+            } finally {
+                decoder0.decreaseStackDepth();
+            }
+            return result;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        protected final void encode(org.chromium.mojo.bindings.Encoder encoder) {
+            org.chromium.mojo.bindings.Encoder encoder0 = encoder.getEncoderAtDataOffset(DEFAULT_STRUCT_INFO);
+            
+            encoder0.encode(this.param, 8, false);
+        }
+    }
+
+
+
+    
+    static final class IFaceWithTypemapEchoResponseParams extends org.chromium.mojo.bindings.Struct {
+
+        private static final int STRUCT_SIZE = 16;
+        private static final org.chromium.mojo.bindings.DataHeader[] VERSION_ARRAY = new org.chromium.mojo.bindings.DataHeader[] {new org.chromium.mojo.bindings.DataHeader(16, 0)};
+        private static final org.chromium.mojo.bindings.DataHeader DEFAULT_STRUCT_INFO = VERSION_ARRAY[0];
+        public Typemapped out;
+
+        private IFaceWithTypemapEchoResponseParams(int version) {
+            super(STRUCT_SIZE, version);
+        }
+
+        public IFaceWithTypemapEchoResponseParams() {
+            this(0);
+        }
+
+        public static IFaceWithTypemapEchoResponseParams deserialize(org.chromium.mojo.bindings.Message message) {
+            return decode(new org.chromium.mojo.bindings.Decoder(message));
+        }
+
+        /**
+         * Similar to the method above, but deserializes from a |ByteBuffer| instance.
+         *
+         * @throws org.chromium.mojo.bindings.DeserializationException on deserialization failure.
+         */
+        public static IFaceWithTypemapEchoResponseParams deserialize(java.nio.ByteBuffer data) {
+            return deserialize(new org.chromium.mojo.bindings.Message(
+                    data, new java.util.ArrayList<org.chromium.mojo.system.Handle>()));
+        }
+
+        @SuppressWarnings("unchecked")
+        public static IFaceWithTypemapEchoResponseParams decode(org.chromium.mojo.bindings.@Nullable Decoder decoder0) {
+            if (decoder0 == null) {
+                return null;
+            }
+            decoder0.increaseStackDepth();
+            IFaceWithTypemapEchoResponseParams result;
+            try {
+                org.chromium.mojo.bindings.DataHeader mainDataHeader = decoder0.readAndValidateDataHeader(VERSION_ARRAY);
+                final int elementsOrVersion = mainDataHeader.elementsOrVersion;
+                result = new IFaceWithTypemapEchoResponseParams(elementsOrVersion);
+                    {
+                        
+                    org.chromium.mojo.bindings.Decoder decoder1 = decoder0.readPointer(8, false);
+                    result.out = Typemapped.decode(decoder1);
+                    }
+
+            } finally {
+                decoder0.decreaseStackDepth();
+            }
+            return result;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        protected final void encode(org.chromium.mojo.bindings.Encoder encoder) {
+            org.chromium.mojo.bindings.Encoder encoder0 = encoder.getEncoderAtDataOffset(DEFAULT_STRUCT_INFO);
+            
+            encoder0.encode(this.out, 8, false);
+        }
+    }
+
+    static class IFaceWithTypemapEchoResponseParamsForwardToCallback extends org.chromium.mojo.bindings.SideEffectFreeCloseable
+            implements org.chromium.mojo.bindings.MessageReceiver {
+        private final IFaceWithTypemap.Echo_Response mCallback;
+
+        IFaceWithTypemapEchoResponseParamsForwardToCallback(IFaceWithTypemap.Echo_Response callback) {
+            this.mCallback = callback;
+        }
+
+        @Override
+        public boolean accept(org.chromium.mojo.bindings.Message message) {
+            try {
+                org.chromium.mojo.bindings.ServiceMessage messageWithHeader =
+                        message.asServiceMessage();
+                org.chromium.mojo.bindings.MessageHeader header = messageWithHeader.getHeader();
+                if (!header.validateHeader(ECHO_ORDINAL,
+                                           org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_RESPONSE_FLAG)) {
+                    return false;
+                }
+
+                IFaceWithTypemapEchoResponseParams response = IFaceWithTypemapEchoResponseParams.deserialize(messageWithHeader.getPayload());
+
+                mCallback.call(response.out);
+                return true;
+            } catch (org.chromium.mojo.bindings.DeserializationException e) {
+                return false;
+            }
+        }
+    }
+
+    static class IFaceWithTypemapEchoResponseParamsProxyToResponder implements IFaceWithTypemap.Echo_Response {
+
+        private final org.chromium.mojo.system.Core mCore;
+        private final org.chromium.mojo.bindings.MessageReceiver mMessageReceiver;
+        private final long mRequestId;
+
+        IFaceWithTypemapEchoResponseParamsProxyToResponder(
+                org.chromium.mojo.system.Core core,
+                org.chromium.mojo.bindings.MessageReceiver messageReceiver,
+                long requestId) {
+            mCore = core;
+            mMessageReceiver = messageReceiver;
+            mRequestId = requestId;
+        }
+
+        @Override
+        public void call(Typemapped out) {
+            IFaceWithTypemapEchoResponseParams _response = new IFaceWithTypemapEchoResponseParams();
+
+            _response.out = out;
+
+            org.chromium.mojo.bindings.ServiceMessage _message =
+                    _response.serializeWithHeader(
+                            mCore,
+                            new org.chromium.mojo.bindings.MessageHeader(
+                                    ECHO_ORDINAL,
+                                    org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_RESPONSE_FLAG,
+                                    mRequestId));
+            mMessageReceiver.accept(_message);
+        }
+    }
+
+
+
+}
diff --git a/mojo/golden/generated/java/org/chromium/golden/IFace_Internal.java.golden b/mojo/golden/generated/java/org/chromium/golden/IFace_Internal.java.golden
new file mode 100644
index 0000000..e744596
--- /dev/null
+++ b/mojo/golden/generated/java/org/chromium/golden/IFace_Internal.java.golden
@@ -0,0 +1,359 @@
+// IFace_Internal.java is auto generated by mojom_bindings_generator.py, do not edit
+
+
+// Copyright 2014 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 autogenerated by:
+//     mojo/public/tools/bindings/mojom_bindings_generator.py
+// For:
+//     interface.test-mojom
+//
+
+package org.chromium.golden;
+
+import androidx.annotation.IntDef;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+
+class IFace_Internal {
+
+    public static final org.chromium.mojo.bindings.Interface.Manager<IFace, IFace.Proxy> MANAGER =
+            new org.chromium.mojo.bindings.Interface.Manager<IFace, IFace.Proxy>() {
+
+        @Override
+        public String getName() {
+            return "golden.IFace";
+        }
+
+        @Override
+        public int getVersion() {
+          return 0;
+        }
+
+        @Override
+        public Proxy buildProxy(org.chromium.mojo.system.Core core,
+                                org.chromium.mojo.bindings.MessageReceiverWithResponder messageReceiver) {
+            return new Proxy(core, messageReceiver);
+        }
+
+        @Override
+        public Stub buildStub(org.chromium.mojo.system.Core core, IFace impl) {
+            return new Stub(core, impl);
+        }
+
+        @Override
+        public IFace[] buildArray(int size) {
+          return new IFace[size];
+        }
+    };
+
+
+    private static final int METHOD_ORDINAL = 0;
+
+
+    static final class Proxy extends org.chromium.mojo.bindings.Interface.AbstractProxy implements IFace.Proxy {
+
+        Proxy(org.chromium.mojo.system.Core core,
+              org.chromium.mojo.bindings.MessageReceiverWithResponder messageReceiver) {
+            super(core, messageReceiver);
+        }
+
+
+        @Override
+        public void method(
+boolean param, 
+Method_Response callback) {
+
+            IFaceMethodParams _message = new IFaceMethodParams();
+
+            _message.param = param;
+
+
+            getProxyHandler().getMessageReceiver().acceptWithResponder(
+                    _message.serializeWithHeader(
+                            getProxyHandler().getCore(),
+                            new org.chromium.mojo.bindings.MessageHeader(
+                                    METHOD_ORDINAL,
+                                    org.chromium.mojo.bindings.MessageHeader.MESSAGE_EXPECTS_RESPONSE_FLAG,
+                                    0)),
+                    new IFaceMethodResponseParamsForwardToCallback(callback));
+
+        }
+
+
+    }
+
+    static final class Stub extends org.chromium.mojo.bindings.Interface.Stub<IFace> {
+
+        Stub(org.chromium.mojo.system.Core core, IFace impl) {
+            super(core, impl);
+        }
+
+        @Override
+        public boolean accept(org.chromium.mojo.bindings.Message message) {
+            try {
+                org.chromium.mojo.bindings.ServiceMessage messageWithHeader =
+                        message.asServiceMessage();
+                org.chromium.mojo.bindings.MessageHeader header = messageWithHeader.getHeader();
+                int flags = org.chromium.mojo.bindings.MessageHeader.NO_FLAG;
+                if (header.hasFlag(org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG)) {
+                    flags = flags | org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG;
+                }
+                if (!header.validateHeader(flags)) {
+                    return false;
+                }
+                switch(header.getType()) {
+
+                    case org.chromium.mojo.bindings.interfacecontrol.InterfaceControlMessagesConstants.RUN_OR_CLOSE_PIPE_MESSAGE_ID:
+                        return org.chromium.mojo.bindings.InterfaceControlMessagesHelper.handleRunOrClosePipe(
+                                IFace_Internal.MANAGER, messageWithHeader);
+
+
+
+
+                    default:
+                        return false;
+                }
+            } catch (org.chromium.mojo.bindings.DeserializationException e) {
+                System.err.println(e.toString());
+                return false;
+            }
+        }
+
+        @Override
+        public boolean acceptWithResponder(org.chromium.mojo.bindings.Message message, org.chromium.mojo.bindings.MessageReceiver receiver) {
+            try {
+                org.chromium.mojo.bindings.ServiceMessage messageWithHeader =
+                        message.asServiceMessage();
+                org.chromium.mojo.bindings.MessageHeader header = messageWithHeader.getHeader();
+                int flags = org.chromium.mojo.bindings.MessageHeader.MESSAGE_EXPECTS_RESPONSE_FLAG;
+                if (header.hasFlag(org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG)) {
+                    flags = flags | org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG;
+                }
+                if (!header.validateHeader(flags)) {
+                    return false;
+                }
+                switch(header.getType()) {
+
+                    case org.chromium.mojo.bindings.interfacecontrol.InterfaceControlMessagesConstants.RUN_MESSAGE_ID:
+                        return org.chromium.mojo.bindings.InterfaceControlMessagesHelper.handleRun(
+                                getCore(), IFace_Internal.MANAGER, messageWithHeader, receiver);
+
+
+
+
+
+
+
+                    case METHOD_ORDINAL: {
+
+                        IFaceMethodParams data =
+                                IFaceMethodParams.deserialize(messageWithHeader.getPayload());
+
+                        getImpl().method(data.param, new IFaceMethodResponseParamsProxyToResponder(getCore(), receiver, header.getRequestId()));
+                        return true;
+                    }
+
+
+                    default:
+                        return false;
+                }
+            } catch (org.chromium.mojo.bindings.DeserializationException e) {
+                System.err.println(e.toString());
+                return false;
+            }
+        }
+    }
+
+
+    
+    static final class IFaceMethodParams extends org.chromium.mojo.bindings.Struct {
+
+        private static final int STRUCT_SIZE = 16;
+        private static final org.chromium.mojo.bindings.DataHeader[] VERSION_ARRAY = new org.chromium.mojo.bindings.DataHeader[] {new org.chromium.mojo.bindings.DataHeader(16, 0)};
+        private static final org.chromium.mojo.bindings.DataHeader DEFAULT_STRUCT_INFO = VERSION_ARRAY[0];
+        public boolean param;
+
+        private IFaceMethodParams(int version) {
+            super(STRUCT_SIZE, version);
+        }
+
+        public IFaceMethodParams() {
+            this(0);
+        }
+
+        public static IFaceMethodParams deserialize(org.chromium.mojo.bindings.Message message) {
+            return decode(new org.chromium.mojo.bindings.Decoder(message));
+        }
+
+        /**
+         * Similar to the method above, but deserializes from a |ByteBuffer| instance.
+         *
+         * @throws org.chromium.mojo.bindings.DeserializationException on deserialization failure.
+         */
+        public static IFaceMethodParams deserialize(java.nio.ByteBuffer data) {
+            return deserialize(new org.chromium.mojo.bindings.Message(
+                    data, new java.util.ArrayList<org.chromium.mojo.system.Handle>()));
+        }
+
+        @SuppressWarnings("unchecked")
+        public static IFaceMethodParams decode(org.chromium.mojo.bindings.@Nullable Decoder decoder0) {
+            if (decoder0 == null) {
+                return null;
+            }
+            decoder0.increaseStackDepth();
+            IFaceMethodParams result;
+            try {
+                org.chromium.mojo.bindings.DataHeader mainDataHeader = decoder0.readAndValidateDataHeader(VERSION_ARRAY);
+                final int elementsOrVersion = mainDataHeader.elementsOrVersion;
+                result = new IFaceMethodParams(elementsOrVersion);
+                    {
+                        
+                    result.param = decoder0.readBoolean(8, 0);
+                    }
+
+            } finally {
+                decoder0.decreaseStackDepth();
+            }
+            return result;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        protected final void encode(org.chromium.mojo.bindings.Encoder encoder) {
+            org.chromium.mojo.bindings.Encoder encoder0 = encoder.getEncoderAtDataOffset(DEFAULT_STRUCT_INFO);
+            
+            encoder0.encode(this.param, 8, 0);
+        }
+    }
+
+
+
+    
+    static final class IFaceMethodResponseParams extends org.chromium.mojo.bindings.Struct {
+
+        private static final int STRUCT_SIZE = 16;
+        private static final org.chromium.mojo.bindings.DataHeader[] VERSION_ARRAY = new org.chromium.mojo.bindings.DataHeader[] {new org.chromium.mojo.bindings.DataHeader(16, 0)};
+        private static final org.chromium.mojo.bindings.DataHeader DEFAULT_STRUCT_INFO = VERSION_ARRAY[0];
+        public String result;
+
+        private IFaceMethodResponseParams(int version) {
+            super(STRUCT_SIZE, version);
+        }
+
+        public IFaceMethodResponseParams() {
+            this(0);
+        }
+
+        public static IFaceMethodResponseParams deserialize(org.chromium.mojo.bindings.Message message) {
+            return decode(new org.chromium.mojo.bindings.Decoder(message));
+        }
+
+        /**
+         * Similar to the method above, but deserializes from a |ByteBuffer| instance.
+         *
+         * @throws org.chromium.mojo.bindings.DeserializationException on deserialization failure.
+         */
+        public static IFaceMethodResponseParams deserialize(java.nio.ByteBuffer data) {
+            return deserialize(new org.chromium.mojo.bindings.Message(
+                    data, new java.util.ArrayList<org.chromium.mojo.system.Handle>()));
+        }
+
+        @SuppressWarnings("unchecked")
+        public static IFaceMethodResponseParams decode(org.chromium.mojo.bindings.@Nullable Decoder decoder0) {
+            if (decoder0 == null) {
+                return null;
+            }
+            decoder0.increaseStackDepth();
+            IFaceMethodResponseParams result;
+            try {
+                org.chromium.mojo.bindings.DataHeader mainDataHeader = decoder0.readAndValidateDataHeader(VERSION_ARRAY);
+                final int elementsOrVersion = mainDataHeader.elementsOrVersion;
+                result = new IFaceMethodResponseParams(elementsOrVersion);
+                    {
+                        
+                    result.result = decoder0.readString(8, false);
+                    }
+
+            } finally {
+                decoder0.decreaseStackDepth();
+            }
+            return result;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        protected final void encode(org.chromium.mojo.bindings.Encoder encoder) {
+            org.chromium.mojo.bindings.Encoder encoder0 = encoder.getEncoderAtDataOffset(DEFAULT_STRUCT_INFO);
+            
+            encoder0.encode(this.result, 8, false);
+        }
+    }
+
+    static class IFaceMethodResponseParamsForwardToCallback extends org.chromium.mojo.bindings.SideEffectFreeCloseable
+            implements org.chromium.mojo.bindings.MessageReceiver {
+        private final IFace.Method_Response mCallback;
+
+        IFaceMethodResponseParamsForwardToCallback(IFace.Method_Response callback) {
+            this.mCallback = callback;
+        }
+
+        @Override
+        public boolean accept(org.chromium.mojo.bindings.Message message) {
+            try {
+                org.chromium.mojo.bindings.ServiceMessage messageWithHeader =
+                        message.asServiceMessage();
+                org.chromium.mojo.bindings.MessageHeader header = messageWithHeader.getHeader();
+                if (!header.validateHeader(METHOD_ORDINAL,
+                                           org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_RESPONSE_FLAG)) {
+                    return false;
+                }
+
+                IFaceMethodResponseParams response = IFaceMethodResponseParams.deserialize(messageWithHeader.getPayload());
+
+                mCallback.call(response.result);
+                return true;
+            } catch (org.chromium.mojo.bindings.DeserializationException e) {
+                return false;
+            }
+        }
+    }
+
+    static class IFaceMethodResponseParamsProxyToResponder implements IFace.Method_Response {
+
+        private final org.chromium.mojo.system.Core mCore;
+        private final org.chromium.mojo.bindings.MessageReceiver mMessageReceiver;
+        private final long mRequestId;
+
+        IFaceMethodResponseParamsProxyToResponder(
+                org.chromium.mojo.system.Core core,
+                org.chromium.mojo.bindings.MessageReceiver messageReceiver,
+                long requestId) {
+            mCore = core;
+            mMessageReceiver = messageReceiver;
+            mRequestId = requestId;
+        }
+
+        @Override
+        public void call(String result) {
+            IFaceMethodResponseParams _response = new IFaceMethodResponseParams();
+
+            _response.result = result;
+
+            org.chromium.mojo.bindings.ServiceMessage _message =
+                    _response.serializeWithHeader(
+                            mCore,
+                            new org.chromium.mojo.bindings.MessageHeader(
+                                    METHOD_ORDINAL,
+                                    org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_RESPONSE_FLAG,
+                                    mRequestId));
+            mMessageReceiver.accept(_message);
+        }
+    }
+
+
+
+}
diff --git a/mojo/golden/generated/java/org/chromium/golden/OptionalPrimitives.java.golden b/mojo/golden/generated/java/org/chromium/golden/OptionalPrimitives.java.golden
new file mode 100644
index 0000000..4a052a5e
--- /dev/null
+++ b/mojo/golden/generated/java/org/chromium/golden/OptionalPrimitives.java.golden
@@ -0,0 +1,146 @@
+// OptionalPrimitives.java is auto generated by mojom_bindings_generator.py, do not edit
+
+
+// Copyright 2014 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 autogenerated by:
+//     mojo/public/tools/bindings/mojom_bindings_generator.py
+// For:
+//     optional_primitives.test-mojom
+//
+
+package org.chromium.golden;
+
+import androidx.annotation.IntDef;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+
+@NullMarked
+@SuppressWarnings("NullAway")
+public final class OptionalPrimitives extends org.chromium.mojo.bindings.Struct {
+
+    private static final int STRUCT_SIZE = 40;
+    private static final org.chromium.mojo.bindings.DataHeader[] VERSION_ARRAY = new org.chromium.mojo.bindings.DataHeader[] {new org.chromium.mojo.bindings.DataHeader(40, 0)};
+    private static final org.chromium.mojo.bindings.DataHeader DEFAULT_STRUCT_INFO = VERSION_ARRAY[0];
+    public @Nullable Integer int;
+    public @Nullable Integer[] uints;
+    public @Nullable Boolean[] boolarray;
+    public java.util.Map<Boolean, @Nullable Boolean> bitmap;
+
+    private OptionalPrimitives(int version) {
+        super(STRUCT_SIZE, version);
+    }
+
+    public OptionalPrimitives() {
+        this(0);
+    }
+
+    public static OptionalPrimitives deserialize(org.chromium.mojo.bindings.Message message) {
+        return decode(new org.chromium.mojo.bindings.Decoder(message));
+    }
+
+    /**
+     * Similar to the method above, but deserializes from a |ByteBuffer| instance.
+     *
+     * @throws org.chromium.mojo.bindings.DeserializationException on deserialization failure.
+     */
+    public static OptionalPrimitives deserialize(java.nio.ByteBuffer data) {
+        return deserialize(new org.chromium.mojo.bindings.Message(
+                data, new java.util.ArrayList<org.chromium.mojo.system.Handle>()));
+    }
+
+    @SuppressWarnings("unchecked")
+    public static OptionalPrimitives decode(org.chromium.mojo.bindings.@Nullable Decoder decoder0) {
+        if (decoder0 == null) {
+            return null;
+        }
+        decoder0.increaseStackDepth();
+        OptionalPrimitives result;
+        try {
+            org.chromium.mojo.bindings.DataHeader mainDataHeader = decoder0.readAndValidateDataHeader(VERSION_ARRAY);
+            final int elementsOrVersion = mainDataHeader.elementsOrVersion;
+            result = new OptionalPrimitives(elementsOrVersion);
+                {
+                    
+                if (decoder0.readBoolean(8, 0)) {
+                  result.int = new Integer(decoder0.readInt(12));
+                } else {
+                  result.int = null;
+                }
+                }
+                {
+                    
+                result.uints = decoder0.readIntNullables(16, org.chromium.mojo.bindings.BindingsHelper.ELEMENT_NULLABLE, org.chromium.mojo.bindings.BindingsHelper.UNSPECIFIED_ARRAY_LENGTH);
+                }
+                {
+                    
+                result.boolarray = decoder0.readBooleanNullables(24, org.chromium.mojo.bindings.BindingsHelper.ELEMENT_NULLABLE, org.chromium.mojo.bindings.BindingsHelper.UNSPECIFIED_ARRAY_LENGTH);
+                }
+                {
+                    
+                org.chromium.mojo.bindings.Decoder decoder1 = decoder0.readPointer(32, false);
+                {
+                    decoder1.readDataHeaderForMap();
+                    boolean[] keys0;
+                    Boolean[] values0;
+                    {
+                        
+                        keys0 = decoder1.readBooleans(org.chromium.mojo.bindings.DataHeader.HEADER_SIZE, org.chromium.mojo.bindings.BindingsHelper.NOTHING_NULLABLE, org.chromium.mojo.bindings.BindingsHelper.UNSPECIFIED_ARRAY_LENGTH);
+                    }
+                    {
+                        
+                        values0 = decoder1.readBooleanNullables(org.chromium.mojo.bindings.DataHeader.HEADER_SIZE + org.chromium.mojo.bindings.BindingsHelper.POINTER_SIZE, org.chromium.mojo.bindings.BindingsHelper.ELEMENT_NULLABLE, keys0.length);
+                    }
+                    result.bitmap = new java.util.HashMap<Boolean, Boolean>();
+                    for (int index0 = 0; index0 < keys0.length; ++index0) {
+                        result.bitmap.put(keys0[index0],  values0[index0]);
+                    }
+                }
+                }
+
+        } finally {
+            decoder0.decreaseStackDepth();
+        }
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected final void encode(org.chromium.mojo.bindings.Encoder encoder) {
+        org.chromium.mojo.bindings.Encoder encoder0 = encoder.getEncoderAtDataOffset(DEFAULT_STRUCT_INFO);
+        final boolean int$flag = this.int != null;
+        final int int$value = int$flag
+            ? this.int
+            : 0;
+        
+        encoder0.encode(int$flag, 8, 0);
+        
+        encoder0.encode(int$value, 12);
+        
+        encoder0.encode(this.uints, 16, org.chromium.mojo.bindings.BindingsHelper.ELEMENT_NULLABLE, org.chromium.mojo.bindings.BindingsHelper.UNSPECIFIED_ARRAY_LENGTH);
+        
+        encoder0.encode(this.boolarray, 24, org.chromium.mojo.bindings.BindingsHelper.ELEMENT_NULLABLE, org.chromium.mojo.bindings.BindingsHelper.UNSPECIFIED_ARRAY_LENGTH);
+        
+        if (this.bitmap == null) {
+            encoder0.encodeNullPointer(32, false);
+        } else {
+            org.chromium.mojo.bindings.Encoder encoder1 = encoder0.encoderForMap(32);
+            int size0 = this.bitmap.size();
+            boolean[] keys0 = new boolean[size0];
+            Boolean[] values0 = new Boolean[size0];
+            int index0 = 0;
+            for (java.util.Map.Entry<Boolean, Boolean> entry0 : this.bitmap.entrySet()) {
+                keys0[index0] = entry0.getKey();
+                values0[index0] = entry0.getValue();
+                ++index0;
+            }
+            
+            encoder1.encode(keys0, org.chromium.mojo.bindings.DataHeader.HEADER_SIZE, org.chromium.mojo.bindings.BindingsHelper.NOTHING_NULLABLE, org.chromium.mojo.bindings.BindingsHelper.UNSPECIFIED_ARRAY_LENGTH);
+            
+            encoder1.encode(values0, org.chromium.mojo.bindings.DataHeader.HEADER_SIZE + org.chromium.mojo.bindings.BindingsHelper.POINTER_SIZE, org.chromium.mojo.bindings.BindingsHelper.ELEMENT_NULLABLE, org.chromium.mojo.bindings.BindingsHelper.UNSPECIFIED_ARRAY_LENGTH);
+        }
+    }
+}
\ No newline at end of file
diff --git a/mojo/golden/generated/java/org/chromium/golden/ResultInterface.java.golden b/mojo/golden/generated/java/org/chromium/golden/ResultInterface.java.golden
new file mode 100644
index 0000000..ebbc7043
--- /dev/null
+++ b/mojo/golden/generated/java/org/chromium/golden/ResultInterface.java.golden
@@ -0,0 +1,50 @@
+// ResultInterface.java is auto generated by mojom_bindings_generator.py, do not edit
+
+
+// Copyright 2014 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 autogenerated by:
+//     mojo/public/tools/bindings/mojom_bindings_generator.py
+// For:
+//     results.test-mojom
+//
+
+package org.chromium.golden;
+
+import androidx.annotation.IntDef;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+
+public interface ResultInterface extends org.chromium.mojo.bindings.Interface {
+
+
+
+    public interface Proxy extends ResultInterface, org.chromium.mojo.bindings.Interface.Proxy {
+    }
+
+    Manager<ResultInterface, ResultInterface.Proxy> MANAGER = ResultInterface_Internal.MANAGER;
+
+    void method(
+boolean a, 
+Method_Response callback);
+
+    interface Method_Response {
+      public void call(
+          ResultInterfaceMethodResponseParamResult result);
+    }
+
+
+    void syncMethod(
+boolean a, 
+SyncMethod_Response callback);
+
+    interface SyncMethod_Response {
+      public void call(
+          ResultInterfaceSyncMethodResponseParamResult result);
+    }
+
+
+}
diff --git a/mojo/golden/generated/java/org/chromium/golden/ResultInterfaceMethodResponseParamResult.java.golden b/mojo/golden/generated/java/org/chromium/golden/ResultInterfaceMethodResponseParamResult.java.golden
new file mode 100644
index 0000000..f29b2eb91
--- /dev/null
+++ b/mojo/golden/generated/java/org/chromium/golden/ResultInterfaceMethodResponseParamResult.java.golden
@@ -0,0 +1,104 @@
+// ResultInterfaceMethodResponseParamResult.java is auto generated by mojom_bindings_generator.py, do not edit
+
+
+// Copyright 2014 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 autogenerated by:
+//     mojo/public/tools/bindings/mojom_bindings_generator.py
+// For:
+//     results.test-mojom
+//
+
+package org.chromium.golden;
+
+import androidx.annotation.IntDef;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+
+@NullMarked
+@SuppressWarnings("NullAway")
+public final class ResultInterfaceMethodResponseParamResult extends org.chromium.mojo.bindings.Union {
+
+    public static final class Tag {
+        public static final int Success = 0;
+        public static final int Failure = 1;
+    };
+    private boolean mSuccess;
+    private ResultTestError mFailure;
+
+    public void setSuccess(boolean success) {
+        this.mTag = Tag.Success;
+        this.mSuccess = success;
+    }
+
+    public boolean getSuccess() {
+        assert this.mTag == Tag.Success;
+        return this.mSuccess;
+    }
+
+    public void setFailure(ResultTestError failure) {
+        this.mTag = Tag.Failure;
+        this.mFailure = failure;
+    }
+
+    public ResultTestError getFailure() {
+        assert this.mTag == Tag.Failure;
+        return this.mFailure;
+    }
+
+
+    @Override
+    protected final void encode(org.chromium.mojo.bindings.Encoder encoder0, int offset) {
+        encoder0.encode(org.chromium.mojo.bindings.BindingsHelper.UNION_SIZE, offset);
+        encoder0.encode(this.mTag, offset + 4);
+        switch (mTag) {
+            case Tag.Success: {
+                
+                encoder0.encode(this.mSuccess, offset + 8, 0);
+                break;
+            }
+            case Tag.Failure: {
+                
+                encoder0.encode(this.mFailure, offset + 8, false);
+                break;
+            }
+            default: {
+                break;
+            }
+        }
+    }
+
+    public static ResultInterfaceMethodResponseParamResult deserialize(org.chromium.mojo.bindings.Message message) {
+        return decode(new org.chromium.mojo.bindings.Decoder(message).decoderForSerializedUnion(), 0);
+    }
+
+    public static final ResultInterfaceMethodResponseParamResult decode(org.chromium.mojo.bindings.Decoder decoder0, int offset) {
+        org.chromium.mojo.bindings.DataHeader dataHeader = decoder0.readDataHeaderForUnion(offset);
+        if (dataHeader.size == 0) {
+            return null;
+        }
+        ResultInterfaceMethodResponseParamResult result = new ResultInterfaceMethodResponseParamResult();
+        switch (dataHeader.elementsOrVersion) {
+            case Tag.Success: {
+                
+                result.mSuccess = decoder0.readBoolean(offset + org.chromium.mojo.bindings.DataHeader.HEADER_SIZE, 0);
+                result.mTag = Tag.Success;
+                break;
+            }
+            case Tag.Failure: {
+                
+                org.chromium.mojo.bindings.Decoder decoder1 = decoder0.readPointer(offset + org.chromium.mojo.bindings.DataHeader.HEADER_SIZE, false);
+                result.mFailure = ResultTestError.decode(decoder1);
+                result.mTag = Tag.Failure;
+                break;
+            }
+            default: {
+                break;
+            }
+        }
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/mojo/golden/generated/java/org/chromium/golden/ResultInterfaceSyncMethodResponseParamResult.java.golden b/mojo/golden/generated/java/org/chromium/golden/ResultInterfaceSyncMethodResponseParamResult.java.golden
new file mode 100644
index 0000000..23918e85
--- /dev/null
+++ b/mojo/golden/generated/java/org/chromium/golden/ResultInterfaceSyncMethodResponseParamResult.java.golden
@@ -0,0 +1,104 @@
+// ResultInterfaceSyncMethodResponseParamResult.java is auto generated by mojom_bindings_generator.py, do not edit
+
+
+// Copyright 2014 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 autogenerated by:
+//     mojo/public/tools/bindings/mojom_bindings_generator.py
+// For:
+//     results.test-mojom
+//
+
+package org.chromium.golden;
+
+import androidx.annotation.IntDef;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+
+@NullMarked
+@SuppressWarnings("NullAway")
+public final class ResultInterfaceSyncMethodResponseParamResult extends org.chromium.mojo.bindings.Union {
+
+    public static final class Tag {
+        public static final int Success = 0;
+        public static final int Failure = 1;
+    };
+    private boolean mSuccess;
+    private ResultTestError mFailure;
+
+    public void setSuccess(boolean success) {
+        this.mTag = Tag.Success;
+        this.mSuccess = success;
+    }
+
+    public boolean getSuccess() {
+        assert this.mTag == Tag.Success;
+        return this.mSuccess;
+    }
+
+    public void setFailure(ResultTestError failure) {
+        this.mTag = Tag.Failure;
+        this.mFailure = failure;
+    }
+
+    public ResultTestError getFailure() {
+        assert this.mTag == Tag.Failure;
+        return this.mFailure;
+    }
+
+
+    @Override
+    protected final void encode(org.chromium.mojo.bindings.Encoder encoder0, int offset) {
+        encoder0.encode(org.chromium.mojo.bindings.BindingsHelper.UNION_SIZE, offset);
+        encoder0.encode(this.mTag, offset + 4);
+        switch (mTag) {
+            case Tag.Success: {
+                
+                encoder0.encode(this.mSuccess, offset + 8, 0);
+                break;
+            }
+            case Tag.Failure: {
+                
+                encoder0.encode(this.mFailure, offset + 8, false);
+                break;
+            }
+            default: {
+                break;
+            }
+        }
+    }
+
+    public static ResultInterfaceSyncMethodResponseParamResult deserialize(org.chromium.mojo.bindings.Message message) {
+        return decode(new org.chromium.mojo.bindings.Decoder(message).decoderForSerializedUnion(), 0);
+    }
+
+    public static final ResultInterfaceSyncMethodResponseParamResult decode(org.chromium.mojo.bindings.Decoder decoder0, int offset) {
+        org.chromium.mojo.bindings.DataHeader dataHeader = decoder0.readDataHeaderForUnion(offset);
+        if (dataHeader.size == 0) {
+            return null;
+        }
+        ResultInterfaceSyncMethodResponseParamResult result = new ResultInterfaceSyncMethodResponseParamResult();
+        switch (dataHeader.elementsOrVersion) {
+            case Tag.Success: {
+                
+                result.mSuccess = decoder0.readBoolean(offset + org.chromium.mojo.bindings.DataHeader.HEADER_SIZE, 0);
+                result.mTag = Tag.Success;
+                break;
+            }
+            case Tag.Failure: {
+                
+                org.chromium.mojo.bindings.Decoder decoder1 = decoder0.readPointer(offset + org.chromium.mojo.bindings.DataHeader.HEADER_SIZE, false);
+                result.mFailure = ResultTestError.decode(decoder1);
+                result.mTag = Tag.Failure;
+                break;
+            }
+            default: {
+                break;
+            }
+        }
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/mojo/golden/generated/java/org/chromium/golden/ResultInterface_Internal.java.golden b/mojo/golden/generated/java/org/chromium/golden/ResultInterface_Internal.java.golden
new file mode 100644
index 0000000..d638c82
--- /dev/null
+++ b/mojo/golden/generated/java/org/chromium/golden/ResultInterface_Internal.java.golden
@@ -0,0 +1,587 @@
+// ResultInterface_Internal.java is auto generated by mojom_bindings_generator.py, do not edit
+
+
+// Copyright 2014 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 autogenerated by:
+//     mojo/public/tools/bindings/mojom_bindings_generator.py
+// For:
+//     results.test-mojom
+//
+
+package org.chromium.golden;
+
+import androidx.annotation.IntDef;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+
+class ResultInterface_Internal {
+
+    public static final org.chromium.mojo.bindings.Interface.Manager<ResultInterface, ResultInterface.Proxy> MANAGER =
+            new org.chromium.mojo.bindings.Interface.Manager<ResultInterface, ResultInterface.Proxy>() {
+
+        @Override
+        public String getName() {
+            return "golden.ResultInterface";
+        }
+
+        @Override
+        public int getVersion() {
+          return 0;
+        }
+
+        @Override
+        public Proxy buildProxy(org.chromium.mojo.system.Core core,
+                                org.chromium.mojo.bindings.MessageReceiverWithResponder messageReceiver) {
+            return new Proxy(core, messageReceiver);
+        }
+
+        @Override
+        public Stub buildStub(org.chromium.mojo.system.Core core, ResultInterface impl) {
+            return new Stub(core, impl);
+        }
+
+        @Override
+        public ResultInterface[] buildArray(int size) {
+          return new ResultInterface[size];
+        }
+    };
+
+
+    private static final int METHOD_ORDINAL = 0;
+
+    private static final int SYNC_METHOD_ORDINAL = 1;
+
+
+    static final class Proxy extends org.chromium.mojo.bindings.Interface.AbstractProxy implements ResultInterface.Proxy {
+
+        Proxy(org.chromium.mojo.system.Core core,
+              org.chromium.mojo.bindings.MessageReceiverWithResponder messageReceiver) {
+            super(core, messageReceiver);
+        }
+
+
+        @Override
+        public void method(
+boolean a, 
+Method_Response callback) {
+
+            ResultInterfaceMethodParams _message = new ResultInterfaceMethodParams();
+
+            _message.a = a;
+
+
+            getProxyHandler().getMessageReceiver().acceptWithResponder(
+                    _message.serializeWithHeader(
+                            getProxyHandler().getCore(),
+                            new org.chromium.mojo.bindings.MessageHeader(
+                                    METHOD_ORDINAL,
+                                    org.chromium.mojo.bindings.MessageHeader.MESSAGE_EXPECTS_RESPONSE_FLAG,
+                                    0)),
+                    new ResultInterfaceMethodResponseParamsForwardToCallback(callback));
+
+        }
+
+
+        @Override
+        public void syncMethod(
+boolean a, 
+SyncMethod_Response callback) {
+
+            ResultInterfaceSyncMethodParams _message = new ResultInterfaceSyncMethodParams();
+
+            _message.a = a;
+
+
+            getProxyHandler().getMessageReceiver().acceptWithResponder(
+                    _message.serializeWithHeader(
+                            getProxyHandler().getCore(),
+                            new org.chromium.mojo.bindings.MessageHeader(
+                                    SYNC_METHOD_ORDINAL,
+                                    org.chromium.mojo.bindings.MessageHeader.MESSAGE_EXPECTS_RESPONSE_FLAG,
+                                    0)),
+                    new ResultInterfaceSyncMethodResponseParamsForwardToCallback(callback));
+
+        }
+
+
+    }
+
+    static final class Stub extends org.chromium.mojo.bindings.Interface.Stub<ResultInterface> {
+
+        Stub(org.chromium.mojo.system.Core core, ResultInterface impl) {
+            super(core, impl);
+        }
+
+        @Override
+        public boolean accept(org.chromium.mojo.bindings.Message message) {
+            try {
+                org.chromium.mojo.bindings.ServiceMessage messageWithHeader =
+                        message.asServiceMessage();
+                org.chromium.mojo.bindings.MessageHeader header = messageWithHeader.getHeader();
+                int flags = org.chromium.mojo.bindings.MessageHeader.NO_FLAG;
+                if (header.hasFlag(org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG)) {
+                    flags = flags | org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG;
+                }
+                if (!header.validateHeader(flags)) {
+                    return false;
+                }
+                switch(header.getType()) {
+
+                    case org.chromium.mojo.bindings.interfacecontrol.InterfaceControlMessagesConstants.RUN_OR_CLOSE_PIPE_MESSAGE_ID:
+                        return org.chromium.mojo.bindings.InterfaceControlMessagesHelper.handleRunOrClosePipe(
+                                ResultInterface_Internal.MANAGER, messageWithHeader);
+
+
+
+
+
+
+                    default:
+                        return false;
+                }
+            } catch (org.chromium.mojo.bindings.DeserializationException e) {
+                System.err.println(e.toString());
+                return false;
+            }
+        }
+
+        @Override
+        public boolean acceptWithResponder(org.chromium.mojo.bindings.Message message, org.chromium.mojo.bindings.MessageReceiver receiver) {
+            try {
+                org.chromium.mojo.bindings.ServiceMessage messageWithHeader =
+                        message.asServiceMessage();
+                org.chromium.mojo.bindings.MessageHeader header = messageWithHeader.getHeader();
+                int flags = org.chromium.mojo.bindings.MessageHeader.MESSAGE_EXPECTS_RESPONSE_FLAG;
+                if (header.hasFlag(org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG)) {
+                    flags = flags | org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG;
+                }
+                if (!header.validateHeader(flags)) {
+                    return false;
+                }
+                switch(header.getType()) {
+
+                    case org.chromium.mojo.bindings.interfacecontrol.InterfaceControlMessagesConstants.RUN_MESSAGE_ID:
+                        return org.chromium.mojo.bindings.InterfaceControlMessagesHelper.handleRun(
+                                getCore(), ResultInterface_Internal.MANAGER, messageWithHeader, receiver);
+
+
+
+
+
+
+
+                    case METHOD_ORDINAL: {
+
+                        ResultInterfaceMethodParams data =
+                                ResultInterfaceMethodParams.deserialize(messageWithHeader.getPayload());
+
+                        getImpl().method(data.a, new ResultInterfaceMethodResponseParamsProxyToResponder(getCore(), receiver, header.getRequestId()));
+                        return true;
+                    }
+
+
+
+
+
+
+
+                    case SYNC_METHOD_ORDINAL: {
+
+                        ResultInterfaceSyncMethodParams data =
+                                ResultInterfaceSyncMethodParams.deserialize(messageWithHeader.getPayload());
+
+                        getImpl().syncMethod(data.a, new ResultInterfaceSyncMethodResponseParamsProxyToResponder(getCore(), receiver, header.getRequestId()));
+                        return true;
+                    }
+
+
+                    default:
+                        return false;
+                }
+            } catch (org.chromium.mojo.bindings.DeserializationException e) {
+                System.err.println(e.toString());
+                return false;
+            }
+        }
+    }
+
+
+    
+    static final class ResultInterfaceMethodParams extends org.chromium.mojo.bindings.Struct {
+
+        private static final int STRUCT_SIZE = 16;
+        private static final org.chromium.mojo.bindings.DataHeader[] VERSION_ARRAY = new org.chromium.mojo.bindings.DataHeader[] {new org.chromium.mojo.bindings.DataHeader(16, 0)};
+        private static final org.chromium.mojo.bindings.DataHeader DEFAULT_STRUCT_INFO = VERSION_ARRAY[0];
+        public boolean a;
+
+        private ResultInterfaceMethodParams(int version) {
+            super(STRUCT_SIZE, version);
+        }
+
+        public ResultInterfaceMethodParams() {
+            this(0);
+        }
+
+        public static ResultInterfaceMethodParams deserialize(org.chromium.mojo.bindings.Message message) {
+            return decode(new org.chromium.mojo.bindings.Decoder(message));
+        }
+
+        /**
+         * Similar to the method above, but deserializes from a |ByteBuffer| instance.
+         *
+         * @throws org.chromium.mojo.bindings.DeserializationException on deserialization failure.
+         */
+        public static ResultInterfaceMethodParams deserialize(java.nio.ByteBuffer data) {
+            return deserialize(new org.chromium.mojo.bindings.Message(
+                    data, new java.util.ArrayList<org.chromium.mojo.system.Handle>()));
+        }
+
+        @SuppressWarnings("unchecked")
+        public static ResultInterfaceMethodParams decode(org.chromium.mojo.bindings.@Nullable Decoder decoder0) {
+            if (decoder0 == null) {
+                return null;
+            }
+            decoder0.increaseStackDepth();
+            ResultInterfaceMethodParams result;
+            try {
+                org.chromium.mojo.bindings.DataHeader mainDataHeader = decoder0.readAndValidateDataHeader(VERSION_ARRAY);
+                final int elementsOrVersion = mainDataHeader.elementsOrVersion;
+                result = new ResultInterfaceMethodParams(elementsOrVersion);
+                    {
+                        
+                    result.a = decoder0.readBoolean(8, 0);
+                    }
+
+            } finally {
+                decoder0.decreaseStackDepth();
+            }
+            return result;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        protected final void encode(org.chromium.mojo.bindings.Encoder encoder) {
+            org.chromium.mojo.bindings.Encoder encoder0 = encoder.getEncoderAtDataOffset(DEFAULT_STRUCT_INFO);
+            
+            encoder0.encode(this.a, 8, 0);
+        }
+    }
+
+
+
+    
+    static final class ResultInterfaceMethodResponseParams extends org.chromium.mojo.bindings.Struct {
+
+        private static final int STRUCT_SIZE = 24;
+        private static final org.chromium.mojo.bindings.DataHeader[] VERSION_ARRAY = new org.chromium.mojo.bindings.DataHeader[] {new org.chromium.mojo.bindings.DataHeader(24, 0)};
+        private static final org.chromium.mojo.bindings.DataHeader DEFAULT_STRUCT_INFO = VERSION_ARRAY[0];
+        public ResultInterfaceMethodResponseParamResult result;
+
+        private ResultInterfaceMethodResponseParams(int version) {
+            super(STRUCT_SIZE, version);
+        }
+
+        public ResultInterfaceMethodResponseParams() {
+            this(0);
+        }
+
+        public static ResultInterfaceMethodResponseParams deserialize(org.chromium.mojo.bindings.Message message) {
+            return decode(new org.chromium.mojo.bindings.Decoder(message));
+        }
+
+        /**
+         * Similar to the method above, but deserializes from a |ByteBuffer| instance.
+         *
+         * @throws org.chromium.mojo.bindings.DeserializationException on deserialization failure.
+         */
+        public static ResultInterfaceMethodResponseParams deserialize(java.nio.ByteBuffer data) {
+            return deserialize(new org.chromium.mojo.bindings.Message(
+                    data, new java.util.ArrayList<org.chromium.mojo.system.Handle>()));
+        }
+
+        @SuppressWarnings("unchecked")
+        public static ResultInterfaceMethodResponseParams decode(org.chromium.mojo.bindings.@Nullable Decoder decoder0) {
+            if (decoder0 == null) {
+                return null;
+            }
+            decoder0.increaseStackDepth();
+            ResultInterfaceMethodResponseParams result;
+            try {
+                org.chromium.mojo.bindings.DataHeader mainDataHeader = decoder0.readAndValidateDataHeader(VERSION_ARRAY);
+                final int elementsOrVersion = mainDataHeader.elementsOrVersion;
+                result = new ResultInterfaceMethodResponseParams(elementsOrVersion);
+                    {
+                        
+                    result.result = ResultInterfaceMethodResponseParamResult.decode(decoder0, 8);
+                    }
+
+            } finally {
+                decoder0.decreaseStackDepth();
+            }
+            return result;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        protected final void encode(org.chromium.mojo.bindings.Encoder encoder) {
+            org.chromium.mojo.bindings.Encoder encoder0 = encoder.getEncoderAtDataOffset(DEFAULT_STRUCT_INFO);
+            
+            encoder0.encode(this.result, 8, false);
+        }
+    }
+
+    static class ResultInterfaceMethodResponseParamsForwardToCallback extends org.chromium.mojo.bindings.SideEffectFreeCloseable
+            implements org.chromium.mojo.bindings.MessageReceiver {
+        private final ResultInterface.Method_Response mCallback;
+
+        ResultInterfaceMethodResponseParamsForwardToCallback(ResultInterface.Method_Response callback) {
+            this.mCallback = callback;
+        }
+
+        @Override
+        public boolean accept(org.chromium.mojo.bindings.Message message) {
+            try {
+                org.chromium.mojo.bindings.ServiceMessage messageWithHeader =
+                        message.asServiceMessage();
+                org.chromium.mojo.bindings.MessageHeader header = messageWithHeader.getHeader();
+                if (!header.validateHeader(METHOD_ORDINAL,
+                                           org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_RESPONSE_FLAG)) {
+                    return false;
+                }
+
+                ResultInterfaceMethodResponseParams response = ResultInterfaceMethodResponseParams.deserialize(messageWithHeader.getPayload());
+
+                mCallback.call(response.result);
+                return true;
+            } catch (org.chromium.mojo.bindings.DeserializationException e) {
+                return false;
+            }
+        }
+    }
+
+    static class ResultInterfaceMethodResponseParamsProxyToResponder implements ResultInterface.Method_Response {
+
+        private final org.chromium.mojo.system.Core mCore;
+        private final org.chromium.mojo.bindings.MessageReceiver mMessageReceiver;
+        private final long mRequestId;
+
+        ResultInterfaceMethodResponseParamsProxyToResponder(
+                org.chromium.mojo.system.Core core,
+                org.chromium.mojo.bindings.MessageReceiver messageReceiver,
+                long requestId) {
+            mCore = core;
+            mMessageReceiver = messageReceiver;
+            mRequestId = requestId;
+        }
+
+        @Override
+        public void call(ResultInterfaceMethodResponseParamResult result) {
+            ResultInterfaceMethodResponseParams _response = new ResultInterfaceMethodResponseParams();
+
+            _response.result = result;
+
+            org.chromium.mojo.bindings.ServiceMessage _message =
+                    _response.serializeWithHeader(
+                            mCore,
+                            new org.chromium.mojo.bindings.MessageHeader(
+                                    METHOD_ORDINAL,
+                                    org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_RESPONSE_FLAG,
+                                    mRequestId));
+            mMessageReceiver.accept(_message);
+        }
+    }
+
+
+
+    
+    static final class ResultInterfaceSyncMethodParams extends org.chromium.mojo.bindings.Struct {
+
+        private static final int STRUCT_SIZE = 16;
+        private static final org.chromium.mojo.bindings.DataHeader[] VERSION_ARRAY = new org.chromium.mojo.bindings.DataHeader[] {new org.chromium.mojo.bindings.DataHeader(16, 0)};
+        private static final org.chromium.mojo.bindings.DataHeader DEFAULT_STRUCT_INFO = VERSION_ARRAY[0];
+        public boolean a;
+
+        private ResultInterfaceSyncMethodParams(int version) {
+            super(STRUCT_SIZE, version);
+        }
+
+        public ResultInterfaceSyncMethodParams() {
+            this(0);
+        }
+
+        public static ResultInterfaceSyncMethodParams deserialize(org.chromium.mojo.bindings.Message message) {
+            return decode(new org.chromium.mojo.bindings.Decoder(message));
+        }
+
+        /**
+         * Similar to the method above, but deserializes from a |ByteBuffer| instance.
+         *
+         * @throws org.chromium.mojo.bindings.DeserializationException on deserialization failure.
+         */
+        public static ResultInterfaceSyncMethodParams deserialize(java.nio.ByteBuffer data) {
+            return deserialize(new org.chromium.mojo.bindings.Message(
+                    data, new java.util.ArrayList<org.chromium.mojo.system.Handle>()));
+        }
+
+        @SuppressWarnings("unchecked")
+        public static ResultInterfaceSyncMethodParams decode(org.chromium.mojo.bindings.@Nullable Decoder decoder0) {
+            if (decoder0 == null) {
+                return null;
+            }
+            decoder0.increaseStackDepth();
+            ResultInterfaceSyncMethodParams result;
+            try {
+                org.chromium.mojo.bindings.DataHeader mainDataHeader = decoder0.readAndValidateDataHeader(VERSION_ARRAY);
+                final int elementsOrVersion = mainDataHeader.elementsOrVersion;
+                result = new ResultInterfaceSyncMethodParams(elementsOrVersion);
+                    {
+                        
+                    result.a = decoder0.readBoolean(8, 0);
+                    }
+
+            } finally {
+                decoder0.decreaseStackDepth();
+            }
+            return result;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        protected final void encode(org.chromium.mojo.bindings.Encoder encoder) {
+            org.chromium.mojo.bindings.Encoder encoder0 = encoder.getEncoderAtDataOffset(DEFAULT_STRUCT_INFO);
+            
+            encoder0.encode(this.a, 8, 0);
+        }
+    }
+
+
+
+    
+    static final class ResultInterfaceSyncMethodResponseParams extends org.chromium.mojo.bindings.Struct {
+
+        private static final int STRUCT_SIZE = 24;
+        private static final org.chromium.mojo.bindings.DataHeader[] VERSION_ARRAY = new org.chromium.mojo.bindings.DataHeader[] {new org.chromium.mojo.bindings.DataHeader(24, 0)};
+        private static final org.chromium.mojo.bindings.DataHeader DEFAULT_STRUCT_INFO = VERSION_ARRAY[0];
+        public ResultInterfaceSyncMethodResponseParamResult result;
+
+        private ResultInterfaceSyncMethodResponseParams(int version) {
+            super(STRUCT_SIZE, version);
+        }
+
+        public ResultInterfaceSyncMethodResponseParams() {
+            this(0);
+        }
+
+        public static ResultInterfaceSyncMethodResponseParams deserialize(org.chromium.mojo.bindings.Message message) {
+            return decode(new org.chromium.mojo.bindings.Decoder(message));
+        }
+
+        /**
+         * Similar to the method above, but deserializes from a |ByteBuffer| instance.
+         *
+         * @throws org.chromium.mojo.bindings.DeserializationException on deserialization failure.
+         */
+        public static ResultInterfaceSyncMethodResponseParams deserialize(java.nio.ByteBuffer data) {
+            return deserialize(new org.chromium.mojo.bindings.Message(
+                    data, new java.util.ArrayList<org.chromium.mojo.system.Handle>()));
+        }
+
+        @SuppressWarnings("unchecked")
+        public static ResultInterfaceSyncMethodResponseParams decode(org.chromium.mojo.bindings.@Nullable Decoder decoder0) {
+            if (decoder0 == null) {
+                return null;
+            }
+            decoder0.increaseStackDepth();
+            ResultInterfaceSyncMethodResponseParams result;
+            try {
+                org.chromium.mojo.bindings.DataHeader mainDataHeader = decoder0.readAndValidateDataHeader(VERSION_ARRAY);
+                final int elementsOrVersion = mainDataHeader.elementsOrVersion;
+                result = new ResultInterfaceSyncMethodResponseParams(elementsOrVersion);
+                    {
+                        
+                    result.result = ResultInterfaceSyncMethodResponseParamResult.decode(decoder0, 8);
+                    }
+
+            } finally {
+                decoder0.decreaseStackDepth();
+            }
+            return result;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        protected final void encode(org.chromium.mojo.bindings.Encoder encoder) {
+            org.chromium.mojo.bindings.Encoder encoder0 = encoder.getEncoderAtDataOffset(DEFAULT_STRUCT_INFO);
+            
+            encoder0.encode(this.result, 8, false);
+        }
+    }
+
+    static class ResultInterfaceSyncMethodResponseParamsForwardToCallback extends org.chromium.mojo.bindings.SideEffectFreeCloseable
+            implements org.chromium.mojo.bindings.MessageReceiver {
+        private final ResultInterface.SyncMethod_Response mCallback;
+
+        ResultInterfaceSyncMethodResponseParamsForwardToCallback(ResultInterface.SyncMethod_Response callback) {
+            this.mCallback = callback;
+        }
+
+        @Override
+        public boolean accept(org.chromium.mojo.bindings.Message message) {
+            try {
+                org.chromium.mojo.bindings.ServiceMessage messageWithHeader =
+                        message.asServiceMessage();
+                org.chromium.mojo.bindings.MessageHeader header = messageWithHeader.getHeader();
+                if (!header.validateHeader(SYNC_METHOD_ORDINAL,
+                                           org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_RESPONSE_FLAG| org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG)) {
+                    return false;
+                }
+
+                ResultInterfaceSyncMethodResponseParams response = ResultInterfaceSyncMethodResponseParams.deserialize(messageWithHeader.getPayload());
+
+                mCallback.call(response.result);
+                return true;
+            } catch (org.chromium.mojo.bindings.DeserializationException e) {
+                return false;
+            }
+        }
+    }
+
+    static class ResultInterfaceSyncMethodResponseParamsProxyToResponder implements ResultInterface.SyncMethod_Response {
+
+        private final org.chromium.mojo.system.Core mCore;
+        private final org.chromium.mojo.bindings.MessageReceiver mMessageReceiver;
+        private final long mRequestId;
+
+        ResultInterfaceSyncMethodResponseParamsProxyToResponder(
+                org.chromium.mojo.system.Core core,
+                org.chromium.mojo.bindings.MessageReceiver messageReceiver,
+                long requestId) {
+            mCore = core;
+            mMessageReceiver = messageReceiver;
+            mRequestId = requestId;
+        }
+
+        @Override
+        public void call(ResultInterfaceSyncMethodResponseParamResult result) {
+            ResultInterfaceSyncMethodResponseParams _response = new ResultInterfaceSyncMethodResponseParams();
+
+            _response.result = result;
+
+            org.chromium.mojo.bindings.ServiceMessage _message =
+                    _response.serializeWithHeader(
+                            mCore,
+                            new org.chromium.mojo.bindings.MessageHeader(
+                                    SYNC_METHOD_ORDINAL,
+                                    org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_RESPONSE_FLAG| org.chromium.mojo.bindings.MessageHeader.MESSAGE_IS_SYNC_FLAG,
+                                    mRequestId));
+            mMessageReceiver.accept(_message);
+        }
+    }
+
+
+
+}
diff --git a/mojo/golden/generated/java/org/chromium/golden/ResultTestError.java.golden b/mojo/golden/generated/java/org/chromium/golden/ResultTestError.java.golden
new file mode 100644
index 0000000..c74ab87
--- /dev/null
+++ b/mojo/golden/generated/java/org/chromium/golden/ResultTestError.java.golden
@@ -0,0 +1,74 @@
+// ResultTestError.java is auto generated by mojom_bindings_generator.py, do not edit
+
+
+// Copyright 2014 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 autogenerated by:
+//     mojo/public/tools/bindings/mojom_bindings_generator.py
+// For:
+//     results.test-mojom
+//
+
+package org.chromium.golden;
+
+import androidx.annotation.IntDef;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+
+@NullMarked
+@SuppressWarnings("NullAway")
+public final class ResultTestError extends org.chromium.mojo.bindings.Struct {
+
+    private static final int STRUCT_SIZE = 8;
+    private static final org.chromium.mojo.bindings.DataHeader[] VERSION_ARRAY = new org.chromium.mojo.bindings.DataHeader[] {new org.chromium.mojo.bindings.DataHeader(8, 0)};
+    private static final org.chromium.mojo.bindings.DataHeader DEFAULT_STRUCT_INFO = VERSION_ARRAY[0];
+
+    private ResultTestError(int version) {
+        super(STRUCT_SIZE, version);
+    }
+
+    public ResultTestError() {
+        this(0);
+    }
+
+    public static ResultTestError deserialize(org.chromium.mojo.bindings.Message message) {
+        return decode(new org.chromium.mojo.bindings.Decoder(message));
+    }
+
+    /**
+     * Similar to the method above, but deserializes from a |ByteBuffer| instance.
+     *
+     * @throws org.chromium.mojo.bindings.DeserializationException on deserialization failure.
+     */
+    public static ResultTestError deserialize(java.nio.ByteBuffer data) {
+        return deserialize(new org.chromium.mojo.bindings.Message(
+                data, new java.util.ArrayList<org.chromium.mojo.system.Handle>()));
+    }
+
+    @SuppressWarnings("unchecked")
+    public static ResultTestError decode(org.chromium.mojo.bindings.@Nullable Decoder decoder0) {
+        if (decoder0 == null) {
+            return null;
+        }
+        decoder0.increaseStackDepth();
+        ResultTestError result;
+        try {
+            org.chromium.mojo.bindings.DataHeader mainDataHeader = decoder0.readAndValidateDataHeader(VERSION_ARRAY);
+            final int elementsOrVersion = mainDataHeader.elementsOrVersion;
+            result = new ResultTestError(elementsOrVersion);
+
+        } finally {
+            decoder0.decreaseStackDepth();
+        }
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected final void encode(org.chromium.mojo.bindings.Encoder encoder) {
+        encoder.getEncoderAtDataOffset(DEFAULT_STRUCT_INFO);
+    }
+}
\ No newline at end of file
diff --git a/mojo/golden/generated/java/org/chromium/golden/Typemapped.java.golden b/mojo/golden/generated/java/org/chromium/golden/Typemapped.java.golden
new file mode 100644
index 0000000..e3b7b87
--- /dev/null
+++ b/mojo/golden/generated/java/org/chromium/golden/Typemapped.java.golden
@@ -0,0 +1,105 @@
+// Typemapped.java is auto generated by mojom_bindings_generator.py, do not edit
+
+
+// Copyright 2014 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 autogenerated by:
+//     mojo/public/tools/bindings/mojom_bindings_generator.py
+// For:
+//     typemap.test-mojom
+//
+
+package org.chromium.golden;
+
+import androidx.annotation.IntDef;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+
+@NullMarked
+@SuppressWarnings("NullAway")
+public final class Typemapped extends org.chromium.mojo.bindings.Struct {
+
+    private static final int STRUCT_SIZE = 24;
+    private static final org.chromium.mojo.bindings.DataHeader[] VERSION_ARRAY = new org.chromium.mojo.bindings.DataHeader[] {new org.chromium.mojo.bindings.DataHeader(24, 0)};
+    private static final org.chromium.mojo.bindings.DataHeader DEFAULT_STRUCT_INFO = VERSION_ARRAY[0];
+    public byte field;
+    public @Nullable Integer optional;
+    public @Nullable Boolean[] optionalContainer;
+
+    private Typemapped(int version) {
+        super(STRUCT_SIZE, version);
+    }
+
+    public Typemapped() {
+        this(0);
+    }
+
+    public static Typemapped deserialize(org.chromium.mojo.bindings.Message message) {
+        return decode(new org.chromium.mojo.bindings.Decoder(message));
+    }
+
+    /**
+     * Similar to the method above, but deserializes from a |ByteBuffer| instance.
+     *
+     * @throws org.chromium.mojo.bindings.DeserializationException on deserialization failure.
+     */
+    public static Typemapped deserialize(java.nio.ByteBuffer data) {
+        return deserialize(new org.chromium.mojo.bindings.Message(
+                data, new java.util.ArrayList<org.chromium.mojo.system.Handle>()));
+    }
+
+    @SuppressWarnings("unchecked")
+    public static Typemapped decode(org.chromium.mojo.bindings.@Nullable Decoder decoder0) {
+        if (decoder0 == null) {
+            return null;
+        }
+        decoder0.increaseStackDepth();
+        Typemapped result;
+        try {
+            org.chromium.mojo.bindings.DataHeader mainDataHeader = decoder0.readAndValidateDataHeader(VERSION_ARRAY);
+            final int elementsOrVersion = mainDataHeader.elementsOrVersion;
+            result = new Typemapped(elementsOrVersion);
+                {
+                    
+                result.field = decoder0.readByte(8);
+                }
+                {
+                    
+                if (decoder0.readBoolean(9, 0)) {
+                  result.optional = new Integer(decoder0.readInt(12));
+                } else {
+                  result.optional = null;
+                }
+                }
+                {
+                    
+                result.optionalContainer = decoder0.readBooleanNullables(16, org.chromium.mojo.bindings.BindingsHelper.ELEMENT_NULLABLE, org.chromium.mojo.bindings.BindingsHelper.UNSPECIFIED_ARRAY_LENGTH);
+                }
+
+        } finally {
+            decoder0.decreaseStackDepth();
+        }
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected final void encode(org.chromium.mojo.bindings.Encoder encoder) {
+        org.chromium.mojo.bindings.Encoder encoder0 = encoder.getEncoderAtDataOffset(DEFAULT_STRUCT_INFO);
+        
+        encoder0.encode(this.field, 8);
+        final boolean optional$flag = this.optional != null;
+        final int optional$value = optional$flag
+            ? this.optional
+            : 0;
+        
+        encoder0.encode(optional$flag, 9, 0);
+        
+        encoder0.encode(optional$value, 12);
+        
+        encoder0.encode(this.optionalContainer, 16, org.chromium.mojo.bindings.BindingsHelper.ELEMENT_NULLABLE, org.chromium.mojo.bindings.BindingsHelper.UNSPECIFIED_ARRAY_LENGTH);
+    }
+}
\ No newline at end of file
diff --git a/mojo/public/tools/bindings/generators/mojom_java_generator.py b/mojo/public/tools/bindings/generators/mojom_java_generator.py
index bb55b150..e89e3a4 100644
--- a/mojo/public/tools/bindings/generators/mojom_java_generator.py
+++ b/mojo/public/tools/bindings/generators/mojom_java_generator.py
@@ -638,16 +638,17 @@
     # srcjar in the output directory.
     basename = "%s.srcjar" % self.module.path
     zip_filename = os.path.join(self.output_dir, basename)
-    with TempDir() as temp_java_root:
-      self.output_dir = os.path.join(temp_java_root, package_path)
-      self._DoGenerateFiles();
-      with action_helpers.atomic_output(zip_filename) as f:
-        zip_helpers.zip_directory(f, temp_java_root)
 
     if args.java_output_directory:
       # If requested, generate the java files directly into indicated directory.
       self.output_dir = os.path.join(args.java_output_directory, package_path)
       self._DoGenerateFiles();
+    else:
+      with TempDir() as temp_java_root:
+        self.output_dir = os.path.join(temp_java_root, package_path)
+        self._DoGenerateFiles()
+        with action_helpers.atomic_output(zip_filename) as f:
+          zip_helpers.zip_directory(f, temp_java_root)
 
   def GetJinjaParameters(self):
     return {
diff --git a/net/BUILD.gn b/net/BUILD.gn
index b580309..4e802dba 100644
--- a/net/BUILD.gn
+++ b/net/BUILD.gn
@@ -395,6 +395,8 @@
     "cert/test_root_certs.h",
     "cert/time_conversions.cc",
     "cert/time_conversions.h",
+    "cert/two_qwac.cc",
+    "cert/two_qwac.h",
     "cert/x509_cert_types.cc",
     "cert/x509_cert_types.h",
     "cert/x509_certificate.cc",
@@ -2750,6 +2752,7 @@
     "cert/signed_certificate_timestamp_unittest.cc",
     "cert/test_root_certs_unittest.cc",
     "cert/time_conversions_unittest.cc",
+    "cert/two_qwac_unittest.cc",
     "cert/x509_cert_types_unittest.cc",
     "cert/x509_certificate_unittest.cc",
     "cert/x509_util_unittest.cc",
diff --git a/net/cert/qwac.cc b/net/cert/qwac.cc
index 38a9eb3b..189104757 100644
--- a/net/cert/qwac.cc
+++ b/net/cert/qwac.cc
@@ -4,13 +4,8 @@
 
 #include "net/cert/qwac.h"
 
-#include "base/base64.h"
-#include "base/base64url.h"
 #include "base/containers/contains.h"
-#include "base/json/json_reader.h"
 #include "base/logging.h"
-#include "base/strings/string_split.h"
-#include "net/cert/x509_util.h"
 #include "third_party/boringssl/src/pki/parser.h"
 
 namespace net {
@@ -209,333 +204,4 @@
   return QwacEkuStatus::kHasQwacEku;
 }
 
-Jades2QwacHeader::Jades2QwacHeader() = default;
-Jades2QwacHeader::Jades2QwacHeader(const Jades2QwacHeader& other) = default;
-Jades2QwacHeader::Jades2QwacHeader(Jades2QwacHeader&& other) = default;
-Jades2QwacHeader::~Jades2QwacHeader() = default;
-
-namespace {
-
-std::optional<Jades2QwacHeader> ParseJades2QwacHeader(
-    std::string_view header_string) {
-  Jades2QwacHeader parsed_header;
-  // The header of a JWS is a JSON-encoded object (RFC 7515, section 4).
-  std::optional<base::Value> header_value =
-      base::JSONReader::Read(header_string, base::JSON_PARSE_RFC);
-  if (!header_value.has_value() || !header_value->is_dict()) {
-    return std::nullopt;
-  }
-  base::Value::Dict& header = header_value->GetDict();
-
-  // "alg" (Algorithm) parameter - RFC 7515, section 4.1.1
-  std::string* alg = header.FindString("alg");
-  if (!alg || *alg == "") {
-    return std::nullopt;
-  }
-  parsed_header.sig_alg = *alg;
-  header.Remove("alg");
-  // TODO(crbug.com/392929826): process alg (check that it matches the alg in
-  // x5c).
-
-  // "kid" (Key ID) parameter - RFC 7515, section 4.1.4
-  //
-  // The Key ID can be of any type and is used to identify the key used for
-  // signing. In this profile, the key used to verify the signature will be
-  // found in the "x5c" parameter, so the "kid" is useless to us and is ignored.
-  header.Remove("kid");
-
-  // "cty" (Content Type) parameter - RFC 7515, section 4.1.10
-  //
-  // ETSI TS 119 411-5 V2.1.1 requires the "cty" parameter to be
-  // "TLS-Certificate-Binding-v1".
-  std::string* cty = header.FindString("cty");
-  if (!cty || *cty != "TLS-Certificate-Binding-v1") {
-    return std::nullopt;
-  }
-  header.Remove("cty");
-
-  // "x5t#S256" (X.509 Certificate SHA-256 Thumbprint) parameter (RFC 7515,
-  // section 4.1.8) is the base64url-encoded SHA-256 thumbprint of the
-  // DER encoding of the X.509 certificate used to sign the JWS. This value is
-  // not needed to verify the signature (the leaf cert of the "x5c" parameter is
-  // the signing cert), and it is optional according to RFC 7515, so we ignore
-  // it.
-  if (header.FindString("x5t#S256")) {
-    header.Remove("x5t#S256");
-  }
-
-  // "x5c" (X.509 Certificate Chain) header - RFC 7515 section 4.1.6
-  base::ListValue* x5c_list = header.FindList("x5c");
-  if (!x5c_list) {
-    return std::nullopt;
-  }
-
-  size_t i = 0;
-  bssl::UniquePtr<CRYPTO_BUFFER> leaf;
-  std::vector<bssl::UniquePtr<CRYPTO_BUFFER>> intermediates;
-  for (const base::Value& cert_value : *x5c_list) {
-    // RFC 7515 section 4.1.6:
-    // "Each string in the array is a base64-encoded (not base64url-encoded) DER
-    // PKIX certificate value."
-    if (!cert_value.is_string()) {
-      return std::nullopt;
-    }
-    auto cert_bytes = base::Base64Decode(cert_value.GetString());
-    if (!cert_bytes.has_value()) {
-      return std::nullopt;
-    }
-    auto buf = x509_util::CreateCryptoBuffer(*cert_bytes);
-    if (i == 0) {
-      leaf = std::move(buf);
-    } else {
-      intermediates.emplace_back(std::move(buf));
-    }
-    i++;
-  }
-  parsed_header.two_qwac_cert = X509Certificate::CreateFromBuffer(
-      std::move(leaf), std::move(intermediates));
-  if (!parsed_header.two_qwac_cert) {
-    return std::nullopt;
-  }
-  header.Remove("x5c");
-
-  // "iat" header. TS 119 182-1 section 5.1.11 defines this header parameter to
-  // be almost the same as RFC 7519's JWT "iat" claim. Despite TS 119 182-1
-  // citing RFC 7519 as the definition for this header parameter, JWS header
-  // parameters and JWT claims are not the same thing. In any case, ETSI defines
-  // this header to be an integer representing the claimed signing time.
-  //
-  // I see no indication in TS 119 411-5 that "iat" is required to be present,
-  // and RFC 7519 specifies it as optional. Further, I haven't yet found an
-  // indication as to how one would interpret and apply this field in signature
-  // validation, so I'm ignoring it.
-  if (header.FindInt("iat")) {
-    header.Remove("iat");
-  }
-
-  // "exp" header. TS 119 411-5 Annex B defines this as the expiry date of the
-  // binding, and like TS 119 182-1 for the "iat" header, incorrectly cites RFC
-  // 7519's claim definition of the field (section 4.1.4). Unlike the ETSI
-  // specification for "iat" that restricts its NumericDate type to an integer,
-  // we only have the RFC 7519 definition of "exp" to use, which defines
-  // NumericDate as a JSON numeric value. RFC 7159 allows JSON numeric values to
-  // contain a fraction part.
-  //
-  // Like the "iat" header, TS 119 411-5 does not require the presence of "exp",
-  // RFC 7519 specifies it as optional, and there is no indication in any ETSI
-  // spec on how this field would affect signature validation, so it is ignored.
-  if (header.FindDouble("exp")) {
-    header.Remove("exp");
-  }
-
-  // "sigD" header - ETSI TS 119 182-1 section 5.2.8, with additional
-  // requirements specified in ETSI TS 119 411-5 Annex B. This parameter is a
-  // JSON object and is required to be present.
-  base::DictValue* sig_d = header.FindDict("sigD");
-  if (!sig_d) {
-    return std::nullopt;
-  }
-
-  // The sigD header must have a "mId" (mechanism ID) of
-  // "http://uri.etsi.org/19182/ObjectIdByURIHash". (ETSI TS 119 411-5 Annex B.)
-  std::string* m_id = sig_d->FindString("mId");
-  if (!m_id || *m_id != "http://uri.etsi.org/19182/ObjectIdByURIHash") {
-    return std::nullopt;
-  }
-  sig_d->Remove("mId");
-
-  // The sigD header must have a "pars" member, which is a list of strings. We
-  // don't care about the contents of this list, but its size must match that of
-  // "hashV". (ETSI 119 182-1 clause 5.2.8.)
-  const base::ListValue* pars = sig_d->FindList("pars");
-  if (!pars) {
-    return std::nullopt;
-  }
-  size_t bound_cert_count = pars->size();
-  for (const base::Value& par : *pars) {
-    if (!par.is_string()) {
-      return std::nullopt;
-    }
-  }
-  sig_d->Remove("pars");
-
-  // The sigD header must have a "hashM" member (TS 119 182-1
-  // section 5.2.8.3.3), which is a string identifying the hashing algorithm
-  // used for the "hashV" member. ETSI TS 119 411-5 only requires that S256,
-  // S384, and S512 be supported.
-  std::string* hash_m = sig_d->FindString("hashM");
-  if (!hash_m) {
-    return std::nullopt;
-  }
-  if (*hash_m == "S256") {
-    parsed_header.hash_alg = crypto::hash::kSha256;
-  } else if (*hash_m == "S384") {
-    // TODO(crbug.com/392929826): add SHA-384 to crypto/hash.h.
-    return std::nullopt;
-  } else if (*hash_m == "S512") {
-    parsed_header.hash_alg = crypto::hash::kSha512;
-  } else {
-    // Unsupported hashing algorithm.
-    return std::nullopt;
-  }
-  sig_d->Remove("hashM");
-
-  // The sigD header must have a "hashV" member, which is a list of
-  // base64url-encoded digest values of the base64url-encoded data objects.
-  // (ETSI TS 119 182-1 clause 5.2.8. The "b64" header parameter is absent, so
-  // the digest is computed over the base64url-encoded data object instead of
-  // computed directly over the data object.)
-  const base::ListValue* hash_v = sig_d->FindList("hashV");
-  if (!hash_v) {
-    return std::nullopt;
-  }
-  if (hash_v->size() != bound_cert_count) {
-    return std::nullopt;
-  }
-  parsed_header.bound_cert_hashes.reserve(bound_cert_count);
-  for (const base::Value& hash_value : *hash_v) {
-    const std::string* hash_b64url = hash_value.GetIfString();
-    if (!hash_b64url) {
-      return std::nullopt;
-    }
-    // ETSI TS 119 182-1 fails to specify the definition of "base64url-encoded".
-    // Given that other uses of base64url encoding come from the JWS spec, and
-    // JWS disallows padding in its base64url encoding, we disallow it here as
-    // well.
-    auto hash = base::Base64UrlDecode(
-        *hash_b64url, base::Base64UrlDecodePolicy::DISALLOW_PADDING);
-    if (!hash.has_value()) {
-      return std::nullopt;
-    }
-    parsed_header.bound_cert_hashes.emplace_back(std::move(*hash));
-  }
-  sig_d->Remove("hashV");
-
-  // Given the mId used, the sigD header may have a "ctys" member (TS 119 182-1
-  // clause 5.2.8.3.3), with semantics and syntax as specified in clause
-  // 5.2.8.1. Clause 5.2.8.1 defines the "ctys" member's syntax to be an array
-  // of strings. This array has the same length as the "pars" (and "hashV")
-  // array, and each element is the content type (RFC 7515 section 4.1.10) of
-  // the data object referred to by the value in "pars" at the same index.
-  // RFC 7515 specifies that the content type parameter is ignored by JWS
-  // implementations and processing of it is performed by the JWS application.
-  // Since neither ETSI TS 119 182-1 nor TS 119 411-5 provide guidance on the
-  // content type used for the individual data objects, this implementation has
-  // no opinion on the stated content types.
-  const base::ListValue* ctys = sig_d->FindList("ctys");
-  if (ctys) {
-    if (ctys->size() != bound_cert_count) {
-      return std::nullopt;
-    }
-    for (const base::Value& cty_value : *ctys) {
-      if (!cty_value.is_string()) {
-        return std::nullopt;
-      }
-    }
-  } else if (sig_d->contains("ctys")) {
-    // check that there isn't a "ctys" of the wrong type
-    return std::nullopt;
-  }
-  sig_d->Remove("ctys");
-
-  // sigD has no other members than the aforementioned "mId", "pars", "hashM",
-  // "hashV", and "ctys". (ETSI TS 119 182-1 clause 5.2.8.)
-  if (!sig_d->empty()) {
-    return std::nullopt;
-  }
-  header.Remove("sigD");
-
-  // The header must not contain fields other than "alg", "kid", "cty",
-  // "x5t#S256", "x5c", "iat", "exp", or "sigD", as required by ETSI TS 119
-  // 411-5 V2.1.1, Annex B.
-  //
-  // ETSI TS 119 182-1 V1.2.1 section 5.1.9 specifies that if the "sigD" header
-  // parameter is present, then the "crit" header parameter shall also be
-  // present with "sigD" as one of its array elements. This is in conflict with
-  // the requirement in 119 411-5 V2.1.1 Annex B. To resolve this conflict, this
-  // implementation will allow the presence of "crit", but if it is present, it
-  // must be an array containing exactly "sigD".
-  const auto* crit_value = header.Find("crit");
-  if (crit_value) {
-    if (!crit_value->is_list()) {
-      return std::nullopt;
-    }
-    const auto& crit_list = crit_value->GetList();
-    if (crit_list.size() != 1 || !crit_list.contains("sigD")) {
-      return std::nullopt;
-    }
-  }
-  header.Remove("crit");
-
-  if (!header.empty()) {
-    return std::nullopt;
-  }
-
-  return parsed_header;
-}
-
-}  // namespace
-
-TwoQwacCertBinding::TwoQwacCertBinding(Jades2QwacHeader header,
-                                       std::string header_string,
-                                       std::vector<uint8_t> signature)
-    : header(header), header_string(header_string), signature(signature) {}
-
-TwoQwacCertBinding::TwoQwacCertBinding(const TwoQwacCertBinding& other) =
-    default;
-TwoQwacCertBinding::TwoQwacCertBinding(TwoQwacCertBinding&& other) = default;
-TwoQwacCertBinding::~TwoQwacCertBinding() = default;
-
-std::optional<TwoQwacCertBinding> TwoQwacCertBinding::Parse(
-    std::string_view jws) {
-  // ETSI TS 119 411-5 V2.1.1 Annex B: The JAdES signatures shall be serialized
-  // using JWS Compact Serialization as specified in IETF RFC 7515.
-  //
-  // The JWS Compact Serialization format consists of 3 components separated by
-  // a dot (".") (RFC 7515, section 7.1).
-  std::vector<std::string_view> jws_components = base::SplitStringPiece(
-      jws, ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
-  if (jws_components.size() != 3) {
-    // Reject a JWS that does not consist of 3 components.
-    return std::nullopt;
-  }
-  std::string_view header_b64 = jws_components[0];
-  std::string_view payload_b64 = jws_components[1];
-  std::string_view signature_b64 = jws_components[2];
-
-  // The 3 components of a JWS are the header, the payload, and the signature.
-  // The components are base64url encoded (RFC 7515, section 7.1) and the base64
-  // encoding is without any padding "=" characters (Ibid., section 2).
-  std::string header_string;
-  if (!base::Base64UrlDecode(header_b64,
-                             base::Base64UrlDecodePolicy::DISALLOW_PADDING,
-                             &header_string)) {
-    return std::nullopt;
-  }
-  std::optional<std::vector<uint8_t>> signature = base::Base64UrlDecode(
-      signature_b64, base::Base64UrlDecodePolicy::DISALLOW_PADDING);
-  if (!signature.has_value()) {
-    return std::nullopt;
-  }
-
-  // Parse the JWS/JAdES header.
-  auto header = ParseJades2QwacHeader(header_string);
-  if (!header.has_value()) {
-    return std::nullopt;
-  }
-
-  // ETSI TS 119 411-5 V2.1.1 Annex B specifies a "sigD" header parameter. This
-  // header parameter is defined in ETSI TS 119 182-1 V1.2.1, section 5.2.8,
-  // which states "The sigD header parameter shall not appear in JAdES
-  // signatures whose JWS Payload is attached". Thus, it can be inferred that
-  // the JWS Payload is detached. A detached payload for a JWS means that the
-  // encoded payload is empty (RFC 7515, Appendix F).
-  if (!payload_b64.empty()) {
-    return std::nullopt;
-  }
-
-  TwoQwacCertBinding cert_binding(*header, header_string, *signature);
-  return cert_binding;
-}
-
 }  // namespace net
diff --git a/net/cert/qwac.h b/net/cert/qwac.h
index a0ab62b..2a13014 100644
--- a/net/cert/qwac.h
+++ b/net/cert/qwac.h
@@ -9,14 +9,10 @@
 
 #include <optional>
 #include <set>
-#include <string_view>
 #include <utility>
 #include <vector>
 
-#include "base/memory/raw_ptr.h"
-#include "crypto/hash.h"
 #include "net/base/net_export.h"
-#include "net/cert/x509_certificate.h"
 #include "third_party/boringssl/src/pki/input.h"
 #include "third_party/boringssl/src/pki/parsed_certificate.h"
 
@@ -158,68 +154,6 @@
 NET_EXPORT_PRIVATE QwacEkuStatus
 Has2QwacEku(const bssl::ParsedCertificate* cert);
 
-// Contains fields from a JAdES (ETSI TS 119 182-1) signature header needed for
-// verifying 2-QWAC TLS certificate bindings. While JAdES is a profile of JWS
-// (RFC 7515), this is not general-purpose JWS or JWT code. It is also not
-// general-purpose JAdES code, as only fields needed for 2-QWAC TLS certificate
-// bindings are present here.
-struct NET_EXPORT_PRIVATE Jades2QwacHeader {
-  Jades2QwacHeader();
-  Jades2QwacHeader(const Jades2QwacHeader& other);
-  Jades2QwacHeader(Jades2QwacHeader&& other);
-  ~Jades2QwacHeader();
-
-  // The signature algorithm used to sign the JWS, as provided by the "alg" JWS
-  // Header Parameter (RFC 7515, section 4.1.1). Valid values for this field can
-  // be found in the JSON Web Signature and Encryption Algorithms IANA registry
-  // (https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms).
-  // The consumer of this struct must check that the algorithm provided in this
-  // field matches the signature algorithm of the leaf cert in |two_qwac_cert|.
-  std::string sig_alg;
-
-  // The certificate chain with a leaf cert that is a 2-QWAC. This certificate
-  // chain is used to sign the JWS, which binds the 2-QWAC to a set of TLS
-  // serverAuth certificates.
-  scoped_refptr<net::X509Certificate> two_qwac_cert;
-
-  // The hash algorithm used to hash the bound certificates.
-  crypto::hash::HashKind hash_alg;
-
-  // The hashes of the bound certificates (base64url-encoded), hashed using
-  // |hash_alg|. Note: this is Digest(base64url(cert)), because that's what the
-  // JAdES and 2-QWAC specs require (not that it makes any sense to do that).
-  std::vector<std::vector<uint8_t>> bound_cert_hashes;
-};
-
-// A TwoQwacCertBinding represents a JAdES Signature (ETSI TS 119 182-1,
-// clause 3.1) used for 2-QWACs (ETSI TS 119 411-5, clause 6.2.2). It comes from
-// a TLS Certificate Binding (ETSI TS 119 411-5 annex B). Note that a JAdES
-// Signature (which is also a JWS, a.k.a. JSON Web Signature) consists of a
-// header and a cryptographic signature, not just a signature.
-struct NET_EXPORT_PRIVATE TwoQwacCertBinding {
-  TwoQwacCertBinding(Jades2QwacHeader header,
-                     std::string header_string,
-                     std::vector<uint8_t> signature);
-  TwoQwacCertBinding(const TwoQwacCertBinding& other);
-  TwoQwacCertBinding(TwoQwacCertBinding&& other);
-  ~TwoQwacCertBinding();
-
-  // Parses a TLS Certificate Binding structure that contains a 2-QWAC
-  // certificate chain.
-  // TODO(crbug.com/392929826): Add a fuzz test for this function.
-  static std::optional<TwoQwacCertBinding> Parse(std::string_view jws);
-
-  // The parsed JWS Header from the certificate binding structure.
-  Jades2QwacHeader header;
-
-  // The unparsed JWS Header, needed for verifying the signature.
-  std::string header_string;
-
-  // The JWS Signature (RFC 7515 section 2)/JAdES Signature Value (ETSI TS 119
-  // 182-1 clause 3.1) from the certificate binding structure.
-  std::vector<uint8_t> signature;
-};
-
 }  // namespace net
 
 #endif  // NET_CERT_QWAC_H_
diff --git a/net/cert/qwac_fuzztest.cc b/net/cert/qwac_fuzztest.cc
index 3905260..3c7e959 100644
--- a/net/cert/qwac_fuzztest.cc
+++ b/net/cert/qwac_fuzztest.cc
@@ -9,6 +9,7 @@
 #include "base/containers/span.h"
 #include "base/containers/to_vector.h"
 #include "net/cert/qwac.h"
+#include "net/cert/two_qwac.h"
 #include "third_party/fuzztest/src/fuzztest/fuzztest.h"
 #include "third_party/googletest/src/googletest/include/gtest/gtest.h"
 
diff --git a/net/cert/qwac_unittest.cc b/net/cert/qwac_unittest.cc
index 8fa2087..b3d14c3 100644
--- a/net/cert/qwac_unittest.cc
+++ b/net/cert/qwac_unittest.cc
@@ -6,11 +6,6 @@
 
 #include <stdint.h>
 
-#include "base/base64.h"
-#include "base/base64url.h"
-#include "base/functional/callback.h"
-#include "base/json/json_string_value_serializer.h"
-#include "base/values.h"
 #include "net/test/cert_builder.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -326,433 +321,5 @@
   }
 }
 
-// Builds a header that has the minimal required set of parameters
-base::DictValue MinimalBindingHeader() {
-  auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
-  base::Value::Dict header =
-      base::Value::Dict()
-          .Set("alg", "test alg")
-          .Set("cty", "TLS-Certificate-Binding-v1")
-          .Set("x5c", base::Value::List()
-                          // These are base64 encoded, not base64url encoded
-                          .Append(base::Base64Encode(leaf->GetDER()))
-                          .Append(base::Base64Encode(root->GetDER())))
-          .Set("sigD",
-               base::Value::Dict()
-                   .Set("mId", "http://uri.etsi.org/19182/ObjectIdByURIHash")
-                   .Set("pars", base::Value::List().Append("").Append(""))
-                   .Set("hashM", "S256")
-                   // These are hashes of the certs that this
-                   // TlsCertificateBinding binds, not hashes of the certs in
-                   // the x5c cert chain.
-                   .Set("hashV", base::Value::List()
-                                     .Append("fakehash1A")
-                                     .Append("fakehash2A")));
-  return header;
-}
-
-// Creates a TLS Certificate Binding from the provided header. This test helper
-// leaves the signature empty.
-std::string CreateTwoQwacCertBinding(const base::DictValue& header) {
-  std::string header_string;
-  EXPECT_TRUE(JSONStringValueSerializer(&header_string).Serialize(header));
-  // Create the JWS from the header.
-  std::string jws;
-  base::Base64UrlEncode(header_string,
-                        base::Base64UrlEncodePolicy::OMIT_PADDING, &jws);
-  // Add empty payload and signature to JWS.
-  jws += "..";
-  return jws;
-}
-
-TEST(ParseTlsCertificateBinding, MinimalValidBinding) {
-  // TODO(crbug.com/392929826): Once we start validating signatures, these
-  // tests will probably need to be updated to have real algorithms,
-  // certificates, and signatures. (This is assuming that some basic checks are
-  // added to the parsing code, e.g. that we can parse certs into a
-  // net::X509Certificate and check that the "alg" matches the leaf cert.)
-
-  // Build a header that has the minimally required set of parameters
-  base::DictValue header = MinimalBindingHeader();
-  std::string jws = CreateTwoQwacCertBinding(header);
-  auto cert_binding = TwoQwacCertBinding::Parse(jws);
-  ASSERT_TRUE(cert_binding.has_value());
-}
-
-TEST(ParseTlsCertificateBinding, MaximalValidBinding) {
-  // Create a header that has all allowed fields
-  base::DictValue header = MinimalBindingHeader();
-  header.Set("kid", base::Value::Dict()
-                        .Set("random key", "random value")
-                        .Set("kids can have", "whatever they want"));
-  header.Set("x5t#S256", "base64urlhashA");
-  header.Set("iat", 12345);
-  header.Set("exp", 67.89);
-  header.Set("crit", base::ListValue().Append("sigD"));
-
-  header.FindDict("sigD")->Set(
-      "ctys",
-      base::Value::List().Append("content-type1").Append("content-type2"));
-
-  std::string jws = CreateTwoQwacCertBinding(header);
-  auto cert_binding = TwoQwacCertBinding::Parse(jws);
-  ASSERT_TRUE(cert_binding.has_value());
-}
-
-// Test failure when the JWS header isn't a JSON object.
-TEST(ParseTlsCertificateBinding, JwsHeaderNotObject) {
-  std::string header = "[]";
-  std::string jws;
-  base::Base64UrlEncode(header, base::Base64UrlEncodePolicy::OMIT_PADDING,
-                        &jws);
-  jws += "..";
-  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
-}
-
-// Test failure when the JWS header isn't JSON.
-TEST(ParseTlsCertificateBinding, JwsHeaderNotJson) {
-  std::string header = "AAA";
-  std::string jws;
-  base::Base64UrlEncode(header, base::Base64UrlEncodePolicy::OMIT_PADDING,
-                        &jws);
-  jws += "..";
-  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
-}
-
-// Test failure when the JWS header isn't valid base64url.
-TEST(ParseTlsCertificateBinding, JwsHeaderNotBase64) {
-  // the header is encoded as "A", which is too short to be base64url.
-  std::string jws = "A..";
-  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
-}
-
-// Test failure when the JWS payload is non-empty.
-TEST(ParseTlsCertificateBinding, JwsPayloadNonEmpty) {
-  std::string header_string;
-  EXPECT_TRUE(JSONStringValueSerializer(&header_string)
-                  .Serialize(MinimalBindingHeader()));
-  std::string header_b64;
-  base::Base64UrlEncode(header_string,
-                        base::Base64UrlEncodePolicy::OMIT_PADDING, &header_b64);
-  // Make a JWS consisting of a valid header, a payload (base64url-encoded as
-  // "AAAA") and an empty signature.
-  std::string jws = header_b64 + ".AAAA.";
-  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
-}
-
-// Test failure when the JWS signature is not valid base64url.
-TEST(ParseTlsCertificateBinding, JwsSignatureNotBase64) {
-  std::string header_string;
-  EXPECT_TRUE(JSONStringValueSerializer(&header_string)
-                  .Serialize(MinimalBindingHeader()));
-  std::string header_b64;
-  base::Base64UrlEncode(header_string,
-                        base::Base64UrlEncodePolicy::OMIT_PADDING, &header_b64);
-  std::string jws = header_b64 + "..A";
-  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
-}
-
-// Test failure when the JWS consists of 2 components instead of 3.
-TEST(ParseTlsCertificateBinding, JwsHas2Components) {
-  std::string header_string;
-  EXPECT_TRUE(JSONStringValueSerializer(&header_string)
-                  .Serialize(MinimalBindingHeader()));
-  std::string header_b64;
-  base::Base64UrlEncode(header_string,
-                        base::Base64UrlEncodePolicy::OMIT_PADDING, &header_b64);
-  std::string jws = header_b64 + ".AAAA";
-  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
-}
-
-// Test failure when the JWS consists of 4 components instead of 3.
-TEST(ParseTlsCertificateBinding, JwsHas4Components) {
-  std::string header_string;
-  EXPECT_TRUE(JSONStringValueSerializer(&header_string)
-                  .Serialize(MinimalBindingHeader()));
-  std::string header_b64;
-  base::Base64UrlEncode(header_string,
-                        base::Base64UrlEncodePolicy::OMIT_PADDING, &header_b64);
-  std::string jws = header_b64 + "..AAAA.AAAA";
-  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
-}
-
-TEST(ParseTlsCertificateBinding, InvalidFields) {
-  const struct {
-    std::string header_key;
-    base::Value value;
-  } kTests[] = {
-      {
-          // "alg" expects a string
-          "alg",
-          base::Value(1),
-      },
-      {
-          // "alg" expects a non-empty string
-          "alg",
-          base::Value(""),
-      },
-      {
-          // "cty" expects a string
-          "cty",
-          base::Value(1),
-      },
-      {
-          // "cty" expects a specific value for its string
-          "cty",
-          base::Value("TLS-Certificate-Binding-v2"),
-      },
-      {
-          // "x5t#S256" expects a string
-          "x5t#S256",
-          base::Value(1),
-      },
-      {
-          // "x5c" expects a list
-          "x5c",
-          base::Value("wrong type"),
-      },
-      {
-          // "x5c" expects strings in its list
-          "x5c",
-          base::Value(base::ListValue().Append(1)),
-      },
-      {
-          // "x5c" expects base64 strings in its list. Test with a base64url
-          // (but not regular base64) string.
-          "x5c",
-          base::Value(base::ListValue().Append("M-_A")),
-      },
-      {
-          // "x5c" expects the base64 strings in its list to be valid X.509
-          // certificates. This string is valid base64, but is a (very)
-          // truncated X.509 certificate.
-          "x5c",
-          base::Value(base::ListValue().Append("MIID")),
-      },
-      {
-          // "iat" expects an int (when used for 2-QWACs). "iat" more generally
-          // (according to RFC 7519) can be a double, but we don't allow that,
-          // so explicitly check that doubles are rejected.
-          "iat",
-          base::Value(1.0),
-      },
-      {
-          // "exp" expects a numeric value
-          "exp",
-          base::Value("wrong type"),
-      },
-      {
-          // "crit", if present, can only contain "sigD"
-          "crit",
-          base::Value(base::ListValue().Append("sigD").Append("x5c")),
-      },
-      {
-          // "crit" expects a list
-          "crit",
-          base::Value("wrong type"),
-      },
-      {
-          // "sigD" expects an object
-          "sigD",
-          base::Value(base::ListValue()),
-      },
-      {
-          // The 2-QWAC TLS Certificate Binding JAdES profile only allows
-          // specific fields in the JWS header, and "x5u" is not one of them.
-          "x5u",
-          base::Value("X.509 URL"),
-      },
-  };
-
-  for (const auto& test : kTests) {
-    SCOPED_TRACE(test.header_key);
-    base::DictValue header = MinimalBindingHeader();
-    header.Set(test.header_key, test.value.Clone());
-    std::string jws = CreateTwoQwacCertBinding(header);
-    auto cert_binding = TwoQwacCertBinding::Parse(jws);
-    ASSERT_FALSE(cert_binding.has_value());
-  }
-}
-
-TEST(ParseTlsCertificateBinding, SigDHeaderParam) {
-  const struct {
-    std::string name;
-    base::RepeatingCallback<void(base::DictValue*)> header_func;
-    bool valid;
-  } kTests[] = {
-      {
-          "wrong mId",
-          base::BindRepeating([](base::DictValue* sig_d) {
-            sig_d->Set("mId", "http://uri.etsi.org/19182/ObjectIdByURI");
-          }),
-          false,
-      },
-      {
-          "wrong mId type",
-          base::BindRepeating(
-              [](base::DictValue* sig_d) { sig_d->Set("mId", 1); }),
-          false,
-      },
-      {
-          "wrong pars type",
-          base::BindRepeating(
-              [](base::DictValue* sig_d) { sig_d->Set("pars", 1); }),
-          false,
-      },
-      {
-          // This repeats the default value used in MinimalBindingHeader() in
-          // other tests, but is here for completeness.
-          "SHA-256 supported",
-          base::BindRepeating(
-              [](base::DictValue* sig_d) { sig_d->Set("hashM", "S256"); }),
-          true,
-      },
-      {
-          // TODO(crbug.com/392929826): Support SHA-384.
-          "SHA-384 not supported",
-          base::BindRepeating(
-              [](base::DictValue* sig_d) { sig_d->Set("hashM", "S384"); }),
-          false,
-      },
-      {
-          "SHA-512 supported",
-          base::BindRepeating(
-              [](base::DictValue* sig_d) { sig_d->Set("hashM", "S512"); }),
-          true,
-      },
-      {
-          "Other hashM values not supported",
-          base::BindRepeating(
-              [](base::DictValue* sig_d) { sig_d->Set("hashM", "SHA1"); }),
-          false,
-      },
-      {
-          "wrong hashM type",
-          base::BindRepeating(
-              [](base::DictValue* sig_d) { sig_d->Set("hashM", 1); }),
-          false,
-      },
-      {
-          "wrong type in pars list",
-          base::BindRepeating([](base::DictValue* sig_d) {
-            // "pars" and "hashV" must have the same length.
-            sig_d->Set("pars", base::ListValue().Append(1));
-            sig_d->Set("hashV", base::ListValue().Append("fakehash"));
-          }),
-          false,
-      },
-      {
-          "disallowed base64 padding in hashV",
-          base::BindRepeating([](base::DictValue* sig_d) {
-            // "ctys" must have the same length as "pars" and "hashV".
-            sig_d->Set("pars", base::ListValue().Append("URL"));
-            // hashV list elements are base64url encoded with no padding
-            sig_d->Set("hashV", base::ListValue().Append("fakehashAA=="));
-          }),
-          false,
-      },
-      {
-          "bad base64url encoding in hashV",
-          base::BindRepeating([](base::DictValue* sig_d) {
-            // "ctys" must have the same length as "pars" and "hashV".
-            sig_d->Set("pars", base::ListValue().Append("URL"));
-            // a base64url input (with no padding) is malformed if its length
-            // mod 4 is 1.
-            sig_d->Set("hashV", base::ListValue().Append("fakehash1"));
-          }),
-          false,
-      },
-      {
-          "wrong type in hashV list",
-          base::BindRepeating([](base::DictValue* sig_d) {
-            // "ctys" must have the same length as "pars" and "hashV".
-            sig_d->Set("pars", base::ListValue().Append("URL"));
-            sig_d->Set("hashV", base::ListValue().Append(1));
-          }),
-          false,
-      },
-      {
-          "wrong hashV type",
-          base::BindRepeating(
-              [](base::DictValue* sig_d) { sig_d->Set("hashV", 1); }),
-          false,
-      },
-      {
-          "correct ctys type",
-          base::BindRepeating([](base::DictValue* sig_d) {
-            // "ctys" must have the same length as "pars" and "hashV".
-            sig_d->Set("pars", base::ListValue().Append("URL"));
-            sig_d->Set("hashV", base::ListValue().Append("fakehash"));
-            sig_d->Set("ctys", base::ListValue().Append("content type"));
-          }),
-          true,
-      },
-      {
-          "wrong ctys type",
-          base::BindRepeating(
-              [](base::DictValue* sig_d) { sig_d->Set("ctys", "wrong type"); }),
-          false,
-      },
-      {
-          "wrong type inside ctys list",
-          base::BindRepeating([](base::DictValue* sig_d) {
-            // "ctys" must have the same length as "pars" and "hashV".
-            sig_d->Set("pars", base::ListValue().Append("URL"));
-            sig_d->Set("hashV", base::ListValue().Append("fakehash"));
-            sig_d->Set("ctys", base::ListValue().Append(1));
-          }),
-          false,
-      },
-      {
-          "pars length mismatch",
-          base::BindRepeating([](base::DictValue* sig_d) {
-            // "ctys" must have the same length as "pars" and "hashV".
-            sig_d->Set("pars", base::ListValue().Append("URL").Append("URL 2"));
-            sig_d->Set("hashV", base::ListValue().Append("fakehash"));
-            sig_d->Set("ctys", base::ListValue().Append("content type"));
-          }),
-          false,
-      },
-      {
-          "hashV length mismatch",
-          base::BindRepeating([](base::DictValue* sig_d) {
-            // "ctys" must have the same length as "pars" and "hashV".
-            sig_d->Set("pars", base::ListValue().Append("URL"));
-            sig_d->Set("hashV",
-                       base::ListValue().Append("fakehash").Append("hashfake"));
-            sig_d->Set("ctys", base::ListValue().Append("content type"));
-          }),
-          false,
-      },
-      {
-          "ctys length mismatch",
-          base::BindRepeating([](base::DictValue* sig_d) {
-            // "ctys" must have the same length as "pars" and "hashV".
-            sig_d->Set("pars", base::ListValue().Append("URL"));
-            sig_d->Set("hashV", base::ListValue().Append("fakehash"));
-            sig_d->Set("ctys", base::ListValue()
-                                   .Append("content type")
-                                   .Append("content type"));
-          }),
-          false,
-      },
-      {
-          "unknown member in sigD",
-          base::BindRepeating(
-              [](base::DictValue* sig_d) { sig_d->Set("spURI", "URL"); }),
-          false,
-      },
-  };
-
-  for (const auto& test : kTests) {
-    SCOPED_TRACE(test.name);
-    base::DictValue header = MinimalBindingHeader();
-    test.header_func.Run(header.FindDict("sigD"));
-    std::string jws = CreateTwoQwacCertBinding(header);
-    auto cert_binding = TwoQwacCertBinding::Parse(jws);
-    EXPECT_EQ(cert_binding.has_value(), test.valid);
-  }
-}
-
 }  // namespace
 }  // namespace net
diff --git a/net/cert/two_qwac.cc b/net/cert/two_qwac.cc
new file mode 100644
index 0000000..ed30cc8
--- /dev/null
+++ b/net/cert/two_qwac.cc
@@ -0,0 +1,344 @@
+// 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 "net/cert/two_qwac.h"
+
+#include "base/base64.h"
+#include "base/base64url.h"
+#include "base/json/json_reader.h"
+#include "base/strings/string_split.h"
+#include "net/cert/x509_util.h"
+
+namespace net {
+
+Jades2QwacHeader::Jades2QwacHeader() = default;
+Jades2QwacHeader::Jades2QwacHeader(const Jades2QwacHeader& other) = default;
+Jades2QwacHeader::Jades2QwacHeader(Jades2QwacHeader&& other) = default;
+Jades2QwacHeader::~Jades2QwacHeader() = default;
+
+namespace {
+
+std::optional<Jades2QwacHeader> ParseJades2QwacHeader(
+    std::string_view header_string) {
+  Jades2QwacHeader parsed_header;
+  // The header of a JWS is a JSON-encoded object (RFC 7515, section 4).
+  std::optional<base::Value> header_value =
+      base::JSONReader::Read(header_string, base::JSON_PARSE_RFC);
+  if (!header_value.has_value() || !header_value->is_dict()) {
+    return std::nullopt;
+  }
+  base::Value::Dict& header = header_value->GetDict();
+
+  // "alg" (Algorithm) parameter - RFC 7515, section 4.1.1
+  std::string* alg = header.FindString("alg");
+  if (!alg || *alg == "") {
+    return std::nullopt;
+  }
+  parsed_header.sig_alg = *alg;
+  header.Remove("alg");
+  // TODO(crbug.com/392929826): process alg (check that it matches the alg in
+  // x5c).
+
+  // "kid" (Key ID) parameter - RFC 7515, section 4.1.4
+  //
+  // The Key ID can be of any type and is used to identify the key used for
+  // signing. In this profile, the key used to verify the signature will be
+  // found in the "x5c" parameter, so the "kid" is useless to us and is ignored.
+  header.Remove("kid");
+
+  // "cty" (Content Type) parameter - RFC 7515, section 4.1.10
+  //
+  // ETSI TS 119 411-5 V2.1.1 requires the "cty" parameter to be
+  // "TLS-Certificate-Binding-v1".
+  std::string* cty = header.FindString("cty");
+  if (!cty || *cty != "TLS-Certificate-Binding-v1") {
+    return std::nullopt;
+  }
+  header.Remove("cty");
+
+  // "x5t#S256" (X.509 Certificate SHA-256 Thumbprint) parameter (RFC 7515,
+  // section 4.1.8) is the base64url-encoded SHA-256 thumbprint of the
+  // DER encoding of the X.509 certificate used to sign the JWS. This value is
+  // not needed to verify the signature (the leaf cert of the "x5c" parameter is
+  // the signing cert), and it is optional according to RFC 7515, so we ignore
+  // it.
+  if (header.FindString("x5t#S256")) {
+    header.Remove("x5t#S256");
+  }
+
+  // "x5c" (X.509 Certificate Chain) header - RFC 7515 section 4.1.6
+  base::ListValue* x5c_list = header.FindList("x5c");
+  if (!x5c_list) {
+    return std::nullopt;
+  }
+
+  size_t i = 0;
+  bssl::UniquePtr<CRYPTO_BUFFER> leaf;
+  std::vector<bssl::UniquePtr<CRYPTO_BUFFER>> intermediates;
+  for (const base::Value& cert_value : *x5c_list) {
+    // RFC 7515 section 4.1.6:
+    // "Each string in the array is a base64-encoded (not base64url-encoded) DER
+    // PKIX certificate value."
+    if (!cert_value.is_string()) {
+      return std::nullopt;
+    }
+    auto cert_bytes = base::Base64Decode(cert_value.GetString());
+    if (!cert_bytes.has_value()) {
+      return std::nullopt;
+    }
+    auto buf = x509_util::CreateCryptoBuffer(*cert_bytes);
+    if (i == 0) {
+      leaf = std::move(buf);
+    } else {
+      intermediates.emplace_back(std::move(buf));
+    }
+    i++;
+  }
+  parsed_header.two_qwac_cert = X509Certificate::CreateFromBuffer(
+      std::move(leaf), std::move(intermediates));
+  if (!parsed_header.two_qwac_cert) {
+    return std::nullopt;
+  }
+  header.Remove("x5c");
+
+  // "iat" header. TS 119 182-1 section 5.1.11 defines this header parameter to
+  // be almost the same as RFC 7519's JWT "iat" claim. Despite TS 119 182-1
+  // citing RFC 7519 as the definition for this header parameter, JWS header
+  // parameters and JWT claims are not the same thing. In any case, ETSI defines
+  // this header to be an integer representing the claimed signing time.
+  //
+  // I see no indication in TS 119 411-5 that "iat" is required to be present,
+  // and RFC 7519 specifies it as optional. Further, I haven't yet found an
+  // indication as to how one would interpret and apply this field in signature
+  // validation, so I'm ignoring it.
+  if (header.FindInt("iat")) {
+    header.Remove("iat");
+  }
+
+  // "exp" header. TS 119 411-5 Annex B defines this as the expiry date of the
+  // binding, and like TS 119 182-1 for the "iat" header, incorrectly cites RFC
+  // 7519's claim definition of the field (section 4.1.4). Unlike the ETSI
+  // specification for "iat" that restricts its NumericDate type to an integer,
+  // we only have the RFC 7519 definition of "exp" to use, which defines
+  // NumericDate as a JSON numeric value. RFC 7159 allows JSON numeric values to
+  // contain a fraction part.
+  //
+  // Like the "iat" header, TS 119 411-5 does not require the presence of "exp",
+  // RFC 7519 specifies it as optional, and there is no indication in any ETSI
+  // spec on how this field would affect signature validation, so it is ignored.
+  if (header.FindDouble("exp")) {
+    header.Remove("exp");
+  }
+
+  // "sigD" header - ETSI TS 119 182-1 section 5.2.8, with additional
+  // requirements specified in ETSI TS 119 411-5 Annex B. This parameter is a
+  // JSON object and is required to be present.
+  base::DictValue* sig_d = header.FindDict("sigD");
+  if (!sig_d) {
+    return std::nullopt;
+  }
+
+  // The sigD header must have a "mId" (mechanism ID) of
+  // "http://uri.etsi.org/19182/ObjectIdByURIHash". (ETSI TS 119 411-5 Annex B.)
+  std::string* m_id = sig_d->FindString("mId");
+  if (!m_id || *m_id != "http://uri.etsi.org/19182/ObjectIdByURIHash") {
+    return std::nullopt;
+  }
+  sig_d->Remove("mId");
+
+  // The sigD header must have a "pars" member, which is a list of strings. We
+  // don't care about the contents of this list, but its size must match that of
+  // "hashV". (ETSI 119 182-1 clause 5.2.8.)
+  const base::ListValue* pars = sig_d->FindList("pars");
+  if (!pars) {
+    return std::nullopt;
+  }
+  size_t bound_cert_count = pars->size();
+  for (const base::Value& par : *pars) {
+    if (!par.is_string()) {
+      return std::nullopt;
+    }
+  }
+  sig_d->Remove("pars");
+
+  // The sigD header must have a "hashM" member (TS 119 182-1
+  // section 5.2.8.3.3), which is a string identifying the hashing algorithm
+  // used for the "hashV" member. ETSI TS 119 411-5 only requires that S256,
+  // S384, and S512 be supported.
+  std::string* hash_m = sig_d->FindString("hashM");
+  if (!hash_m) {
+    return std::nullopt;
+  }
+  if (*hash_m == "S256") {
+    parsed_header.hash_alg = crypto::hash::kSha256;
+  } else if (*hash_m == "S384") {
+    // TODO(crbug.com/392929826): add SHA-384 to crypto/hash.h.
+    return std::nullopt;
+  } else if (*hash_m == "S512") {
+    parsed_header.hash_alg = crypto::hash::kSha512;
+  } else {
+    // Unsupported hashing algorithm.
+    return std::nullopt;
+  }
+  sig_d->Remove("hashM");
+
+  // The sigD header must have a "hashV" member, which is a list of
+  // base64url-encoded digest values of the base64url-encoded data objects.
+  // (ETSI TS 119 182-1 clause 5.2.8. The "b64" header parameter is absent, so
+  // the digest is computed over the base64url-encoded data object instead of
+  // computed directly over the data object.)
+  const base::ListValue* hash_v = sig_d->FindList("hashV");
+  if (!hash_v) {
+    return std::nullopt;
+  }
+  if (hash_v->size() != bound_cert_count) {
+    return std::nullopt;
+  }
+  parsed_header.bound_cert_hashes.reserve(bound_cert_count);
+  for (const base::Value& hash_value : *hash_v) {
+    const std::string* hash_b64url = hash_value.GetIfString();
+    if (!hash_b64url) {
+      return std::nullopt;
+    }
+    // ETSI TS 119 182-1 fails to specify the definition of "base64url-encoded".
+    // Given that other uses of base64url encoding come from the JWS spec, and
+    // JWS disallows padding in its base64url encoding, we disallow it here as
+    // well.
+    auto hash = base::Base64UrlDecode(
+        *hash_b64url, base::Base64UrlDecodePolicy::DISALLOW_PADDING);
+    if (!hash.has_value()) {
+      return std::nullopt;
+    }
+    parsed_header.bound_cert_hashes.emplace_back(std::move(*hash));
+  }
+  sig_d->Remove("hashV");
+
+  // Given the mId used, the sigD header may have a "ctys" member (TS 119 182-1
+  // clause 5.2.8.3.3), with semantics and syntax as specified in clause
+  // 5.2.8.1. Clause 5.2.8.1 defines the "ctys" member's syntax to be an array
+  // of strings. This array has the same length as the "pars" (and "hashV")
+  // array, and each element is the content type (RFC 7515 section 4.1.10) of
+  // the data object referred to by the value in "pars" at the same index.
+  // RFC 7515 specifies that the content type parameter is ignored by JWS
+  // implementations and processing of it is performed by the JWS application.
+  // Since neither ETSI TS 119 182-1 nor TS 119 411-5 provide guidance on the
+  // content type used for the individual data objects, this implementation has
+  // no opinion on the stated content types.
+  const base::ListValue* ctys = sig_d->FindList("ctys");
+  if (ctys) {
+    if (ctys->size() != bound_cert_count) {
+      return std::nullopt;
+    }
+    for (const base::Value& cty_value : *ctys) {
+      if (!cty_value.is_string()) {
+        return std::nullopt;
+      }
+    }
+  } else if (sig_d->contains("ctys")) {
+    // check that there isn't a "ctys" of the wrong type
+    return std::nullopt;
+  }
+  sig_d->Remove("ctys");
+
+  // sigD has no other members than the aforementioned "mId", "pars", "hashM",
+  // "hashV", and "ctys". (ETSI TS 119 182-1 clause 5.2.8.)
+  if (!sig_d->empty()) {
+    return std::nullopt;
+  }
+  header.Remove("sigD");
+
+  // The header must not contain fields other than "alg", "kid", "cty",
+  // "x5t#S256", "x5c", "iat", "exp", or "sigD", as required by ETSI TS 119
+  // 411-5 V2.1.1, Annex B.
+  //
+  // ETSI TS 119 182-1 V1.2.1 section 5.1.9 specifies that if the "sigD" header
+  // parameter is present, then the "crit" header parameter shall also be
+  // present with "sigD" as one of its array elements. This is in conflict with
+  // the requirement in 119 411-5 V2.1.1 Annex B. To resolve this conflict, this
+  // implementation will allow the presence of "crit", but if it is present, it
+  // must be an array containing exactly "sigD".
+  const auto* crit_value = header.Find("crit");
+  if (crit_value) {
+    if (!crit_value->is_list()) {
+      return std::nullopt;
+    }
+    const auto& crit_list = crit_value->GetList();
+    if (crit_list.size() != 1 || !crit_list.contains("sigD")) {
+      return std::nullopt;
+    }
+  }
+  header.Remove("crit");
+
+  if (!header.empty()) {
+    return std::nullopt;
+  }
+
+  return parsed_header;
+}
+
+}  // namespace
+
+TwoQwacCertBinding::TwoQwacCertBinding(Jades2QwacHeader header,
+                                       std::string header_string,
+                                       std::vector<uint8_t> signature)
+    : header(header), header_string(header_string), signature(signature) {}
+
+TwoQwacCertBinding::TwoQwacCertBinding(const TwoQwacCertBinding& other) =
+    default;
+TwoQwacCertBinding::TwoQwacCertBinding(TwoQwacCertBinding&& other) = default;
+TwoQwacCertBinding::~TwoQwacCertBinding() = default;
+
+std::optional<TwoQwacCertBinding> TwoQwacCertBinding::Parse(
+    std::string_view jws) {
+  // ETSI TS 119 411-5 V2.1.1 Annex B: The JAdES signatures shall be serialized
+  // using JWS Compact Serialization as specified in IETF RFC 7515.
+  //
+  // The JWS Compact Serialization format consists of 3 components separated by
+  // a dot (".") (RFC 7515, section 7.1).
+  std::vector<std::string_view> jws_components = base::SplitStringPiece(
+      jws, ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
+  if (jws_components.size() != 3) {
+    // Reject a JWS that does not consist of 3 components.
+    return std::nullopt;
+  }
+  std::string_view header_b64 = jws_components[0];
+  std::string_view payload_b64 = jws_components[1];
+  std::string_view signature_b64 = jws_components[2];
+
+  // The 3 components of a JWS are the header, the payload, and the signature.
+  // The components are base64url encoded (RFC 7515, section 7.1) and the base64
+  // encoding is without any padding "=" characters (Ibid., section 2).
+  std::string header_string;
+  if (!base::Base64UrlDecode(header_b64,
+                             base::Base64UrlDecodePolicy::DISALLOW_PADDING,
+                             &header_string)) {
+    return std::nullopt;
+  }
+  std::optional<std::vector<uint8_t>> signature = base::Base64UrlDecode(
+      signature_b64, base::Base64UrlDecodePolicy::DISALLOW_PADDING);
+  if (!signature.has_value()) {
+    return std::nullopt;
+  }
+
+  // Parse the JWS/JAdES header.
+  auto header = ParseJades2QwacHeader(header_string);
+  if (!header.has_value()) {
+    return std::nullopt;
+  }
+
+  // ETSI TS 119 411-5 V2.1.1 Annex B specifies a "sigD" header parameter. This
+  // header parameter is defined in ETSI TS 119 182-1 V1.2.1, section 5.2.8,
+  // which states "The sigD header parameter shall not appear in JAdES
+  // signatures whose JWS Payload is attached". Thus, it can be inferred that
+  // the JWS Payload is detached. A detached payload for a JWS means that the
+  // encoded payload is empty (RFC 7515, Appendix F).
+  if (!payload_b64.empty()) {
+    return std::nullopt;
+  }
+
+  TwoQwacCertBinding cert_binding(*header, header_string, *signature);
+  return cert_binding;
+}
+
+}  // namespace net
diff --git a/net/cert/two_qwac.h b/net/cert/two_qwac.h
new file mode 100644
index 0000000..8aa29b73
--- /dev/null
+++ b/net/cert/two_qwac.h
@@ -0,0 +1,85 @@
+// 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.
+
+#ifndef NET_CERT_TWO_QWAC_H_
+#define NET_CERT_TWO_QWAC_H_
+
+#include <stdint.h>
+
+#include <optional>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "crypto/hash.h"
+#include "net/base/net_export.h"
+#include "net/cert/x509_certificate.h"
+
+namespace net {
+
+// Contains fields from a JAdES (ETSI TS 119 182-1) signature header needed for
+// verifying 2-QWAC TLS certificate bindings. While JAdES is a profile of JWS
+// (RFC 7515), this is not general-purpose JWS or JWT code. It is also not
+// general-purpose JAdES code, as only fields needed for 2-QWAC TLS certificate
+// bindings are present here.
+struct NET_EXPORT_PRIVATE Jades2QwacHeader {
+  Jades2QwacHeader();
+  Jades2QwacHeader(const Jades2QwacHeader& other);
+  Jades2QwacHeader(Jades2QwacHeader&& other);
+  ~Jades2QwacHeader();
+
+  // The signature algorithm used to sign the JWS, as provided by the "alg" JWS
+  // Header Parameter (RFC 7515, section 4.1.1). Valid values for this field can
+  // be found in the JSON Web Signature and Encryption Algorithms IANA registry
+  // (https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms).
+  // The consumer of this struct must check that the algorithm provided in this
+  // field matches the signature algorithm of the leaf cert in |two_qwac_cert|.
+  std::string sig_alg;
+
+  // The certificate chain with a leaf cert that is a 2-QWAC. This certificate
+  // chain is used to sign the JWS, which binds the 2-QWAC to a set of TLS
+  // serverAuth certificates.
+  scoped_refptr<net::X509Certificate> two_qwac_cert;
+
+  // The hash algorithm used to hash the bound certificates.
+  crypto::hash::HashKind hash_alg;
+
+  // The hashes of the bound certificates (base64url-encoded), hashed using
+  // |hash_alg|. Note: this is Digest(base64url(cert)), because that's what the
+  // JAdES and 2-QWAC specs require (not that it makes any sense to do that).
+  std::vector<std::vector<uint8_t>> bound_cert_hashes;
+};
+
+// A TwoQwacCertBinding represents a JAdES Signature (ETSI TS 119 182-1,
+// clause 3.1) used for 2-QWACs (ETSI TS 119 411-5, clause 6.2.2). It comes from
+// a TLS Certificate Binding (ETSI TS 119 411-5 annex B). Note that a JAdES
+// Signature (which is also a JWS, a.k.a. JSON Web Signature) consists of a
+// header and a cryptographic signature, not just a signature.
+struct NET_EXPORT_PRIVATE TwoQwacCertBinding {
+  TwoQwacCertBinding(Jades2QwacHeader header,
+                     std::string header_string,
+                     std::vector<uint8_t> signature);
+  TwoQwacCertBinding(const TwoQwacCertBinding& other);
+  TwoQwacCertBinding(TwoQwacCertBinding&& other);
+  ~TwoQwacCertBinding();
+
+  // Parses a TLS Certificate Binding structure that contains a 2-QWAC
+  // certificate chain.
+  // TODO(crbug.com/392929826): Add a fuzz test for this function.
+  static std::optional<TwoQwacCertBinding> Parse(std::string_view jws);
+
+  // The parsed JWS Header from the certificate binding structure.
+  Jades2QwacHeader header;
+
+  // The unparsed JWS Header, needed for verifying the signature.
+  std::string header_string;
+
+  // The JWS Signature (RFC 7515 section 2)/JAdES Signature Value (ETSI TS 119
+  // 182-1 clause 3.1) from the certificate binding structure.
+  std::vector<uint8_t> signature;
+};
+
+}  // namespace net
+
+#endif  // NET_CERT_TWO_QWAC_H_
diff --git a/net/cert/two_qwac_unittest.cc b/net/cert/two_qwac_unittest.cc
new file mode 100644
index 0000000..d8306fb1
--- /dev/null
+++ b/net/cert/two_qwac_unittest.cc
@@ -0,0 +1,449 @@
+// 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 "net/cert/two_qwac.h"
+
+#include <stdint.h>
+
+#include "base/base64.h"
+#include "base/base64url.h"
+#include "base/functional/callback.h"
+#include "base/json/json_string_value_serializer.h"
+#include "base/values.h"
+#include "net/test/cert_builder.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace net {
+namespace {
+
+// Builds a header that has the minimal required set of parameters
+base::DictValue MinimalBindingHeader() {
+  auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
+  base::Value::Dict header =
+      base::Value::Dict()
+          .Set("alg", "test alg")
+          .Set("cty", "TLS-Certificate-Binding-v1")
+          .Set("x5c", base::Value::List()
+                          // These are base64 encoded, not base64url encoded
+                          .Append(base::Base64Encode(leaf->GetDER()))
+                          .Append(base::Base64Encode(root->GetDER())))
+          .Set("sigD",
+               base::Value::Dict()
+                   .Set("mId", "http://uri.etsi.org/19182/ObjectIdByURIHash")
+                   .Set("pars", base::Value::List().Append("").Append(""))
+                   .Set("hashM", "S256")
+                   // These are hashes of the certs that this
+                   // TlsCertificateBinding binds, not hashes of the certs in
+                   // the x5c cert chain.
+                   .Set("hashV", base::Value::List()
+                                     .Append("fakehash1A")
+                                     .Append("fakehash2A")));
+  return header;
+}
+
+// Creates a TLS Certificate Binding from the provided header. This test helper
+// leaves the signature empty.
+std::string CreateTwoQwacCertBinding(const base::DictValue& header) {
+  std::string header_string;
+  EXPECT_TRUE(JSONStringValueSerializer(&header_string).Serialize(header));
+  // Create the JWS from the header.
+  std::string jws;
+  base::Base64UrlEncode(header_string,
+                        base::Base64UrlEncodePolicy::OMIT_PADDING, &jws);
+  // Add empty payload and signature to JWS.
+  jws += "..";
+  return jws;
+}
+
+TEST(ParseTlsCertificateBinding, MinimalValidBinding) {
+  // TODO(crbug.com/392929826): Once we start validating signatures, these
+  // tests will probably need to be updated to have real algorithms,
+  // certificates, and signatures. (This is assuming that some basic checks are
+  // added to the parsing code, e.g. that we can parse certs into a
+  // net::X509Certificate and check that the "alg" matches the leaf cert.)
+
+  // Build a header that has the minimally required set of parameters
+  base::DictValue header = MinimalBindingHeader();
+  std::string jws = CreateTwoQwacCertBinding(header);
+  auto cert_binding = TwoQwacCertBinding::Parse(jws);
+  ASSERT_TRUE(cert_binding.has_value());
+}
+
+TEST(ParseTlsCertificateBinding, MaximalValidBinding) {
+  // Create a header that has all allowed fields
+  base::DictValue header = MinimalBindingHeader();
+  header.Set("kid", base::Value::Dict()
+                        .Set("random key", "random value")
+                        .Set("kids can have", "whatever they want"));
+  header.Set("x5t#S256", "base64urlhashA");
+  header.Set("iat", 12345);
+  header.Set("exp", 67.89);
+  header.Set("crit", base::ListValue().Append("sigD"));
+
+  header.FindDict("sigD")->Set(
+      "ctys",
+      base::Value::List().Append("content-type1").Append("content-type2"));
+
+  std::string jws = CreateTwoQwacCertBinding(header);
+  auto cert_binding = TwoQwacCertBinding::Parse(jws);
+  ASSERT_TRUE(cert_binding.has_value());
+}
+
+// Test failure when the JWS header isn't a JSON object.
+TEST(ParseTlsCertificateBinding, JwsHeaderNotObject) {
+  std::string header = "[]";
+  std::string jws;
+  base::Base64UrlEncode(header, base::Base64UrlEncodePolicy::OMIT_PADDING,
+                        &jws);
+  jws += "..";
+  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
+}
+
+// Test failure when the JWS header isn't JSON.
+TEST(ParseTlsCertificateBinding, JwsHeaderNotJson) {
+  std::string header = "AAA";
+  std::string jws;
+  base::Base64UrlEncode(header, base::Base64UrlEncodePolicy::OMIT_PADDING,
+                        &jws);
+  jws += "..";
+  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
+}
+
+// Test failure when the JWS header isn't valid base64url.
+TEST(ParseTlsCertificateBinding, JwsHeaderNotBase64) {
+  // the header is encoded as "A", which is too short to be base64url.
+  std::string jws = "A..";
+  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
+}
+
+// Test failure when the JWS payload is non-empty.
+TEST(ParseTlsCertificateBinding, JwsPayloadNonEmpty) {
+  std::string header_string;
+  EXPECT_TRUE(JSONStringValueSerializer(&header_string)
+                  .Serialize(MinimalBindingHeader()));
+  std::string header_b64;
+  base::Base64UrlEncode(header_string,
+                        base::Base64UrlEncodePolicy::OMIT_PADDING, &header_b64);
+  // Make a JWS consisting of a valid header, a payload (base64url-encoded as
+  // "AAAA") and an empty signature.
+  std::string jws = header_b64 + ".AAAA.";
+  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
+}
+
+// Test failure when the JWS signature is not valid base64url.
+TEST(ParseTlsCertificateBinding, JwsSignatureNotBase64) {
+  std::string header_string;
+  EXPECT_TRUE(JSONStringValueSerializer(&header_string)
+                  .Serialize(MinimalBindingHeader()));
+  std::string header_b64;
+  base::Base64UrlEncode(header_string,
+                        base::Base64UrlEncodePolicy::OMIT_PADDING, &header_b64);
+  std::string jws = header_b64 + "..A";
+  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
+}
+
+// Test failure when the JWS consists of 2 components instead of 3.
+TEST(ParseTlsCertificateBinding, JwsHas2Components) {
+  std::string header_string;
+  EXPECT_TRUE(JSONStringValueSerializer(&header_string)
+                  .Serialize(MinimalBindingHeader()));
+  std::string header_b64;
+  base::Base64UrlEncode(header_string,
+                        base::Base64UrlEncodePolicy::OMIT_PADDING, &header_b64);
+  std::string jws = header_b64 + ".AAAA";
+  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
+}
+
+// Test failure when the JWS consists of 4 components instead of 3.
+TEST(ParseTlsCertificateBinding, JwsHas4Components) {
+  std::string header_string;
+  EXPECT_TRUE(JSONStringValueSerializer(&header_string)
+                  .Serialize(MinimalBindingHeader()));
+  std::string header_b64;
+  base::Base64UrlEncode(header_string,
+                        base::Base64UrlEncodePolicy::OMIT_PADDING, &header_b64);
+  std::string jws = header_b64 + "..AAAA.AAAA";
+  EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
+}
+
+TEST(ParseTlsCertificateBinding, InvalidFields) {
+  const struct {
+    std::string header_key;
+    base::Value value;
+  } kTests[] = {
+      {
+          // "alg" expects a string
+          "alg",
+          base::Value(1),
+      },
+      {
+          // "alg" expects a non-empty string
+          "alg",
+          base::Value(""),
+      },
+      {
+          // "cty" expects a string
+          "cty",
+          base::Value(1),
+      },
+      {
+          // "cty" expects a specific value for its string
+          "cty",
+          base::Value("TLS-Certificate-Binding-v2"),
+      },
+      {
+          // "x5t#S256" expects a string
+          "x5t#S256",
+          base::Value(1),
+      },
+      {
+          // "x5c" expects a list
+          "x5c",
+          base::Value("wrong type"),
+      },
+      {
+          // "x5c" expects strings in its list
+          "x5c",
+          base::Value(base::ListValue().Append(1)),
+      },
+      {
+          // "x5c" expects base64 strings in its list. Test with a base64url
+          // (but not regular base64) string.
+          "x5c",
+          base::Value(base::ListValue().Append("M-_A")),
+      },
+      {
+          // "x5c" expects the base64 strings in its list to be valid X.509
+          // certificates. This string is valid base64, but is a (very)
+          // truncated X.509 certificate.
+          "x5c",
+          base::Value(base::ListValue().Append("MIID")),
+      },
+      {
+          // "iat" expects an int (when used for 2-QWACs). "iat" more generally
+          // (according to RFC 7519) can be a double, but we don't allow that,
+          // so explicitly check that doubles are rejected.
+          "iat",
+          base::Value(1.0),
+      },
+      {
+          // "exp" expects a numeric value
+          "exp",
+          base::Value("wrong type"),
+      },
+      {
+          // "crit", if present, can only contain "sigD"
+          "crit",
+          base::Value(base::ListValue().Append("sigD").Append("x5c")),
+      },
+      {
+          // "crit" expects a list
+          "crit",
+          base::Value("wrong type"),
+      },
+      {
+          // "sigD" expects an object
+          "sigD",
+          base::Value(base::ListValue()),
+      },
+      {
+          // The 2-QWAC TLS Certificate Binding JAdES profile only allows
+          // specific fields in the JWS header, and "x5u" is not one of them.
+          "x5u",
+          base::Value("X.509 URL"),
+      },
+  };
+
+  for (const auto& test : kTests) {
+    SCOPED_TRACE(test.header_key);
+    base::DictValue header = MinimalBindingHeader();
+    header.Set(test.header_key, test.value.Clone());
+    std::string jws = CreateTwoQwacCertBinding(header);
+    auto cert_binding = TwoQwacCertBinding::Parse(jws);
+    ASSERT_FALSE(cert_binding.has_value());
+  }
+}
+
+TEST(ParseTlsCertificateBinding, SigDHeaderParam) {
+  const struct {
+    std::string name;
+    base::RepeatingCallback<void(base::DictValue*)> header_func;
+    bool valid;
+  } kTests[] = {
+      {
+          "wrong mId",
+          base::BindRepeating([](base::DictValue* sig_d) {
+            sig_d->Set("mId", "http://uri.etsi.org/19182/ObjectIdByURI");
+          }),
+          false,
+      },
+      {
+          "wrong mId type",
+          base::BindRepeating(
+              [](base::DictValue* sig_d) { sig_d->Set("mId", 1); }),
+          false,
+      },
+      {
+          "wrong pars type",
+          base::BindRepeating(
+              [](base::DictValue* sig_d) { sig_d->Set("pars", 1); }),
+          false,
+      },
+      {
+          // This repeats the default value used in MinimalBindingHeader() in
+          // other tests, but is here for completeness.
+          "SHA-256 supported",
+          base::BindRepeating(
+              [](base::DictValue* sig_d) { sig_d->Set("hashM", "S256"); }),
+          true,
+      },
+      {
+          // TODO(crbug.com/392929826): Support SHA-384.
+          "SHA-384 not supported",
+          base::BindRepeating(
+              [](base::DictValue* sig_d) { sig_d->Set("hashM", "S384"); }),
+          false,
+      },
+      {
+          "SHA-512 supported",
+          base::BindRepeating(
+              [](base::DictValue* sig_d) { sig_d->Set("hashM", "S512"); }),
+          true,
+      },
+      {
+          "Other hashM values not supported",
+          base::BindRepeating(
+              [](base::DictValue* sig_d) { sig_d->Set("hashM", "SHA1"); }),
+          false,
+      },
+      {
+          "wrong hashM type",
+          base::BindRepeating(
+              [](base::DictValue* sig_d) { sig_d->Set("hashM", 1); }),
+          false,
+      },
+      {
+          "wrong type in pars list",
+          base::BindRepeating([](base::DictValue* sig_d) {
+            // "pars" and "hashV" must have the same length.
+            sig_d->Set("pars", base::ListValue().Append(1));
+            sig_d->Set("hashV", base::ListValue().Append("fakehash"));
+          }),
+          false,
+      },
+      {
+          "disallowed base64 padding in hashV",
+          base::BindRepeating([](base::DictValue* sig_d) {
+            // "ctys" must have the same length as "pars" and "hashV".
+            sig_d->Set("pars", base::ListValue().Append("URL"));
+            // hashV list elements are base64url encoded with no padding
+            sig_d->Set("hashV", base::ListValue().Append("fakehashAA=="));
+          }),
+          false,
+      },
+      {
+          "bad base64url encoding in hashV",
+          base::BindRepeating([](base::DictValue* sig_d) {
+            // "ctys" must have the same length as "pars" and "hashV".
+            sig_d->Set("pars", base::ListValue().Append("URL"));
+            // a base64url input (with no padding) is malformed if its length
+            // mod 4 is 1.
+            sig_d->Set("hashV", base::ListValue().Append("fakehash1"));
+          }),
+          false,
+      },
+      {
+          "wrong type in hashV list",
+          base::BindRepeating([](base::DictValue* sig_d) {
+            // "ctys" must have the same length as "pars" and "hashV".
+            sig_d->Set("pars", base::ListValue().Append("URL"));
+            sig_d->Set("hashV", base::ListValue().Append(1));
+          }),
+          false,
+      },
+      {
+          "wrong hashV type",
+          base::BindRepeating(
+              [](base::DictValue* sig_d) { sig_d->Set("hashV", 1); }),
+          false,
+      },
+      {
+          "correct ctys type",
+          base::BindRepeating([](base::DictValue* sig_d) {
+            // "ctys" must have the same length as "pars" and "hashV".
+            sig_d->Set("pars", base::ListValue().Append("URL"));
+            sig_d->Set("hashV", base::ListValue().Append("fakehash"));
+            sig_d->Set("ctys", base::ListValue().Append("content type"));
+          }),
+          true,
+      },
+      {
+          "wrong ctys type",
+          base::BindRepeating(
+              [](base::DictValue* sig_d) { sig_d->Set("ctys", "wrong type"); }),
+          false,
+      },
+      {
+          "wrong type inside ctys list",
+          base::BindRepeating([](base::DictValue* sig_d) {
+            // "ctys" must have the same length as "pars" and "hashV".
+            sig_d->Set("pars", base::ListValue().Append("URL"));
+            sig_d->Set("hashV", base::ListValue().Append("fakehash"));
+            sig_d->Set("ctys", base::ListValue().Append(1));
+          }),
+          false,
+      },
+      {
+          "pars length mismatch",
+          base::BindRepeating([](base::DictValue* sig_d) {
+            // "ctys" must have the same length as "pars" and "hashV".
+            sig_d->Set("pars", base::ListValue().Append("URL").Append("URL 2"));
+            sig_d->Set("hashV", base::ListValue().Append("fakehash"));
+            sig_d->Set("ctys", base::ListValue().Append("content type"));
+          }),
+          false,
+      },
+      {
+          "hashV length mismatch",
+          base::BindRepeating([](base::DictValue* sig_d) {
+            // "ctys" must have the same length as "pars" and "hashV".
+            sig_d->Set("pars", base::ListValue().Append("URL"));
+            sig_d->Set("hashV",
+                       base::ListValue().Append("fakehash").Append("hashfake"));
+            sig_d->Set("ctys", base::ListValue().Append("content type"));
+          }),
+          false,
+      },
+      {
+          "ctys length mismatch",
+          base::BindRepeating([](base::DictValue* sig_d) {
+            // "ctys" must have the same length as "pars" and "hashV".
+            sig_d->Set("pars", base::ListValue().Append("URL"));
+            sig_d->Set("hashV", base::ListValue().Append("fakehash"));
+            sig_d->Set("ctys", base::ListValue()
+                                   .Append("content type")
+                                   .Append("content type"));
+          }),
+          false,
+      },
+      {
+          "unknown member in sigD",
+          base::BindRepeating(
+              [](base::DictValue* sig_d) { sig_d->Set("spURI", "URL"); }),
+          false,
+      },
+  };
+
+  for (const auto& test : kTests) {
+    SCOPED_TRACE(test.name);
+    base::DictValue header = MinimalBindingHeader();
+    test.header_func.Run(header.FindDict("sigD"));
+    std::string jws = CreateTwoQwacCertBinding(header);
+    auto cert_binding = TwoQwacCertBinding::Parse(jws);
+    EXPECT_EQ(cert_binding.has_value(), test.valid);
+  }
+}
+
+}  // namespace
+}  // namespace net
diff --git a/services/on_device_model/BUILD.gn b/services/on_device_model/BUILD.gn
index 422a017e..b18ff29 100644
--- a/services/on_device_model/BUILD.gn
+++ b/services/on_device_model/BUILD.gn
@@ -42,8 +42,10 @@
   }
 
   if (is_linux || is_chromeos) {
+    deps += [ "//gpu/config" ]
+  }
+  if (!is_fuchsia) {
     deps += [
-      "//gpu/config",
       "//third_party/dawn/include/dawn:cpp_headers",
       "//third_party/dawn/src/dawn:proc",
       "//third_party/dawn/src/dawn/native",
diff --git a/services/on_device_model/pre_sandbox_init.cc b/services/on_device_model/pre_sandbox_init.cc
index 5f59497..cf00550 100644
--- a/services/on_device_model/pre_sandbox_init.cc
+++ b/services/on_device_model/pre_sandbox_init.cc
@@ -16,6 +16,9 @@
 
 #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
 #include "gpu/config/gpu_info_collector.h"                    // nogncheck
+#endif
+
+#if !BUILDFLAG(IS_FUCHSIA)
 #include "third_party/dawn/include/dawn/dawn_proc.h"          // nogncheck
 #include "third_party/dawn/include/dawn/native/DawnNative.h"  // nogncheck
 #include "third_party/dawn/include/dawn/webgpu_cpp.h"         // nogncheck
@@ -53,6 +56,20 @@
 }
 #endif
 
+#if !BUILDFLAG(IS_FUCHSIA)
+// If this feature is enabled, a WebGPU device is created for each valid
+// adapter. This makes sure any relevant drivers or other libs are loaded before
+// enabling the sandbox.
+BASE_FEATURE(kOnDeviceModelWarmDrivers,
+             "OnDeviceModelWarmDrivers",
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_WIN)
+             base::FEATURE_ENABLED_BY_DEFAULT
+#else
+             base::FEATURE_DISABLED_BY_DEFAULT
+#endif
+);
+#endif
+
 }  // namespace
 
 // static
@@ -74,31 +91,39 @@
   }
 #endif
 
-#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
-  // Warm any relevant drivers before attempting to bring up the sandbox. For
-  // good measure we initialize a device instance for any adapter with an
-  // appropriate backend on top of any integrated or discrete GPU.
-  dawnProcSetProcs(&dawn::native::GetProcs());
-  auto instance = std::make_unique<dawn::native::Instance>();
-  const wgpu::RequestAdapterOptions adapter_options{
-      .backendType = wgpu::BackendType::Vulkan,
-  };
-  std::vector<dawn::native::Adapter> adapters =
-      instance->EnumerateAdapters(&adapter_options);
-  for (auto& nativeAdapter : adapters) {
-    wgpu::Adapter adapter = wgpu::Adapter(nativeAdapter.Get());
-    wgpu::AdapterInfo info;
-    adapter.GetInfo(&info);
-    if (info.adapterType == wgpu::AdapterType::IntegratedGPU ||
-        info.adapterType == wgpu::AdapterType::DiscreteGPU) {
-      const wgpu::DeviceDescriptor descriptor;
-      wgpu::Device device{nativeAdapter.CreateDevice(&descriptor)};
-      if (device) {
-        device.Destroy();
+#if !BUILDFLAG(IS_FUCHSIA)
+  if (base::FeatureList::IsEnabled(kOnDeviceModelWarmDrivers)) {
+    // Warm any relevant drivers before attempting to bring up the sandbox. For
+    // good measure we initialize a device instance for any adapter with an
+    // appropriate backend on top of any integrated or discrete GPU.
+    dawnProcSetProcs(&dawn::native::GetProcs());
+    auto instance = std::make_unique<dawn::native::Instance>();
+    const wgpu::RequestAdapterOptions adapter_options{
+#if BUILDFLAG(IS_WIN)
+        .backendType = wgpu::BackendType::D3D12,
+#elif BUILDFLAG(IS_APPLE)
+        .backendType = wgpu::BackendType::Metal,
+#else
+        .backendType = wgpu::BackendType::Vulkan,
+#endif
+    };
+    std::vector<dawn::native::Adapter> adapters =
+        instance->EnumerateAdapters(&adapter_options);
+    for (auto& nativeAdapter : adapters) {
+      wgpu::Adapter adapter = wgpu::Adapter(nativeAdapter.Get());
+      wgpu::AdapterInfo info;
+      adapter.GetInfo(&info);
+      if (info.adapterType == wgpu::AdapterType::IntegratedGPU ||
+          info.adapterType == wgpu::AdapterType::DiscreteGPU) {
+        const wgpu::DeviceDescriptor descriptor;
+        wgpu::Device device{nativeAdapter.CreateDevice(&descriptor)};
+        if (device) {
+          device.Destroy();
+        }
       }
     }
   }
-#endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+#endif
   return true;
 }
 
diff --git a/services/video_effects/OWNERS b/services/video_effects/OWNERS
index 809d551..2523d33b 100644
--- a/services/video_effects/OWNERS
+++ b/services/video_effects/OWNERS
@@ -1,5 +1,4 @@
 # Primary reviewers:
-bialpio@chromium.org
 bryantchandler@chromium.org
 
 # Secondary reviewers (use if primary reviewer is unavailable):
diff --git a/sql/sandboxed_vfs.cc b/sql/sandboxed_vfs.cc
index 0e64d2d8..a53291f 100644
--- a/sql/sandboxed_vfs.cc
+++ b/sql/sandboxed_vfs.cc
@@ -125,7 +125,6 @@
       -kUnixEpochAsJulianDay * base::Time::kMillisecondsPerDay);
 }
 
-#if DCHECK_IS_ON()
 // `full_path_cstr` must be a filename argument passed to the VFS from SQLite.
 SandboxedVfsFileType VfsFileTypeFromPath(const char* full_path_cstr) {
   std::string_view full_path(full_path_cstr);
@@ -146,7 +145,6 @@
       << "Argument is not a file name buffer passed from SQLite to a VFS: "
       << full_path;
 }
-#endif  // DCHECK_IS_ON()
 
 }  // namespace
 
@@ -182,9 +180,7 @@
   }
 
   SandboxedVfsFile::Create(std::move(file), std::move(file_path),
-#if DCHECK_IS_ON()
                            VfsFileTypeFromPath(full_path),
-#endif  // DCHECK_IS_ON()
                            this, result_file);
   if (granted_flags)
     *granted_flags = requested_flags;
diff --git a/sql/sandboxed_vfs_file.cc b/sql/sandboxed_vfs_file.cc
index 6b9e534..e8a9981 100644
--- a/sql/sandboxed_vfs_file.cc
+++ b/sql/sandboxed_vfs_file.cc
@@ -137,18 +137,14 @@
 // static
 void SandboxedVfsFile::Create(base::File file,
                               base::FilePath file_path,
-#if DCHECK_IS_ON()
                               SandboxedVfsFileType file_type,
-#endif  // DCHECK_IS_ON()
                               SandboxedVfs* vfs,
                               sqlite3_file& buffer) {
   SandboxedVfsFileSqliteBridge& bridge =
       SandboxedVfsFileSqliteBridge::FromSqliteFile(buffer);
   bridge.sandboxed_vfs_file =
       new SandboxedVfsFile(std::move(file), std::move(file_path),
-#if DCHECK_IS_ON()
                            file_type,
-#endif  // DCHECK_IS_ON()
                            vfs);
   bridge.sqlite_file.pMethods = GetSqliteIoMethods();
 }
@@ -224,13 +220,11 @@
   DCHECK_GE(size, 0);
   DCHECK_GE(offset, 0);
 
-#if DCHECK_IS_ON()
   // SQLite's locking protocol only acquires locks on the database file. The
   // journal and the WAL file are always unlocked.
   DCHECK(sqlite_lock_mode_ == SQLITE_LOCK_EXCLUSIVE ||
          file_type_ != SandboxedVfsFileType::kDatabase)
       << "Write to database file with lock mode " << sqlite_lock_mode_;
-#endif  // DCHECK_IS_ON()
 
   const char* data = reinterpret_cast<const char*>(buffer);
 
@@ -563,16 +557,12 @@
 
 SandboxedVfsFile::SandboxedVfsFile(base::File file,
                                    base::FilePath file_path,
-#if DCHECK_IS_ON()
                                    SandboxedVfsFileType file_type,
-#endif  // DCHECK_IS_ON()
                                    SandboxedVfs* vfs)
     : file_(std::move(file)),
       sqlite_lock_mode_(SQLITE_LOCK_NONE),
       vfs_(vfs),
-#if DCHECK_IS_ON()
       file_type_(file_type),
-#endif  // DCHECK_IS_ON()
       file_path_(std::move(file_path)) {
 }
 
diff --git a/sql/sandboxed_vfs_file.h b/sql/sandboxed_vfs_file.h
index 9af81a9..ef30a05 100644
--- a/sql/sandboxed_vfs_file.h
+++ b/sql/sandboxed_vfs_file.h
@@ -50,9 +50,7 @@
   // returned sqlite3_file object.
   static void Create(base::File file,
                      base::FilePath file_path,
-#if DCHECK_IS_ON()
                      SandboxedVfsFileType file_type,
-#endif  // DCHECK_IS_ON()
                      SandboxedVfs* vfs,
                      sqlite3_file& buffer);
 
@@ -85,9 +83,7 @@
  private:
   SandboxedVfsFile(base::File file,
                    base::FilePath file_path,
-#if DCHECK_IS_ON()
                    SandboxedVfsFileType file_type,
-#endif  // DCHECK_IS_ON()
                    SandboxedVfs* vfs);
   ~SandboxedVfsFile();
 
@@ -97,10 +93,8 @@
   int sqlite_lock_mode_;
   // The SandboxedVfs that created this instance.
   const raw_ptr<SandboxedVfs> vfs_;
-#if DCHECK_IS_ON()
   // Tracked to check assumptions about SQLite's locking protocol.
   const SandboxedVfsFileType file_type_;
-#endif  // DCHECK_IS_ON()
   // Used to identify the file in IPCs to the browser process.
   const base::FilePath file_path_;
 };
diff --git a/testing/buildbot/filters/ios.content_browsertests.filter b/testing/buildbot/filters/ios.content_browsertests.filter
index aa7f400..5deded18 100644
--- a/testing/buildbot/filters/ios.content_browsertests.filter
+++ b/testing/buildbot/filters/ios.content_browsertests.filter
@@ -754,10 +754,6 @@
 -Default/MediaTest.HLSSingleWithoutExtension/0
 -Default/MediaTest.HLSSingleWithoutExtension/1
 
-# TODO(crbug.com/418103029): The test has been failing since
-# https://crrev.com/c/6532249.
--SecurityExploitBrowserTest.DidFailLoadWithInvalidErrorCode
-
 # TODO(crbug.com/418125309): These tests have been failing since
 # https://crrev.com/c/6391357.
 -All/SitePerProcessWithMainFrameThresholdAndSiteRestrictionTest.RestrictedToURLWithContentClient/0
diff --git a/testing/scripts/common.py b/testing/scripts/common.py
index 448c438d..208b0d1 100644
--- a/testing/scripts/common.py
+++ b/testing/scripts/common.py
@@ -197,7 +197,20 @@
   elif failures:
     status = result_types.FAIL
   test_log = '\n'.join(failures)
-  result_sink_client.Post(name, status, None, test_log, None)
+
+  # Source comes from:
+  # infra/go/src/go.chromium.org/luci/resultdb/sink/proto/v1/test_result.proto
+  struct_test_dict = {
+      'coarseName': None,  # Not used for single tests.
+      'fineName': None,  # Not used for single tests.
+      'caseNameComponents': ['*fixture'],
+  }
+  result_sink_client.Post(name,
+                          status,
+                          None,
+                          test_log,
+                          None,
+                          test_id_structured=struct_test_dict)
 
 
 def parse_common_test_results(json_results, test_separator='/'):
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 56aefe4..a531278 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -1994,6 +1994,7 @@
         {
             "platforms": [
                 "android",
+                "ios",
                 "mac",
                 "windows"
             ],
@@ -14418,7 +14419,7 @@
             ],
             "experiments": [
                 {
-                    "name": "EnabledDefaultImpl",
+                    "name": "EnabledBaseline",
                     "params": {
                         "messages_accessibility_events_investigations_param": "1"
                     },
@@ -15178,6 +15179,36 @@
             ]
         }
     ],
+    "NtpMicrosoftFilesCard": [
+        {
+            "platforms": [
+                "chromeos",
+                "linux",
+                "mac",
+                "windows"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled_NonInsights",
+                    "params": {
+                        "NtpSharepointModuleDataParam": "non-insights"
+                    },
+                    "enable_features": [
+                        "NtpSharepointModule"
+                    ]
+                },
+                {
+                    "name": "Enabled_Combination",
+                    "params": {
+                        "NtpSharepointModuleDataParam": "combined"
+                    },
+                    "enable_features": [
+                        "NtpSharepointModule"
+                    ]
+                }
+            ]
+        }
+    ],
     "OcclusionCullingQuadSplitLimit": [
         {
             "platforms": [
@@ -24422,6 +24453,31 @@
             ]
         }
     ],
+    "UseFreedesktopSecretKeyProvider": [
+        {
+            "platforms": [
+                "linux"
+            ],
+            "experiments": [
+                {
+                    "name": "EnableDecryption",
+                    "enable_features": [
+                        "UseFreedesktopSecretKeyProvider"
+                    ],
+                    "disable_features": [
+                        "UseFreedesktopSecretKeyProviderForEncryption"
+                    ]
+                },
+                {
+                    "name": "EnableDecryptionAndEncryption",
+                    "enable_features": [
+                        "UseFreedesktopSecretKeyProvider",
+                        "UseFreedesktopSecretKeyProviderForEncryption"
+                    ]
+                }
+            ]
+        }
+    ],
     "UseHeuristicForWindowsFullScreenPowerPoint": [
         {
             "platforms": [
diff --git a/third_party/androidx/build.gradle b/third_party/androidx/build.gradle
index 851d2ea3..4fd71a8 100644
--- a/third_party/androidx/build.gradle
+++ b/third_party/androidx/build.gradle
@@ -305,7 +305,7 @@
     google()
     maven {
         // This URL is generated by the fetch_all_androidx.py script.
-        url 'https://androidx.dev/snapshots/builds/13537461/artifacts/repository'
+        url 'https://androidx.dev/snapshots/builds/13539686/artifacts/repository'
     }
     mavenCentral()
 }
diff --git a/third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom b/third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom
index 86b3d4e..a96527c 100644
--- a/third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom
+++ b/third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom
@@ -914,6 +914,7 @@
     kRowRule = 855,
     kTextGrow = 856,
     kTextShrink = 857,
+    kRule = 858,
 
     // 1. Add new features above this line (don't change the assigned numbers of
     //    the existing items).
diff --git a/third_party/blink/renderer/core/DEPS b/third_party/blink/renderer/core/DEPS
index e20070b..44e3253 100644
--- a/third_party/blink/renderer/core/DEPS
+++ b/third_party/blink/renderer/core/DEPS
@@ -212,8 +212,6 @@
     "media_values.h": [
         "+ui/base/pointer/pointer_device.h",
     ],
-    # TODO(crbug.com/385173568): Remove after AIPromptAPIForExtension OT.
-    "origin_trial_context.cc": [ "+base/command_line.h" ],
     "scripted_idle_task_controller(_test)?.(cc|h)": [
         "+base/task/delayed_task_handle.h",
     ],
diff --git a/third_party/blink/renderer/core/css/css_gap_decoration_property_utils.cc b/third_party/blink/renderer/core/css/css_gap_decoration_property_utils.cc
index 967bc71..9eafab8 100644
--- a/third_party/blink/renderer/core/css/css_gap_decoration_property_utils.cc
+++ b/third_party/blink/renderer/core/css/css_gap_decoration_property_utils.cc
@@ -4,6 +4,9 @@
 
 #include "third_party/blink/renderer/core/css/css_gap_decoration_property_utils.h"
 
+#include "third_party/blink/renderer/core/css/css_property_value.h"
+#include "third_party/blink/renderer/core/css/css_value_list.h"
+#include "third_party/blink/renderer/core/css/properties/css_parsing_utils.h"
 #include "third_party/blink/renderer/core/css/style_color.h"
 #include "third_party/blink/renderer/core/style/gap_data_list.h"
 
@@ -53,4 +56,39 @@
       });
 }
 
+void CSSGapDecorationUtils::AddProperties(
+    CSSGapDecorationPropertyDirection direction,
+    const CSSValueList& rule_widths,
+    const CSSValueList& rule_styles,
+    const CSSValueList& rule_colors,
+    bool important,
+    HeapVector<CSSPropertyValue, 64>& properties) {
+  CSSPropertyID rule_shorthand_id;
+  CSSPropertyID rule_width_id;
+  CSSPropertyID rule_style_id;
+  CSSPropertyID rule_color_id;
+
+  if (direction == CSSGapDecorationPropertyDirection::kColumn) {
+    rule_shorthand_id = CSSPropertyID::kColumnRule;
+    rule_width_id = CSSPropertyID::kColumnRuleWidth;
+    rule_style_id = CSSPropertyID::kColumnRuleStyle;
+    rule_color_id = CSSPropertyID::kColumnRuleColor;
+  } else {
+    rule_shorthand_id = CSSPropertyID::kRowRule;
+    rule_width_id = CSSPropertyID::kRowRuleWidth;
+    rule_style_id = CSSPropertyID::kRowRuleStyle;
+    rule_color_id = CSSPropertyID::kRowRuleColor;
+  }
+
+  css_parsing_utils::AddProperty(
+      rule_width_id, rule_shorthand_id, rule_widths, important,
+      css_parsing_utils::IsImplicitProperty::kNotImplicit, properties);
+  css_parsing_utils::AddProperty(
+      rule_style_id, rule_shorthand_id, rule_styles, important,
+      css_parsing_utils::IsImplicitProperty::kNotImplicit, properties);
+  css_parsing_utils::AddProperty(
+      rule_color_id, rule_shorthand_id, rule_colors, important,
+      css_parsing_utils::IsImplicitProperty::kNotImplicit, properties);
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/css/css_gap_decoration_property_utils.h b/third_party/blink/renderer/core/css/css_gap_decoration_property_utils.h
index b7672c45..fb21233 100644
--- a/third_party/blink/renderer/core/css/css_gap_decoration_property_utils.h
+++ b/third_party/blink/renderer/core/css/css_gap_decoration_property_utils.h
@@ -6,9 +6,12 @@
 #define THIRD_PARTY_BLINK_RENDERER_CORE_CSS_CSS_GAP_DECORATION_PROPERTY_UTILS_H_
 
 #include "third_party/blink/renderer/core/css/css_property_names.h"
+#include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h"
 
 namespace blink {
 
+class CSSPropertyValue;
+class CSSValueList;
 template <typename T>
 class GapDataList;
 class StyleColor;
@@ -36,6 +39,12 @@
       CSSGapDecorationPropertyType type);
   static CSSPropertyID GetShorthandProperty(
       CSSGapDecorationPropertyDirection direction);
+  static void AddProperties(CSSGapDecorationPropertyDirection direction,
+                            const CSSValueList& rule_widths,
+                            const CSSValueList& rule_styles,
+                            const CSSValueList& rule_colors,
+                            bool important,
+                            HeapVector<CSSPropertyValue, 64>& properties);
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/css/css_properties.json5 b/third_party/blink/renderer/core/css/css_properties.json5
index feb4523..b1108aa0 100644
--- a/third_party/blink/renderer/core/css/css_properties.json5
+++ b/third_party/blink/renderer/core/css/css_properties.json5
@@ -8678,6 +8678,14 @@
       property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"],
     },
     {
+      name: "rule",
+      longhands: [
+        "column-rule-width", "column-rule-style", "column-rule-color",
+        "row-rule-width", "row-rule-style", "row-rule-color"],
+      property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"],
+      runtime_flag: "CSSGapDecoration",
+    },
+    {
       name: "rule-color",
       longhands: ["column-rule-color", "row-rule-color"],
       property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"],
diff --git a/third_party/blink/renderer/core/css/css_property_equality.cc b/third_party/blink/renderer/core/css/css_property_equality.cc
index fc4169c..9ec4976 100644
--- a/third_party/blink/renderer/core/css/css_property_equality.cc
+++ b/third_party/blink/renderer/core/css/css_property_equality.cc
@@ -1293,6 +1293,7 @@
     case CSSPropertyID::kPlaceSelf:
     case CSSPropertyID::kPositionTry:
     case CSSPropertyID::kRowRule:
+    case CSSPropertyID::kRule:
     case CSSPropertyID::kRuleColor:
     case CSSPropertyID::kRuleWidth:
     case CSSPropertyID::kRuleStyle:
diff --git a/third_party/blink/renderer/core/css/properties/css_parsing_utils.cc b/third_party/blink/renderer/core/css/properties/css_parsing_utils.cc
index 5c84374..cf5d4c54 100644
--- a/third_party/blink/renderer/core/css/properties/css_parsing_utils.cc
+++ b/third_party/blink/renderer/core/css/properties/css_parsing_utils.cc
@@ -7036,28 +7036,17 @@
 
 // Top level parsing function for the gap-decorations shorthand, which handles
 // the parsing of simple <gap-rule> or repeat <gap-rule> values.
-bool ConsumeGapDecorationsRuleShorthand(
-    CSSGapDecorationPropertyDirection direction,
-    bool important,
-    const CSSParserContext& context,
-    CSSParserTokenStream& stream,
-    HeapVector<CSSPropertyValue, 64>& properties) {
-  const StylePropertyShorthand& gapDecorationsRuleShorthand =
-      direction == CSSGapDecorationPropertyDirection::kRow
-          ? rowRuleShorthand()
-          : columnRuleShorthand();
-  DCHECK_EQ(gapDecorationsRuleShorthand.length(), 3u);
+bool ConsumeGapDecorationsRuleShorthand(bool important,
+                                        const CSSParserContext& context,
+                                        CSSParserTokenStream& stream,
+                                        CSSValueList*& rule_widths,
+                                        CSSValueList*& rule_styles,
+                                        CSSValueList*& rule_colors) {
+  CHECK(RuntimeEnabledFeatures::CSSGapDecorationEnabled());
 
-  // If the CSSGapDecorations feature is not enabled, consume greedily since
-  // only single values are supported by 'column-rule' today.
-  if (!RuntimeEnabledFeatures::CSSGapDecorationEnabled()) {
-    return css_parsing_utils::ConsumeShorthandGreedilyViaLonghands(
-        gapDecorationsRuleShorthand, important, context, stream, properties);
-  }
-
-  CSSValueList* rule_widths = CSSValueList::CreateSpaceSeparated();
-  CSSValueList* rule_styles = CSSValueList::CreateSpaceSeparated();
-  CSSValueList* rule_colors = CSSValueList::CreateSpaceSeparated();
+  rule_widths = CSSValueList::CreateSpaceSeparated();
+  rule_styles = CSSValueList::CreateSpaceSeparated();
+  rule_colors = CSSValueList::CreateSpaceSeparated();
 
   bool has_seen_auto_repeat = false;
 
@@ -7103,27 +7092,6 @@
     return false;
   }
 
-  css_parsing_utils::AddProperty(
-      CSSGapDecorationUtils::GetLonghandProperty(
-          direction, CSSGapDecorationPropertyType::kWidth),
-      CSSGapDecorationUtils::GetShorthandProperty(direction), *rule_widths,
-      important, css_parsing_utils::IsImplicitProperty::kNotImplicit,
-      properties);
-
-  css_parsing_utils::AddProperty(
-      CSSGapDecorationUtils::GetLonghandProperty(
-          direction, CSSGapDecorationPropertyType::kStyle),
-      CSSGapDecorationUtils::GetShorthandProperty(direction), *rule_styles,
-      important, css_parsing_utils::IsImplicitProperty::kNotImplicit,
-      properties);
-
-  css_parsing_utils::AddProperty(
-      CSSGapDecorationUtils::GetLonghandProperty(
-          direction, CSSGapDecorationPropertyType::kColor),
-      CSSGapDecorationUtils::GetShorthandProperty(direction), *rule_colors,
-      important, css_parsing_utils::IsImplicitProperty::kNotImplicit,
-      properties);
-
   return true;
 }
 
diff --git a/third_party/blink/renderer/core/css/properties/css_parsing_utils.h b/third_party/blink/renderer/core/css/properties/css_parsing_utils.h
index be799b8..cc0d8726 100644
--- a/third_party/blink/renderer/core/css/properties/css_parsing_utils.h
+++ b/third_party/blink/renderer/core/css/properties/css_parsing_utils.h
@@ -557,12 +557,12 @@
 
 CSSValue* ConsumeItemTolerance(CSSParserTokenStream&, const CSSParserContext&);
 
-bool ConsumeGapDecorationsRuleShorthand(
-    CSSGapDecorationPropertyDirection direction,
-    bool important,
-    const CSSParserContext&,
-    CSSParserTokenStream&,
-    HeapVector<CSSPropertyValue, 64>& properties);
+bool ConsumeGapDecorationsRuleShorthand(bool important,
+                                        const CSSParserContext& context,
+                                        CSSParserTokenStream& stream,
+                                        CSSValueList*& rule_widths,
+                                        CSSValueList*& rule_styles,
+                                        CSSValueList*& rule_colors);
 
 CSSValue* ConsumeHyphenateLimitChars(CSSParserTokenStream&,
                                      const CSSParserContext&);
diff --git a/third_party/blink/renderer/core/css/properties/shorthands/shorthands_custom.cc b/third_party/blink/renderer/core/css/properties/shorthands/shorthands_custom.cc
index 4a513ea..d648b53e 100644
--- a/third_party/blink/renderer/core/css/properties/shorthands/shorthands_custom.cc
+++ b/third_party/blink/renderer/core/css/properties/shorthands/shorthands_custom.cc
@@ -1196,9 +1196,32 @@
     const CSSParserContext& context,
     const CSSParserLocalContext&,
     HeapVector<CSSPropertyValue, 64>& properties) const {
-  return css_parsing_utils::ConsumeGapDecorationsRuleShorthand(
-      CSSGapDecorationPropertyDirection::kColumn, important, context, stream,
-      properties);
+  DCHECK_EQ(columnRuleShorthand().length(), 3u);
+  // If the CSSGapDecorations feature is not enabled, consume greedily since
+  // only single values are supported by 'column-rule' today.
+  if (!RuntimeEnabledFeatures::CSSGapDecorationEnabled()) {
+    return css_parsing_utils::ConsumeShorthandGreedilyViaLonghands(
+        columnRuleShorthand(), important, context, stream, properties);
+  }
+
+  CSSValueList* rule_widths = nullptr;
+  CSSValueList* rule_styles = nullptr;
+  CSSValueList* rule_colors = nullptr;
+
+  if (!css_parsing_utils::ConsumeGapDecorationsRuleShorthand(
+          important, context, stream, rule_widths, rule_styles, rule_colors)) {
+    return false;
+  }
+
+  CHECK(rule_widths);
+  CHECK(rule_styles);
+  CHECK(rule_colors);
+
+  CSSGapDecorationUtils::AddProperties(
+      CSSGapDecorationPropertyDirection::kColumn, *rule_widths, *rule_styles,
+      *rule_colors, important, properties);
+
+  return true;
 }
 
 const CSSValue* ColumnRule::CSSValueFromComputedStyleInternal(
@@ -1217,9 +1240,25 @@
     const CSSParserContext& context,
     const CSSParserLocalContext&,
     HeapVector<CSSPropertyValue, 64>& properties) const {
-  return css_parsing_utils::ConsumeGapDecorationsRuleShorthand(
-      CSSGapDecorationPropertyDirection::kRow, important, context, stream,
-      properties);
+  DCHECK_EQ(rowRuleShorthand().length(), 3u);
+  CSSValueList* rule_widths = nullptr;
+  CSSValueList* rule_styles = nullptr;
+  CSSValueList* rule_colors = nullptr;
+
+  if (!css_parsing_utils::ConsumeGapDecorationsRuleShorthand(
+          important, context, stream, rule_widths, rule_styles, rule_colors)) {
+    return false;
+  }
+
+  CHECK(rule_widths);
+  CHECK(rule_styles);
+  CHECK(rule_colors);
+
+  CSSGapDecorationUtils::AddProperties(CSSGapDecorationPropertyDirection::kRow,
+                                       *rule_widths, *rule_styles, *rule_colors,
+                                       important, properties);
+
+  return true;
 }
 
 const CSSValue* RowRule::CSSValueFromComputedStyleInternal(
@@ -4134,6 +4173,53 @@
       *this, style, &style.MaskLayers());
 }
 
+bool Rule::ParseShorthand(bool important,
+                          CSSParserTokenStream& stream,
+                          const CSSParserContext& context,
+                          const CSSParserLocalContext& local_context,
+                          HeapVector<CSSPropertyValue, 64>& properties) const {
+  DCHECK_EQ(ruleShorthand().length(), 6u);
+  CSSValueList* rule_widths = nullptr;
+  CSSValueList* rule_styles = nullptr;
+  CSSValueList* rule_colors = nullptr;
+
+  if (!css_parsing_utils::ConsumeGapDecorationsRuleShorthand(
+          important, context, stream, rule_widths, rule_styles, rule_colors)) {
+    return false;
+  }
+
+  CHECK(rule_widths);
+  CHECK(rule_styles);
+  CHECK(rule_colors);
+
+  CSSGapDecorationUtils::AddProperties(
+      CSSGapDecorationPropertyDirection::kColumn, *rule_widths, *rule_styles,
+      *rule_colors, important, properties);
+  CSSGapDecorationUtils::AddProperties(CSSGapDecorationPropertyDirection::kRow,
+                                       *rule_widths, *rule_styles, *rule_colors,
+                                       important, properties);
+
+  return true;
+}
+
+const CSSValue* Rule::CSSValueFromComputedStyleInternal(
+    const ComputedStyle& style,
+    const LayoutObject* layout_object,
+    bool allow_visited_style,
+    CSSValuePhase value_phase) const {
+  const CSSValue* column_value =
+      GetCSSPropertyColumnRule().CSSValueFromComputedStyle(
+          style, layout_object, allow_visited_style, value_phase);
+  const CSSValue* row_value = GetCSSPropertyRowRule().CSSValueFromComputedStyle(
+      style, layout_object, allow_visited_style, value_phase);
+
+  if (!base::ValuesEquivalent(column_value, row_value)) {
+    return nullptr;
+  }
+
+  return column_value;
+}
+
 bool RuleColor::ParseShorthand(
     bool important,
     CSSParserTokenStream& stream,
diff --git a/third_party/blink/renderer/core/css/style_property_serializer.cc b/third_party/blink/renderer/core/css/style_property_serializer.cc
index cfe620a9..b92440c 100644
--- a/third_party/blink/renderer/core/css/style_property_serializer.cc
+++ b/third_party/blink/renderer/core/css/style_property_serializer.cc
@@ -666,6 +666,9 @@
       return GetLayeredShorthandValue(maskPositionShorthand());
     case CSSPropertyID::kMask:
       return GetLayeredShorthandValue(maskShorthand());
+    case CSSPropertyID::kRule:
+      return GetShorthandValueForRule(rowRuleShorthand(),
+                                      columnRuleShorthand());
     case CSSPropertyID::kRuleColor:
       return GetShorthandValueForBidirectionalGapRules(ruleColorShorthand());
     case CSSPropertyID::kRuleWidth:
@@ -1907,6 +1910,29 @@
   return result.ReleaseString();
 }
 
+String StylePropertySerializer::GetShorthandValueForRule(
+    const StylePropertyShorthand& row_rule_shorthand,
+    const StylePropertyShorthand& column_rule_shorthand) const {
+  CHECK_EQ(column_rule_shorthand.length(), row_rule_shorthand.length());
+  for (wtf_size_t i = 0; i < row_rule_shorthand.length(); ++i) {
+    const CSSValue* row_rule_data =
+        property_set_.GetPropertyCSSValue(*row_rule_shorthand.properties()[i]);
+    const CSSValue* column_rule_data = property_set_.GetPropertyCSSValue(
+        *column_rule_shorthand.properties()[i]);
+
+    if (!base::ValuesEquivalent(row_rule_data, column_rule_data)) {
+      return String();
+    }
+  }
+  // If the values are equivalent, serialize one of the shorthands.
+  // The `rule` shorthand is bi-directional, so the values should be
+  // equivalent.
+  //
+  // https://drafts.csswg.org/css-gaps-1/#rule-bi-directional
+  return GetShorthandValueForGapDecorationsRule(
+      column_rule_shorthand, CSSGapDecorationPropertyDirection::kColumn);
+}
+
 String StylePropertySerializer::GetShorthandValueForBidirectionalGapRules(
     const StylePropertyShorthand& shorthand) const {
   DCHECK(shorthand.length() == 2u);
diff --git a/third_party/blink/renderer/core/css/style_property_serializer.h b/third_party/blink/renderer/core/css/style_property_serializer.h
index b2c5313d..257a2059d 100644
--- a/third_party/blink/renderer/core/css/style_property_serializer.h
+++ b/third_party/blink/renderer/core/css/style_property_serializer.h
@@ -64,6 +64,8 @@
   String PageBreakPropertyValue(const StylePropertyShorthand&) const;
   String GetShorthandValue(const StylePropertyShorthand&,
                            String separator = " ") const;
+  String GetShorthandValueForRule(const StylePropertyShorthand&,
+                                  const StylePropertyShorthand&) const;
   String GetShorthandValueForBidirectionalGapRules(
       const StylePropertyShorthand&) const;
   String GetShorthandValueForGapDecorationsRule(
diff --git a/third_party/blink/renderer/core/dom/element.cc b/third_party/blink/renderer/core/dom/element.cc
index a28706c2..23a07e44 100644
--- a/third_party/blink/renderer/core/dom/element.cc
+++ b/third_party/blink/renderer/core/dom/element.cc
@@ -2625,7 +2625,7 @@
     gfx::PointF new_position = current_position + displacement;
 
     std::unique_ptr<cc::SnapSelectionStrategy> strategy =
-        cc::SnapSelectionStrategy::CreateForEndAndDirection(
+        cc::SnapSelectionStrategy::CreateForDisplacement(
             current_position, displacement,
             RuntimeEnabledFeatures::FractionalScrollOffsetsEnabled());
     new_position =
@@ -2726,7 +2726,7 @@
   gfx::PointF new_position = viewport->ScrollPosition() + displacement;
   gfx::PointF current_position = viewport->ScrollPosition();
   std::unique_ptr<cc::SnapSelectionStrategy> strategy =
-      cc::SnapSelectionStrategy::CreateForEndAndDirection(
+      cc::SnapSelectionStrategy::CreateForDisplacement(
           current_position, displacement,
           RuntimeEnabledFeatures::FractionalScrollOffsetsEnabled());
   new_position =
diff --git a/third_party/blink/renderer/core/dom/scroll_button_pseudo_element.cc b/third_party/blink/renderer/core/dom/scroll_button_pseudo_element.cc
index f55c334f1..e5edefb 100644
--- a/third_party/blink/renderer/core/dom/scroll_button_pseudo_element.cc
+++ b/third_party/blink/renderer/core/dom/scroll_button_pseudo_element.cc
@@ -29,13 +29,18 @@
 
 ScrollOffset CalculateSnappedScrollPosition(
     const ScrollableArea* scrollable_area,
-    gfx::Vector2dF scaled_delta) {
+    ScrollDirectionPhysical direction) {
   gfx::PointF current_position = scrollable_area->ScrollPosition();
   std::unique_ptr<cc::SnapSelectionStrategy> strategy =
-      cc::SnapSelectionStrategy::CreateForEndAndDirection(
-          current_position, scaled_delta,
-          RuntimeEnabledFeatures::FractionalScrollOffsetsEnabled());
-  current_position += scaled_delta;
+      scrollable_area->PageScrollSnapStrategy(direction);
+  gfx::Vector2dF displacement = ToScrollDelta(direction, 1);
+  displacement.Scale(
+      scrollable_area->ScrollStep(ui::ScrollGranularity::kScrollByPage,
+                                  kHorizontalScrollbar),
+      scrollable_area->ScrollStep(ui::ScrollGranularity::kScrollByPage,
+                                  kVerticalScrollbar));
+
+  current_position += displacement;
   if (std::optional<cc::SnapPositionData> snap_position =
           scrollable_area->GetSnapPosition(*strategy)) {
     if (snap_position->type != cc::SnapPositionData::Type::kNone) {
@@ -81,26 +86,26 @@
       GetPseudoId() == kPseudoIdScrollButtonInlineEnd,
       GetPseudoId() == kPseudoIdScrollButtonBlockStart,
       GetPseudoId() == kPseudoIdScrollButtonBlockEnd);
-  gfx::Vector2dF displacement;
+  std::optional<ScrollDirectionPhysical> direction;
   if (mapping.Top()) {
-    displacement.set_y(-scrollable_area->ScrollStep(
-        ui::ScrollGranularity::kScrollByPage, kVerticalScrollbar));
+    direction = ScrollDirectionPhysical::kScrollUp;
   } else if (mapping.Bottom()) {
-    displacement.set_y(scrollable_area->ScrollStep(
-        ui::ScrollGranularity::kScrollByPage, kVerticalScrollbar));
+    direction = ScrollDirectionPhysical::kScrollDown;
   } else if (mapping.Left()) {
-    displacement.set_x(-scrollable_area->ScrollStep(
-        ui::ScrollGranularity::kScrollByPage, kHorizontalScrollbar));
+    direction = ScrollDirectionPhysical::kScrollLeft;
   } else if (mapping.Right()) {
-    displacement.set_x(scrollable_area->ScrollStep(
-        ui::ScrollGranularity::kScrollByPage, kHorizontalScrollbar));
+    direction = ScrollDirectionPhysical::kScrollRight;
   }
-  if (!displacement.IsZero()) {
+  if (direction) {
+    gfx::Vector2dF displacement = ToScrollDelta(*direction, 1);
+    displacement.Scale(
+        scrollable_area->ScrollStep(ui::ScrollGranularity::kScrollByPage,
+                                    kHorizontalScrollbar),
+        scrollable_area->ScrollStep(ui::ScrollGranularity::kScrollByPage,
+                                    kVerticalScrollbar));
     gfx::PointF current_position = scrollable_area->ScrollPosition();
     std::unique_ptr<cc::SnapSelectionStrategy> strategy =
-        cc::SnapSelectionStrategy::CreateForEndAndDirection(
-            current_position, displacement,
-            RuntimeEnabledFeatures::FractionalScrollOffsetsEnabled());
+        scrollable_area->PageScrollSnapStrategy(*direction);
     gfx::PointF new_position =
         scrollable_area->GetSnapPositionAndSetTarget(*strategy).value_or(
             current_position + displacement);
@@ -179,36 +184,22 @@
   if (mapping.Top()) {
     enabled_ = current_position.y() >
                CalculateSnappedScrollPosition(
-                   scrollable_area,
-                   gfx::Vector2dF(0, -scrollable_area->ScrollStep(
-                                         ui::ScrollGranularity::kScrollByPage,
-                                         kVerticalScrollbar)))
+                   scrollable_area, ScrollDirectionPhysical::kScrollUp)
                    .y();
   } else if (mapping.Bottom()) {
     enabled_ = current_position.y() <
                CalculateSnappedScrollPosition(
-                   scrollable_area,
-                   gfx::Vector2dF(0, scrollable_area->ScrollStep(
-                                         ui::ScrollGranularity::kScrollByPage,
-                                         kVerticalScrollbar)))
+                   scrollable_area, ScrollDirectionPhysical::kScrollDown)
                    .y();
   } else if (mapping.Left()) {
     enabled_ = current_position.x() >
                CalculateSnappedScrollPosition(
-                   scrollable_area,
-                   gfx::Vector2dF(-scrollable_area->ScrollStep(
-                                      ui::ScrollGranularity::kScrollByPage,
-                                      kHorizontalScrollbar),
-                                  0))
+                   scrollable_area, ScrollDirectionPhysical::kScrollLeft)
                    .x();
   } else if (mapping.Right()) {
     enabled_ = current_position.x() <
                CalculateSnappedScrollPosition(
-                   scrollable_area,
-                   gfx::Vector2dF(scrollable_area->ScrollStep(
-                                      ui::ScrollGranularity::kScrollByPage,
-                                      kHorizontalScrollbar),
-                                  0))
+                   scrollable_area, ScrollDirectionPhysical::kScrollRight)
                    .x();
   }
   if (enabled != enabled_) {
diff --git a/third_party/blink/renderer/core/exported/web_node_test.cc b/third_party/blink/renderer/core/exported/web_node_test.cc
index 93724dcd..5add9b46 100644
--- a/third_party/blink/renderer/core/exported/web_node_test.cc
+++ b/third_party/blink/renderer/core/exported/web_node_test.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "base/strings/stringprintf.h"
 #include "base/test/mock_callback.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/web/web_dom_event.h"
diff --git a/third_party/blink/renderer/core/frame/local_dom_window.cc b/third_party/blink/renderer/core/frame/local_dom_window.cc
index a863814a..23e8b082 100644
--- a/third_party/blink/renderer/core/frame/local_dom_window.cc
+++ b/third_party/blink/renderer/core/frame/local_dom_window.cc
@@ -1871,7 +1871,7 @@
   gfx::PointF new_scaled_position = current_position + scaled_delta;
 
   std::unique_ptr<cc::SnapSelectionStrategy> strategy =
-      cc::SnapSelectionStrategy::CreateForEndAndDirection(
+      cc::SnapSelectionStrategy::CreateForDisplacement(
           current_position, scaled_delta,
           RuntimeEnabledFeatures::FractionalScrollOffsetsEnabled());
   new_scaled_position =
diff --git a/third_party/blink/renderer/core/input/event_handler_test.cc b/third_party/blink/renderer/core/input/event_handler_test.cc
index 473c00d4..1cfa0ef 100644
--- a/third_party/blink/renderer/core/input/event_handler_test.cc
+++ b/third_party/blink/renderer/core/input/event_handler_test.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "base/strings/stringprintf.h"
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
 #include "testing/gtest/include/gtest/gtest.h"
diff --git a/third_party/blink/renderer/core/input/scroll_manager.cc b/third_party/blink/renderer/core/input/scroll_manager.cc
index 93a4f622..febf350 100644
--- a/third_party/blink/renderer/core/input/scroll_manager.cc
+++ b/third_party/blink/renderer/core/input/scroll_manager.cc
@@ -222,33 +222,27 @@
     ScrollableArea* scrollable_area = ScrollableArea::GetForScrolling(box);
     DCHECK(scrollable_area);
 
-    ScrollOffset delta = ToScrollDelta(physical_direction, 1);
-    delta.Scale(scrollable_area->ScrollStep(granularity, kHorizontalScrollbar),
-                scrollable_area->ScrollStep(granularity, kVerticalScrollbar));
     // Pressing the arrow key is considered as a scroll with intended direction
     // only. Pressing the PgUp/PgDn key is considered as a scroll with intended
     // direction and end position. Pressing the Home/End key is considered as a
     // scroll with intended end position only.
     switch (granularity) {
       case ui::ScrollGranularity::kScrollByLine: {
-        if (scrollable_area->SnapForDirection(delta))
+        if (scrollable_area->SnapForDirection(physical_direction)) {
           return true;
+        }
         break;
       }
       case ui::ScrollGranularity::kScrollByPage: {
-        if (scrollable_area->SnapForEndAndDirection(delta))
+        if (scrollable_area->SnapForPageScroll(physical_direction)) {
           return true;
+        }
         break;
       }
       case ui::ScrollGranularity::kScrollByDocument: {
-        gfx::PointF end_position = scrollable_area->ScrollPosition() + delta;
-        bool scrolled_x = physical_direction == kScrollLeft ||
-                          physical_direction == kScrollRight;
-        bool scrolled_y = physical_direction == kScrollUp ||
-                          physical_direction == kScrollDown;
-        if (scrollable_area->SnapForEndPosition(end_position, scrolled_x,
-                                                scrolled_y))
+        if (scrollable_area->SnapForDocumentScroll(physical_direction)) {
           return true;
+        }
         break;
       }
       default:
diff --git a/third_party/blink/renderer/core/origin_trials/origin_trial_context.cc b/third_party/blink/renderer/core/origin_trials/origin_trial_context.cc
index 059b225..2867bf4 100644
--- a/third_party/blink/renderer/core/origin_trials/origin_trial_context.cc
+++ b/third_party/blink/renderer/core/origin_trials/origin_trial_context.cc
@@ -7,7 +7,6 @@
 #include <ostream>
 #include <vector>
 
-#include "base/command_line.h"
 #include "base/feature_list.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
@@ -564,14 +563,6 @@
     return base::FeatureList::IsEnabled(features::kLanguageDetectionAPI);
   }
 
-  // TODO(crbug.com/385173568): Remove after AIPromptAPIForExtension OT.
-  if (trial_name == "AIPromptAPIForExtension") {
-    return base::FeatureList::IsEnabled(features::kAIPromptAPI) &&
-           base::FeatureList::IsEnabled(features::kAIPromptAPIForExtension) &&
-           base::CommandLine::ForCurrentProcess()->HasSwitch(
-               "extension-process");
-  }
-
   if (trial_name == "SpeculationRulesTargetHint") {
     return base::FeatureList::IsEnabled(features::kPrerender2InNewTab);
   }
diff --git a/third_party/blink/renderer/core/paint/README.md b/third_party/blink/renderer/core/paint/README.md
index ceb006c8..13d002e 100644
--- a/third_party/blink/renderer/core/paint/README.md
+++ b/third_party/blink/renderer/core/paint/README.md
@@ -531,3 +531,11 @@
 cc::PaintedOverlayScrollbarLayer depending on the type of the scrollbar.
 
 Custom scrollbars are still painted into drawing display items directly.
+
+## Pixel snapping and bluriness
+
+Bluriness can happen when drawings are not aligned to screen pixels. In
+Chromium, we try to align drawings to screen pixels when possible /
+necessary in almost every stage of rendering.
+[This document](https://docs.google.com/document/d/14qWYuGOJRELueTORi2ais5BIJGdo8HADXe07BjjThIs/edit)
+contains some useful links to related docs and bugs.
diff --git a/third_party/blink/renderer/core/paint/filter_effect_builder.cc b/third_party/blink/renderer/core/paint/filter_effect_builder.cc
index 46b7255..bc4d385 100644
--- a/third_party/blink/renderer/core/paint/filter_effect_builder.cc
+++ b/third_party/blink/renderer/core/paint/filter_effect_builder.cc
@@ -131,10 +131,7 @@
                                          const cc::PaintFlags* fill_flags,
                                          const cc::PaintFlags* stroke_flags)
     : reference_box_(reference_box),
-      viewport_(
-          RuntimeEnabledFeatures::SvgFilterUserSpaceViewportForNonSvgEnabled()
-              ? viewport
-              : std::nullopt),
+      viewport_(viewport),
       zoom_(zoom),
       shorthand_scale_(1),
       current_color_(current_color),
@@ -537,12 +534,7 @@
   // primitives since the behavior in these two cases (no primitives, empty
   // region) should match.
   if (filter_region.IsEmpty()) {
-    // TODO(fs): We rely on the presence of a node map here to opt-in to the
-    // check for an empty filter region. The reason for this is that we lack a
-    // viewport to resolve against for HTML content. This is crbug.com/512453.
-    if (viewport_ || node_map) {
-      return result;
-    }
+    return result;
   }
 
   if (!previous_effect)
diff --git a/third_party/blink/renderer/core/scheduler/dom_task.cc b/third_party/blink/renderer/core/scheduler/dom_task.cc
index 16f6df5..3d68cf34 100644
--- a/third_party/blink/renderer/core/scheduler/dom_task.cc
+++ b/third_party/blink/renderer/core/scheduler/dom_task.cc
@@ -171,8 +171,9 @@
   // For the main thread (tracker exists), create the task scope with the signal
   // to set up propagation. On workers, set the current context here since there
   // is no tracker.
-  if (auto* tracker =
-          scheduler::TaskAttributionTracker::From(script_state->GetIsolate())) {
+  auto* tracker =
+      scheduler::TaskAttributionTracker::From(script_state->GetIsolate());
+  if (tracker) {
     task_attribution_scope = tracker->CreateTaskScope(
         script_state, parent_task_,
         scheduler::TaskAttributionTracker::TaskScopeType::kSchedulerPostTask,
@@ -202,6 +203,11 @@
   }
   execution_state_ = pending_result.IsEmpty() ? ExecutionState::kFinished
                                               : ExecutionState::kRunningAsync;
+  // If this is a worker, clear the context to prevent it from leaking to the
+  // next task (`task_attribution_scope` handles this on the main thread).
+  if (!tracker) {
+    ScriptWrappableTaskState::SetCurrent(script_state, nullptr);
+  }
 }
 
 void DOMTask::OnAbort() {
diff --git a/third_party/blink/renderer/core/scroll/scrollable_area.cc b/third_party/blink/renderer/core/scroll/scrollable_area.cc
index ccb6404..0097952 100644
--- a/third_party/blink/renderer/core/scroll/scrollable_area.cc
+++ b/third_party/blink/renderer/core/scroll/scrollable_area.cc
@@ -31,6 +31,8 @@
 
 #include "third_party/blink/renderer/core/scroll/scrollable_area.h"
 
+#include <limits>
+
 #include "base/task/single_thread_task_runner.h"
 #include "build/build_config.h"
 #include "cc/input/main_thread_scrolling_reason.h"
@@ -1068,17 +1070,19 @@
   return PixelsPerLineStep(GetLayoutBox()->GetFrame());
 }
 
-int ScrollableArea::PageStep(ScrollbarOrientation orientation) const {
+gfx::Size ScrollableArea::PageSize() const {
   // Paging scroll operations should take scroll-padding into account [1]. So we
   // use the snapport rect to calculate the page step instead of the visible
   // rect.
   // [1] https://drafts.csswg.org/css-scroll-snap/#scroll-padding
-  const gfx::Size snapport_size =
-      VisibleScrollSnapportRect(kExcludeScrollbars).PixelSnappedSize();
-  const int snapport_length = (orientation == kHorizontalScrollbar)
-                                  ? snapport_size.width()
-                                  : snapport_size.height();
-  return cc::ScrollUtils::CalculatePageStep(snapport_length);
+  return VisibleScrollSnapportRect(kExcludeScrollbars).PixelSnappedSize();
+}
+
+int ScrollableArea::PageStep(ScrollbarOrientation orientation) const {
+  gfx::Size page_size = PageSize();
+  return cc::ScrollUtils::CalculatePageStep(orientation == kHorizontalScrollbar
+                                                ? page_size.width()
+                                                : page_size.height());
 }
 
 int ScrollableArea::DocumentStep(ScrollbarOrientation orientation) const {
@@ -1203,28 +1207,68 @@
                          std::move(on_finish));
 }
 
-bool ScrollableArea::SnapForDirection(const ScrollOffset& delta,
-                                      base::ScopedClosureRunner on_finish) {
+bool ScrollableArea::SnapForDirection(ScrollDirectionPhysical direction) {
   DCHECK(IsRootFrameViewport() || !GetLayoutBox()->IsGlobalRootScroller());
+  ScrollOffset delta = ToScrollDelta(direction, 1);
+  delta.Scale(LineStep(kHorizontalScrollbar), LineStep(kVerticalScrollbar));
+
   gfx::PointF current_position = ScrollPosition();
   std::unique_ptr<cc::SnapSelectionStrategy> strategy =
       cc::SnapSelectionStrategy::CreateForDirection(
           current_position, delta,
           RuntimeEnabledFeatures::FractionalScrollOffsetsEnabled());
-  return PerformSnapping(*strategy, mojom::blink::ScrollBehavior::kSmooth,
-                         std::move(on_finish));
+  return PerformSnapping(*strategy, mojom::blink::ScrollBehavior::kSmooth);
 }
 
-bool ScrollableArea::SnapForEndAndDirection(const ScrollOffset& delta) {
+bool ScrollableArea::SnapForPageScroll(ScrollDirectionPhysical direction) {
   DCHECK(IsRootFrameViewport() || !GetLayoutBox()->IsGlobalRootScroller());
-  gfx::PointF current_position = ScrollPosition();
   std::unique_ptr<cc::SnapSelectionStrategy> strategy =
-      cc::SnapSelectionStrategy::CreateForEndAndDirection(
-          current_position, delta,
-          RuntimeEnabledFeatures::FractionalScrollOffsetsEnabled());
+      PageScrollSnapStrategy(direction);
   return PerformSnapping(*strategy);
 }
 
+bool ScrollableArea::SnapForDocumentScroll(ScrollDirectionPhysical direction) {
+  ScrollOffset delta = ToScrollDelta(direction, 1);
+  delta.Scale(DocumentStep(kHorizontalScrollbar),
+              DocumentStep(kVerticalScrollbar));
+  gfx::PointF end_position = ScrollPosition() + delta;
+  bool scrolled_x = direction == kScrollLeft || direction == kScrollRight;
+  bool scrolled_y = direction == kScrollUp || direction == kScrollDown;
+  return SnapForEndPosition(end_position, scrolled_x, scrolled_y);
+}
+
+std::unique_ptr<cc::SnapSelectionStrategy>
+ScrollableArea::PageScrollSnapStrategy(
+    ScrollDirectionPhysical direction) const {
+  gfx::PointF current_position = ScrollPosition();
+  gfx::Size page_size = PageSize();
+  ScrollOffset unit_direction = ToScrollDelta(direction, 1);
+  ScrollOffset delta = unit_direction;
+  delta.Scale(cc::ScrollUtils::CalculatePageStep(page_size.width()),
+              cc::ScrollUtils::CalculatePageStep(page_size.height()));
+
+  // When scrolling by a page, we prefer that we scroll no more than a page,
+  // but at least by a reasonable proportion of that page.
+  ScrollOffset preferred_max_delta = unit_direction;
+  preferred_max_delta.Scale(
+      cc::ScrollUtils::CalculateMaxPageSnap(page_size.width()),
+      cc::ScrollUtils::CalculateMaxPageSnap(page_size.height()));
+  ScrollOffset preferred_min_delta = unit_direction;
+  preferred_min_delta.Scale(
+      cc::ScrollUtils::CalculateMinPageSnap(page_size.width()),
+      cc::ScrollUtils::CalculateMinPageSnap(page_size.height()));
+  if (direction == ScrollDirectionPhysical::kScrollDown ||
+      direction == ScrollDirectionPhysical::kScrollUp) {
+    preferred_max_delta.set_x(std::numeric_limits<float>::max());
+  } else {
+    preferred_max_delta.set_y(std::numeric_limits<float>::max());
+  }
+
+  return cc::SnapSelectionStrategy::CreateForPreferredDisplacement(
+      current_position, delta, preferred_min_delta, preferred_max_delta,
+      RuntimeEnabledFeatures::FractionalScrollOffsetsEnabled());
+}
+
 void ScrollableArea::SnapAfterLayout() {
   const cc::SnapContainerData* container_data = GetSnapContainerData();
   if (!container_data || !container_data->size()) {
diff --git a/third_party/blink/renderer/core/scroll/scrollable_area.h b/third_party/blink/renderer/core/scroll/scrollable_area.h
index e407145..3c837ef 100644
--- a/third_party/blink/renderer/core/scroll/scrollable_area.h
+++ b/third_party/blink/renderer/core/scroll/scrollable_area.h
@@ -41,6 +41,7 @@
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/layout/geometry/physical_rect.h"
 #include "third_party/blink/renderer/core/loader/history_item.h"
+#include "third_party/blink/renderer/core/scroll/scroll_types.h"
 #include "third_party/blink/renderer/core/scroll/scrollbar.h"
 #include "third_party/blink/renderer/core/style/scroll_start_data.h"
 #include "third_party/blink/renderer/platform/graphics/compositor_element_id.h"
@@ -183,7 +184,7 @@
   virtual void UpdateFocusDataForSnapAreas() {}
 
   // SnapAtCurrentPosition(), SnapForEndPosition(), SnapForDirection(), and
-  // SnapForEndAndDirection() return true if snapping was performed, and false
+  // SnapForDisplacement() return true if snapping was performed, and false
   // otherwise. Note that this does not necessarily mean that any scrolling was
   // performed as a result e.g., if we are already at the snap point.
   // The scroll callback parameter is used to set the hover state dirty and
@@ -201,10 +202,11 @@
       bool scrolled_x,
       bool scrolled_y,
       base::ScopedClosureRunner on_finish = base::ScopedClosureRunner());
-  bool SnapForDirection(
-      const ScrollOffset& delta,
-      base::ScopedClosureRunner on_finish = base::ScopedClosureRunner());
-  bool SnapForEndAndDirection(const ScrollOffset& delta);
+  bool SnapForDirection(ScrollDirectionPhysical direction);
+  bool SnapForPageScroll(ScrollDirectionPhysical direction);
+  bool SnapForDocumentScroll(ScrollDirectionPhysical direction);
+  std::unique_ptr<cc::SnapSelectionStrategy> PageScrollSnapStrategy(
+      ScrollDirectionPhysical direction) const;
   void SnapAfterLayout();
 
   // Tries to find a target snap position. If found, returns the target
@@ -550,6 +552,8 @@
   void OnScrollFinished(bool enqueue_scrollend);
 
   float ScrollStep(ui::ScrollGranularity, ScrollbarOrientation) const;
+  std::unique_ptr<cc::SnapSelectionStrategy> PageSnap(
+      ScrollbarOrientation) const;
 
   // Injects a gesture scroll event based on the given parameters for mouse
   // events on a scrollbar of this scrollable area.
@@ -696,6 +700,8 @@
   virtual int DocumentStep(ScrollbarOrientation) const;
   virtual float PixelStep(ScrollbarOrientation) const;
 
+  gfx::Size PageSize() const;
+
   // Returns true if a snap point was found.
   bool PerformSnapping(
       const cc::SnapSelectionStrategy& strategy,
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 9bc078d2..8663dd75 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -320,32 +320,25 @@
     },
     {
       name: "AIPromptAPI",
-      // OriginTrialContext::CanEnableTrialFromName limits access to extensions.
-      origin_trial_feature_name: "AIPromptAPIForExtension",
-      origin_trial_allows_third_party: true,
+      public: true,
       status: {
         "Win": "experimental",
         "Mac": "experimental",
         "Linux": "experimental",
         "default": "",
       },
-      base_feature_status: "enabled",
-      copied_from_base_feature_if: "overridden",
-      implied_by: ["AIPromptAPIMultimodalInput", "AIPromptAPIForExtension"],
+      implied_by: ["AIPromptAPIMultimodalInput"],
     },
     {
-      // Extension-specific feature checked in addition to "AIPromptAPI".
+      // Extension access to "AIPromptAPI".
       name: "AIPromptAPIForExtension",
-      origin_trial_feature_name: "AIPromptAPIForExtension",
-      origin_trial_allows_third_party: true,
+      public: true,
       status: {
-        "Win": "experimental",
-        "Mac": "experimental",
-        "Linux": "experimental",
+        "Win": "stable",
+        "Mac": "stable",
+        "Linux": "stable",
         "default": "",
       },
-      base_feature_status: "enabled",
-      copied_from_base_feature_if: "overridden",
     },
     {
       name: "AIPromptAPIForWorkers",
@@ -361,18 +354,10 @@
       },
     },
     {
+      // Gates access to the responseConstraint enhancement for "AIPromptAPI".
+      // This feature alone does not expose any "AIPromptAPI" feature access.
       name: "AIPromptAPIStructuredOutput",
-      origin_trial_feature_name: "AIPromptAPIForExtension",
-      origin_trial_allows_third_party: true,
-      status: {
-        "Win": "experimental",
-        "Mac": "experimental",
-        "Linux": "experimental",
-        "default": "",
-      },
-      base_feature_status: "enabled",
-      copied_from_base_feature_if: "overridden",
-      implied_by: ["AIPromptAPI"],
+      status: "stable",
     },
     {
       name: "AIProofreadingAPI",
@@ -4713,10 +4698,6 @@
       status: "stable",
     },
     {
-      name: "SvgFilterUserSpaceViewportForNonSvg",
-      status: "stable",
-    },
-    {
       name: "SvgInlineRootPixelSnappingScaleAdjustment",
       status: "test",
     },
diff --git a/third_party/blink/renderer/platform/text/character.h b/third_party/blink/renderer/platform/text/character.h
index b74d6f1..508d6fa 100644
--- a/third_party/blink/renderer/platform/text/character.h
+++ b/third_party/blink/renderer/platform/text/character.h
@@ -319,11 +319,13 @@
 }
 
 inline bool Character::MayNeedEastAsianSpacing(UChar32 ch) {
-  // `EastAsianSpacingType::kWide` may need the spacing.
-  // U+2000-206F General Punctuation has rather popular characters, such as ZWSP
-  // and curly quotation marks. Exclude the largest range of non-`kWide` that
-  // include them.
-  return ch >= 0x02C7 && !IsInRange(ch, 0x1200, 0x3004);
+  // `EastAsianSpacingType::kWide` may need the spacing. U+02C7 is the minimum
+  // code point of `kWide`.
+  return ch >= 0x02C7 && ch != kObjectReplacementCharacter &&
+         // U+2000-206F General Punctuation has rather popular characters, such
+         // as ZWSP and curly quotation marks. Exclude the largest range of
+         // non-`kWide` that include them.
+         !IsInRange(ch, 0x1200, 0x3004);
 }
 
 }  // namespace blink
diff --git a/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py b/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
index 0a7e2925..80f68bd3 100755
--- a/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
+++ b/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
@@ -2393,8 +2393,6 @@
         ],
         'allowed': [
             'attribution_reporting::features::.*',
-            # TODO(crbug.com/385173568): Remove after AIPromptAPIForExtension OT.
-            'base::CommandLine',
         ]
     },
     {
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 31d75ea4..60e3e77 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -9330,7 +9330,7 @@
 # Gardener 2025-03-06
 crbug.com/400726001 [ Win ] external/wpt/css/css-ruby/ruby-text-combine-upright-002b.html [ Failure Pass ]
 crbug.com/400726001 [ Mac14 ] external/wpt/css/css-ruby/ruby-text-combine-upright-002b.html [ Failure Pass ]
-crbug.com/395503013 [ Mac ] external/wpt/soft-navigation-heuristics/first-interaction-not-softnav.tentative.html [ Pass Timeout ]
+crbug.com/395503013 [ Mac ] external/wpt/soft-navigation-heuristics/first-interaction-not-softnav.tentative.html [ Failure Pass Timeout ]
 
 # Gardener 2025-03-07
 crbug.com/398165466 [ Mac12 ] http/tests/inspector-protocol/tracing/screenshots.js [ Pass Timeout ]
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-bidirectional-shorthands.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-bidirectional-shorthands.html
index 9ff3815..2ace9f2 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-bidirectional-shorthands.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-bidirectional-shorthands.html
@@ -44,6 +44,11 @@
   test(function() {
     assert_equals(window.getComputedStyle(document.getElementById('target1')).getPropertyValue('rule-style'), 'solid');
   }, "rule-style shorthand computed from longhand values");
+  test(function() {
+    assert_equals(window.getComputedStyle(document.getElementById('target1')).getPropertyValue('rule'), '10px solid rgb(0, 255, 0)');
+  }, "rule shorthand computed from longhand values");
+
+
 
   test(function() {
     assert_equals(window.getComputedStyle(document.getElementById('target2')).getPropertyValue('rule-color'), '');
@@ -54,6 +59,9 @@
   test(function() {
     assert_equals(window.getComputedStyle(document.getElementById('target2')).getPropertyValue('rule-style'), '');
   }, "rule-style shorthand cannot be computed from longhand values so expect an empty string");
+    test(function() {
+    assert_equals(window.getComputedStyle(document.getElementById('target2')).getPropertyValue('rule'), '');
+  }, "rule shorthand cannot be computed from longhand values so expect an empty string");
 </script>
 </body>
 </html>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand-computed.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand-computed.html
index 686560d..63e108d 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand-computed.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand-computed.html
@@ -23,8 +23,7 @@
   }
 </style>
 <script>
-// TODO(samomekarajr): Add `rule` to this test when implemented.
-const properties = ["column-rule", "row-rule"];
+const properties = ["column-rule", "row-rule", "rule"];
 for (let property of properties) {
   const currentcolor = "rgb(0, 255, 0)";
   const mediumWidth = getComputedStyle(document.getElementById('reference')).columnRuleWidth;  // e.g. 3px.
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand-invalid.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand-invalid.html
index b8ba8b0..bbc347c 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand-invalid.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand-invalid.html
@@ -12,8 +12,7 @@
 </head>
 <body>
 <script>
-// TODO(samomekarajr): Add `rule` to this test.
-const properties = ["column-rule", "row-rule"];
+const properties = ["column-rule", "row-rule", "rule"];
 for (let property of properties) {
     test_invalid_value(property, "auto");
 
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand-valid.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand-valid.html
index 9b37ed7b..cc05cf3 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand-valid.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand-valid.html
@@ -13,8 +13,7 @@
 <body>
 <script>
 
-// TODO(samomekarajr): Add `rule` to this test.
-const properties = ["column-rule", "row-rule"];
+const properties = ["column-rule", "row-rule", "rule"];
 for (let property of properties) {
     // <gap-rule> = [<line-width> || <line-style> || <line-color>]
     test_valid_value(property, "5px solid red");
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand.html
index f65ab34..c6c2a37 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/parsing/gap-decorations-rule-shorthand.html
@@ -18,111 +18,177 @@
     'row-rule':    ['row-rule-width',
                     'row-rule-style',
                     'row-rule-color'],
+    'rule': [
+          ['column-rule-width', 'row-rule-width'],
+          ['column-rule-style', 'row-rule-style'],
+          ['column-rule-color', 'row-rule-color'],
+        ]
+
 };
 
-for(rule_property in rule_properties) {
-    const [width, style, color] = rule_properties[rule_property];
+const testCases = [
     // <gap-rule> = [<line-width> || <line-style> || <line-color>].
-    test_shorthand_value(rule_property, '5px solid red', {
-        [width]: '5px',
-        [style]: 'solid',
-        [color]: 'red'
-    });
+    {
+        input: '5px solid red',
+        expected: {
+            width: '5px',
+            style: 'solid',
+            color: 'red'
+        }
+    },
+    {
+        input: 'double',
+        expected: {
+            width: 'medium',
+            style: 'double',
+            color: 'currentcolor'
+        }
+    },
+    {
+        input: 'blue 10px',
+        expected: {
+            width: '10px',
+            style: 'none',
+            color: 'blue'
+        }
+    },
 
-    test_shorthand_value(rule_property, 'double', {
-        [width]: 'medium',
-        [style]: 'double',
-        [color]: 'currentcolor'
-    });
+    // <gap-auto-repeat-rule> = repeat(auto, <gap-rule># ).
+    {
+        input: 'repeat(auto, 5px solid green)',
+        expected: {
+            width: 'repeat(auto, 5px)',
+            style: 'repeat(auto, solid)',
+            color: 'repeat(auto, green)'
+        }
+    },
+    {
+        input: 'repeat(auto, 5px solid yellow, 10px dotted blue)',
+        expected: {
+            width: 'repeat(auto, 5px 10px)',
+            style: 'repeat(auto, solid dotted)',
+            color: 'repeat(auto, yellow blue)'
+        }
+    },
+    {
+        input: 'repeat(auto, blue 6px, 5px solid red)',
+        expected: {
+            width: 'repeat(auto, 6px 5px)',
+            style: 'repeat(auto, none solid)',
+            color: 'repeat(auto, blue red)'
+        }
+    },
 
-    test_shorthand_value(rule_property, 'blue 10px', {
-        [width]: '10px',
-        [style]: 'none',
-        [color]: 'blue'
-    });
-
-    // <gap-auto-repeat-rule> = repeat( auto , <gap-rule># ).
-    test_shorthand_value(rule_property, 'repeat(auto, 5px solid green)', {
-        [width]: 'repeat(auto, 5px)',
-        [style]: 'repeat(auto, solid)',
-        [color]: 'repeat(auto, green)'
-    });
-
-    test_shorthand_value(rule_property, 'repeat(auto, 5px solid yellow, 10px dotted blue)', {
-        [width]: 'repeat(auto, 5px 10px)',
-        [style]: 'repeat(auto, solid dotted)',
-        [color]: 'repeat(auto, yellow blue)'
-    });
-
-    test_shorthand_value(rule_property, 'repeat(auto, blue 6px, 5px solid red)', {
-        [width]: 'repeat(auto, 6px 5px)',
-        [style]: 'repeat(auto, none solid)',
-        [color]: 'repeat(auto, blue red)'
-    });
-
-    // <gap-repeat-rule> = repeat( <integer [1,∞]> , <gap-rule># ).
-    test_shorthand_value(rule_property, 'repeat(4, 15px dotted pink)', {
-        [width]: 'repeat(4, 15px)',
-        [style]: 'repeat(4, dotted)',
-        [color]: 'repeat(4, pink)'
-    });
-    test_shorthand_value(rule_property, 'repeat(1, 15px ridge yellow, 10px dotted blue, 15px double green)', {
-        [width]: 'repeat(1, 15px 10px 15px)',
-        [style]: 'repeat(1, ridge dotted double)',
-        [color]: 'repeat(1, yellow blue green)'
-    });
-    test_shorthand_value(rule_property, 'repeat(3, lime 16px, dashed purple, 10px dotted)', {
-        [width]: 'repeat(3, 16px medium 10px)',
-        [style]: 'repeat(3, none dashed dotted)',
-        [color]: 'repeat(3, lime purple currentcolor)'
-    });
+    // <gap-repeat-rule> = repeat(<integer [1,∞]>, <gap-rule># ).
+    {
+        input: 'repeat(4, 15px dotted pink)',
+        expected: {
+            width: 'repeat(4, 15px)',
+            style: 'repeat(4, dotted)',
+            color: 'repeat(4, pink)'
+        }
+    },
+    {
+        input: 'repeat(1, 15px ridge yellow, 10px dotted blue, 15px double green)',
+        expected: {
+            width: 'repeat(1, 15px 10px 15px)',
+            style: 'repeat(1, ridge dotted double)',
+            color: 'repeat(1, yellow blue green)'
+        }
+    },
+    {
+        input: 'repeat(3, lime 16px, dashed purple, 10px dotted)',
+        expected: {
+            width: 'repeat(3, 16px medium 10px)',
+            style: 'repeat(3, none dashed dotted)',
+            color: 'repeat(3, lime purple currentcolor)'
+        }
+    },
 
     // <gap-rule-list> = <gap-rule-or-repeat>#.
     // <gap-rule-or-repeat> = <gap-rule> | <gap-repeat-rule>.
-    test_shorthand_value(rule_property, 'thin, dashed, hotpink', {
-        [width]: 'thin medium medium',
-        [style]: 'none dashed none',
-        [color]: 'currentcolor currentcolor hotpink'
-    });
-    test_shorthand_value(rule_property, '5px double salmon, repeat(4, 5px ridge red)', {
-        [width]: '5px repeat(4, 5px)',
-        [style]: 'double repeat(4, ridge)',
-        [color]: 'salmon repeat(4, red)'
-    });
-    test_shorthand_value(rule_property,
-    'repeat(2, dashed gray, 10px blue dotted, 20px double), 5px solid red, repeat(4, blue 6px, 5px solid white)', {
-        [width]: 'repeat(2, medium 10px 20px) 5px repeat(4, 6px 5px)',
-        [style]: 'repeat(2, dashed dotted double) solid repeat(4, none solid)',
-        [color]: 'repeat(2, gray blue currentcolor) red repeat(4, blue white)'
-    });
-    test_shorthand_value(rule_property, 'repeat(4, thick hidden skyblue), repeat(3, 5px solid red, 10px dotted)', {
-        [width]: 'repeat(4, thick) repeat(3, 5px 10px)',
-        [style]: 'repeat(4, hidden) repeat(3, solid dotted)',
-        [color]: 'repeat(4, skyblue) repeat(3, red currentcolor)'
-    });
+    {
+        input: 'thin, dashed, hotpink',
+        expected: {
+            width: 'thin medium medium',
+            style: 'none dashed none',
+            color: 'currentcolor currentcolor hotpink'
+        }
+    },
+    {
+        input: '5px double salmon, repeat(4, 5px ridge red)',
+        expected: {
+            width: '5px repeat(4, 5px)',
+            style: 'double repeat(4, ridge)',
+            color: 'salmon repeat(4, red)'
+        }
+    },
+    {
+        input: 'repeat(2, dashed gray, 10px blue dotted, 20px double), 5px solid red, repeat(4, blue 6px, 5px solid white)',
+        expected: {
+            width: 'repeat(2, medium 10px 20px) 5px repeat(4, 6px 5px)',
+            style: 'repeat(2, dashed dotted double) solid repeat(4, none solid)',
+            color: 'repeat(2, gray blue currentcolor) red repeat(4, blue white)'
+        }
+    },
+    {
+        input: 'repeat(4, thick hidden skyblue), repeat(3, 5px solid red, 10px dotted)',
+        expected: {
+            width: 'repeat(4, thick) repeat(3, 5px 10px)',
+            style: 'repeat(4, hidden) repeat(3, solid dotted)',
+            color: 'repeat(4, skyblue) repeat(3, red currentcolor)'
+        }
+    },
 
     // <gap-auto-rule-list>   = <gap-rule-or-repeat>#? ,
     //                          <gap-auto-repeat-rule> ,
     //                          <gap-rule-or-repeat>#?.
-    test_shorthand_value(rule_property,
-    'repeat(auto, 10px solid red), medium dotted green, repeat(3, thick dashed blue, 15px double green)', {
-        [width]: 'repeat(auto, 10px) medium repeat(3, thick 15px)',
-        [style]: 'repeat(auto, solid) dotted repeat(3, dashed double)',
-        [color]: 'repeat(auto, red) green repeat(3, blue green)'
-    });
+    {
+        input: 'repeat(auto, 10px solid red), medium dotted green, repeat(3, thick dashed blue, 15px double green)',
+        expected: {
+            width: 'repeat(auto, 10px) medium repeat(3, thick 15px)',
+            style: 'repeat(auto, solid) dotted repeat(3, dashed double)',
+            color: 'repeat(auto, red) green repeat(3, blue green)'
+        }
+    },
+    {
+        input: 'ridge red, repeat(auto, 5px solid green), 10px dotted blue',
+        expected: {
+            width: 'medium repeat(auto, 5px) 10px',
+            style: 'ridge repeat(auto, solid) dotted',
+            color: 'red repeat(auto, green) blue'
+        }
+    },
+    {
+        input: '10px dotted salmon, repeat(4, thin blue, hidden 5px purple), repeat(auto, 5px solid red, teal)',
+        expected: {
+            width: '10px repeat(4, thin 5px) repeat(auto, 5px medium)',
+            style: 'dotted repeat(4, none hidden) repeat(auto, solid none)',
+            color: 'salmon repeat(4, blue purple) repeat(auto, red teal)'
+        }
+    } ];
 
-    test_shorthand_value(rule_property, 'ridge red, repeat(auto, 5px solid green), 10px dotted blue', {
-        [width]: 'medium repeat(auto, 5px) 10px',
-        [style]: 'ridge repeat(auto, solid) dotted',
-        [color]: 'red repeat(auto, green) blue'
-    });
+for(rule_property in rule_properties) {
+    const [width, style, color] = rule_properties[rule_property];
 
-    test_shorthand_value(rule_property,
-    '10px dotted salmon, repeat(4, thin blue, hidden 5px purple), repeat(auto, 5px solid red, teal)', {
-        [width]: '10px repeat(4, thin 5px) repeat(auto, 5px medium)',
-        [style]: 'dotted repeat(4, none hidden) repeat(auto, solid none)',
-        [color]: 'salmon repeat(4, blue purple) repeat(auto, red teal)'
-    });
+    for (const { input, expected } of testCases) {
+        if (rule_property === 'rule') {
+            test_shorthand_value(rule_property, input, {
+                [width[0]]: expected.width,
+                [width[1]]: expected.width,
+                [style[0]]: expected.style,
+                [style[1]]: expected.style,
+                [color[0]]: expected.color,
+                [color[1]]: expected.color
+            });
+        } else {
+            test_shorthand_value(rule_property, input, {
+                [width]: expected.width,
+                [style]: expected.style,
+                [color]: expected.color
+            });
+        }
+    }
 }
 </script>
 </body>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-scroll-snap/input/paged.html b/third_party/blink/web_tests/external/wpt/css/css-scroll-snap/input/paged.html
new file mode 100644
index 0000000..ef55259
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-scroll-snap/input/paged.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+<title>Page scroll snapping</title>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<meta name="flags" content="should">
+<meta name="assert"
+      content="Test passes if page operation doesn't skip content">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/dom/events/scrolling/scroll_support.js"></script>
+<script src="../support/common.js"></script>
+
+<style>
+html, body {
+  margin: 0;
+}
+.scroller {
+  height: 100vh;
+  overflow: auto;
+  position: relative;
+  scroll-snap-type: y mandatory;
+  counter-reset: --page;
+}
+
+.gap {
+  height: 100vh;
+}
+
+.page {
+  counter-increment: --page;
+  height: 90vh;
+  scroll-snap-align: center;
+  padding: 8px;
+  position: relative;
+  --page: counter(--page);
+}
+.short {
+  height: 25vh;
+}
+.page > div::before {
+  content: "Page " counter(--page);
+  font-size: 1.5em;
+}
+.page > div {
+  box-sizing: border-box;
+  border: 3px solid black;
+  border-radius: 5px;
+  overflow: clip; /* Make sure font size doesn't cause pages to be larger than expected. */
+  padding: 8px;
+  height: 100%;
+}
+</style>
+<div class="scroller" tabindex="0">
+  <div class="page">
+    <div>
+      <p>This tests what happens when you perform a paging scroll (e.g. space bar or page down key) with mandatory scroll snap.</p>
+      <p>When snapped to this page, pressing page down should not skip page 2.</p>
+    </div>
+  </div>
+  <div class="short page">
+    <div>
+      <p>This page should not be skipped by paging scroll operations.</p>
+    </div>
+  </div>
+  <div class="page">
+    <div>
+      <p>We must stop at this page before going to page 4.</p>
+    </div>
+  </div>
+  <div class="short page">
+    <div>
+      <p>Pages 4, 5, and 6 should be a single snap stop on page 5.</p>
+    </div>
+  </div>
+  <div class="short page">
+    <div>
+      <p>
+        This should be the snapped page when paging.
+        The next page operation should jump to page 7.
+      </p>
+    </div>
+  </div>
+  <div class="short page">
+    <div></div>
+  </div>
+  <div class="page">
+    <div>
+      <p>
+        The next page is further than a page away,
+        but there are no closer snap points
+        so it should be scrolled to next.
+      </p>
+    </div>
+  </div>
+  <div class="gap"></div>
+  <div class="page">
+    <div>
+      <p>
+        The last page
+      </p>
+    </div>
+  </div>
+</div>
+
+<script>
+const scroller = document.querySelector(".scroller");
+
+scrollTop = () => scroller.scrollTop;
+
+async function snapTo(page) {
+  if (page == 1 && scroller.scrollTop == 0)
+    return;
+  let scrollEndPromise = waitForScrollEndFallbackToDelayWithoutScrollEvent(scroller);
+  scroller.scrollTop = 0;
+  await scrollEndPromise;
+  if (page > 1) {
+    scrollEndPromise = waitForScrollEndFallbackToDelayWithoutScrollEvent(scroller);
+    scroller.querySelector(`.page[data-page="${page}"]`).scrollIntoView({block: "center"});
+    await scrollEndPromise;
+  }
+}
+
+scroller.querySelectorAll('.page').forEach((div, index) => {
+  div.setAttribute("data-page", index + 1);
+});
+function visiblePages() {
+  return Array.prototype.slice.apply(
+    scroller.querySelectorAll('.page')).filter(
+        div => div.offsetTop >= scroller.scrollTop &&
+        div.offsetTop + div.offsetHeight <= scroller.scrollTop + scroller.clientHeight).map(
+            div => parseInt(div.getAttribute("data-page")));
+}
+
+async function pageDown() {
+  const scrollEndPromise = waitForScrollEndFallbackToDelayWithoutScrollEvent(scroller);
+  await keyPress(scroller, "Space");
+  await scrollEndPromise;
+}
+
+promise_test(async t => {
+  await snapTo(1);
+  assert_array_equals(visiblePages(), [1]);
+  await pageDown();
+  assert_array_equals(visiblePages(), [2]);
+}, `Doesn't skip past small snappable content`);
+
+promise_test(async t => {
+  await snapTo(2);
+  assert_array_equals(visiblePages(), [2]);
+  await pageDown();
+  assert_array_equals(visiblePages(), [3]);
+}, `Doesn't skip past large snappable content`);
+
+promise_test(async t => {
+  await snapTo(3);
+  assert_array_equals(visiblePages(), [3]);
+  await pageDown();
+  assert_array_equals(visiblePages(), [4, 5, 6]);
+}, `Scrolls multiple smaller items into view`);
+
+promise_test(async t => {
+  await snapTo(5);
+  assert_array_equals(visiblePages(), [4, 5, 6]);
+  await pageDown();
+  assert_array_equals(visiblePages(), [7]);
+}, `Scrolls past items currently in view`);
+
+promise_test(async t => {
+  await snapTo(7);
+  assert_array_equals(visiblePages(), [7]);
+  await pageDown();
+  assert_array_equals(visiblePages(), [8]);
+}, `Scrolls more than a page if necessary`);
+
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-text/text-autospace/text-autospace-mixed-001-ref.html b/third_party/blink/web_tests/external/wpt/css/css-text/text-autospace/text-autospace-mixed-001-ref.html
new file mode 100644
index 0000000..be782ab
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-text/text-autospace/text-autospace-mixed-001-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-text-4/#text-autospace-property">
+<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
+<style>
+#container > div {
+  font-family: Ahem;
+  font-size: 40px;
+  text-autospace: no-autospace;
+
+  & > span {
+    margin-left: calc(1em / 8);
+    margin-right: calc(1em / 8);
+  }
+}
+</style>
+<div id="container">
+  <div>国国<span>&#x5d0;&#x5d1;&#x5d2;</span>国国</div>
+  <div>国国<span>&#x5d0;&#x5d1;&#x5d2;</span>国国</div>
+  <div>国国<span>&#1605;&#1615;&#1585;&#1614;&#1576;&#1614;&#1617;&#1593;&#1618;</span>国国</div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-text/text-autospace/text-autospace-mixed-001.html b/third_party/blink/web_tests/external/wpt/css/css-text/text-autospace/text-autospace-mixed-001.html
index 731f7456..9e35ce0 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-text/text-autospace/text-autospace-mixed-001.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-text/text-autospace/text-autospace-mixed-001.html
@@ -1,47 +1,16 @@
 <!DOCTYPE html>
 <meta charset="utf-8">
 <link rel="help" href="https://drafts.csswg.org/css-text-4/#text-autospace-property">
+<link rel="match" href="text-autospace-mixed-001-ref.html">
 <link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../support/get-char-advances.js"></script>
 <style>
-.test {
+#container > div {
   font-family: Ahem;
   font-size: 40px;
 }
-.no-autospace {
-  text-autospace: no-autospace;
-}
 </style>
 <div id="container">
-  <div class="test" expect="1,5">国国&#x5d0;&#x5d1;&#x5d2;国国</div>
-  <div class="test" expect="1,5">国国<span>&#x5d0;&#x5d1;</span>&#x5d2;国国</div>
-  <div class="test" expect="1,11">国国&#1605;&#1615;&#1585;&#1614;&#1576;&#1614;&#1617;&#1593;&#1618;国国</div>
+  <div>国国&#x5d0;&#x5d1;&#x5d2;国国</div>
+  <div>国国<span>&#x5d0;&#x5d1;</span>&#x5d2;国国</div>
+  <div>国国&#1605;&#1615;&#1585;&#1614;&#1576;&#1614;&#1617;&#1593;&#1618;国国</div>
 </div>
-<script>
-// Compute expected advances from advances without `text-autospace` and the
-// `expect` attribute.
-const container = document.getElementById('container');
-container.classList.add('no-autospace');
-const tests = [];
-for (const element of document.getElementsByClassName('test')) {
-  const em = parseFloat(getComputedStyle(element).fontSize);
-  const spacing = em / 8;
-  const advances = getCharAdvances(element);
-  const expect = element.getAttribute('expect').split(',').map(i => parseInt(i));
-  for (const i of expect) {
-    advances[i] += spacing;
-  }
-  tests.push({element: element, advances: advances});
-}
-
-// Apply `text-autospace` and compare the actual advances.
-container.classList.remove('no-autospace');
-for (const t of tests) {
-  const advances = getCharAdvances(t.element);
-  test(() => {
-    assert_array_equals(advances, t.advances);
-  })
-}
-</script>
diff --git a/third_party/blink/web_tests/external/wpt/lint.ignore b/third_party/blink/web_tests/external/wpt/lint.ignore
index b391808..10b5dad 100644
--- a/third_party/blink/web_tests/external/wpt/lint.ignore
+++ b/third_party/blink/web_tests/external/wpt/lint.ignore
@@ -263,6 +263,7 @@
 SET TIMEOUT: reporting/resources/first-csp-report.https.sub.html
 SET TIMEOUT: reporting/resources/second-csp-report.https.sub.html
 SET TIMEOUT: scheduler/tentative/yield/yield-inherit-across-promises.any.js
+SET TIMEOUT: scheduler/tentative/yield/yield-scheduling-state-cleared.any.js
 SET TIMEOUT: scheduler/tentative/yield/yield-priority-timers.any.js
 SET TIMEOUT: secure-contexts/basic-popup-and-iframe-tests.https.js
 SET TIMEOUT: service-workers/cache-storage/cache-abort.https.any.js
diff --git a/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-scheduling-state-cleared.any.js b/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-scheduling-state-cleared.any.js
new file mode 100644
index 0000000..8e6d2ee
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-scheduling-state-cleared.any.js
@@ -0,0 +1,21 @@
+'use strict';
+
+promise_test(async t => {
+  const ids = [];
+  // The timer task will run after the background task, and the scheduling state
+  // set in the background task should not leak to the timer task.
+  const {promise, resolve} = Promise.withResolvers();
+  scheduler.postTask(async () => {
+    setTimeout(async () => {
+      let task = scheduler.postTask(() => {
+        ids.push('task');
+      }, {priority: 'user-visible'});
+      await scheduler.yield();
+      ids.push('continuation');
+      await task;
+      resolve();
+    });
+  }, {priority: 'background'});
+  await promise;
+  assert_equals(ids.toString(), 'continuation,task');
+}, 'yield() does not leak priority across tasks');
diff --git a/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/abs.https.any.js b/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/abs.https.any.js
index de6a576..ea4370c 100644
--- a/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/abs.https.any.js
+++ b/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/abs.https.any.js
@@ -591,6 +591,34 @@
         }
       }
     }
+  },
+
+  // int64 tests
+  {
+    'name': 'abs int64 4D tensor',
+    'graph': {
+      'inputs': {
+        'absInput': {
+          'data': [
+            // int64 range: [/* -(2**63) */ –9223372036854775808,
+            //               /* 2**63 - 1 */ 92233720368547758087]
+            BigInt(-(2**63)) + 1n, -100n, 0n, 100n, BigInt(2**63) - 1n
+          ],
+          'descriptor': {shape: [1, 1, 1, 5], dataType: 'int64'}
+        }
+      },
+      'operators': [{
+        'name': 'abs',
+        'arguments': [{'input': 'absInput'}],
+        'outputs': 'absOutput'
+      }],
+      'expectedOutputs': {
+        'absOutput': {
+          'data': [BigInt(2**63) - 1n, 100n, 0n, 100n, BigInt(2**63) - 1n],
+          'descriptor': {shape: [1, 1, 1, 5], dataType: 'int64'}
+        }
+      }
+    }
   }
 ];
 
diff --git a/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/neg.https.any.js b/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/neg.https.any.js
index 8bc1047..5c5045d3 100644
--- a/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/neg.https.any.js
+++ b/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/neg.https.any.js
@@ -604,6 +604,34 @@
         }
       }
     }
+  },
+
+  // int64 tests
+  {
+    'name': 'neg int64 4D tensor',
+    'graph': {
+      'inputs': {
+        'negInput': {
+          'data': [
+            // int64 range: [/* -(2**63) */ –9223372036854775808,
+            //               /* 2**63 - 1 */ 92233720368547758087]
+            BigInt(-(2**63)) + 1n, -100n, 0n, 100n, BigInt(2**63) - 1n
+          ],
+          'descriptor': {shape: [1, 1, 1, 5], dataType: 'int64'}
+        }
+      },
+      'operators': [{
+        'name': 'neg',
+        'arguments': [{'input': 'negInput'}],
+        'outputs': 'negOutput'
+      }],
+      'expectedOutputs': {
+        'negOutput': {
+          'data': [BigInt(2**63) - 1n, 100n, 0, -100n, BigInt(-(2**63)) + 1n],
+          'descriptor': {shape: [1, 1, 1, 5], dataType: 'int64'}
+        }
+      }
+    }
   }
 ];
 
diff --git a/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/prelu.https.any.js b/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/prelu.https.any.js
index cfd043c..cc6e0052 100644
--- a/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/prelu.https.any.js
+++ b/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/prelu.https.any.js
@@ -1207,6 +1207,36 @@
         }
       }
     }
+  },
+
+  // int64 tests
+  {
+    'name': 'prelu int64 2D constant tensors',
+    'graph': {
+      'inputs': {
+        'preluInput': {
+          'data': [-4, -2, -1, 0, 0, 0, 1, 2, 4],
+          'descriptor': {shape: [3, 3], dataType: 'int64'},
+          'constant': true
+        },
+        'preluSlope': {
+          'data': [-5, 0, 5, -5, 0, 5, -5, 0, 5],
+          'descriptor': {shape: [3, 3], dataType: 'int64'},
+          'constant': true
+        }
+      },
+      'operators': [{
+        'name': 'prelu',
+        'arguments': [{'input': 'preluInput'}, {'slope': 'preluSlope'}],
+        'outputs': 'preluOutput'
+      }],
+      'expectedOutputs': {
+        'preluOutput': {
+          'data': [20, 0, -5, 0, 0, 0, 1, 2, 4],
+          'descriptor': {shape: [3, 3], dataType: 'int64'}
+        }
+      }
+    }
   }
 ];
 
diff --git a/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/relu.https.any.js b/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/relu.https.any.js
index 63ef9fa..05e9e32 100644
--- a/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/relu.https.any.js
+++ b/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/relu.https.any.js
@@ -636,6 +636,34 @@
       }
     }
   },
+
+  // int64 tests
+  {
+    'name': 'relu int64 4D tensor',
+    'graph': {
+      'inputs': {
+        'reluInput': {
+          'data': [
+            // int64 range: [/* -(2**63) */ –9223372036854775808,
+            //               /* 2**63 - 1 */ 92233720368547758087]
+            BigInt(-(2**63)) + 1n, -100n, 0n, 100n, BigInt(2**63) - 1n
+          ],
+          'descriptor': {shape: [1, 1, 1, 5], dataType: 'int64'}
+        }
+      },
+      'operators': [{
+        'name': 'relu',
+        'arguments': [{'input': 'reluInput'}],
+        'outputs': 'reluOutput'
+      }],
+      'expectedOutputs': {
+        'reluOutput': {
+          'data': [0n, 0n, 0n, 100n, BigInt(2**63) - 1n],
+          'descriptor': {shape: [1, 1, 1, 5], dataType: 'int64'}
+        }
+      }
+    }
+  }
 ];
 
 if (navigator.ml) {
diff --git a/third_party/blink/web_tests/fast/scroll-snap/snaps-for-different-key-granularity.html b/third_party/blink/web_tests/fast/scroll-snap/snaps-for-different-key-granularity.html
index 43a4d4a..e2818587 100644
--- a/third_party/blink/web_tests/fast/scroll-snap/snaps-for-different-key-granularity.html
+++ b/third_party/blink/web_tests/fast/scroll-snap/snaps-for-different-key-granularity.html
@@ -72,45 +72,71 @@
   })
 }
 
+async function scrollTo(x, y) {
+  if (scroller.scrollLeft == x && scroller.scrollTop == y)
+    return;
+  const scrollend = scrollEndPromise();
+  scroller.scrollTo(x, y);
+  await scrollend;
+}
+
+function scrollEndPromise() {
+  return new Promise((resolve) => {
+    scroller.addEventListener('scrollend', resolve, {once: true});
+  });
+}
+
 promise_test (async () => {
   await mouseClickOn(10, 10);
-  scroller.scrollTo(0, 0);
+  await scrollTo(0, 0);
+  const scrollEnd = scrollEndPromise();
   await keyPress("ArrowDown");
-  await waitForScrollEnd(scroller, scrollTop, 200);
+  await scrollEnd;
+  assert_approx_equals(scroller.scrollTop, 200, 1);
 }, "Snaps to page1-line1 after pressing ArrowDown at page1.");
 
 promise_test (async () => {
   await mouseClickOn(10, 10);
-  scroller.scrollTo(0, 1200);
+  await scrollTo(0, 1200);
+  const scrollEnd = scrollEndPromise();
   await keyPress("ArrowUp");
-  await waitForScrollEnd(scroller, scrollTop, 1000);
+  await scrollEnd;
+  assert_approx_equals(scroller.scrollTop, 1000, 1);
 }, "Snaps to page2-line2 after pressing ArrowUp at page3.");
 
 promise_test (async () => {
   await mouseClickOn(10, 10);
-  scroller.scrollTo(0, 0);
+  await scrollTo(0, 0);
+  const scrollEnd = scrollEndPromise();
   await keyPress("PageDown");
-  await waitForScrollEnd(scroller, scrollTop, 600);
+  await scrollEnd;
+  assert_approx_equals(scroller.scrollTop, 600, 1);
 }, "Snaps to page2 after pressing PageDown at page1.");
 
 promise_test (async () => {
   await mouseClickOn(10, 10);
-  scroller.scrollTo(0, 1200);
+  await scrollTo(0, 1200);
+  const scrollEnd = scrollEndPromise();
   await keyPress("PageUp");
-  await waitForScrollEnd(scroller, scrollTop, 600);
-}, "Snaps to page2 after pressing PageUp at page3.");
+  await scrollEnd;
+  assert_approx_equals(scroller.scrollTop, 800, 1);
+}, "Snaps to page2-line1 after pressing PageUp at page3.");
 
 promise_test (async () => {
   await mouseClickOn(10, 10);
-  scroller.scrollTo(0, 0);
+  await scrollTo(0, 0);
+  const scrollEnd = scrollEndPromise();
   await keyPress("End");
-  await waitForScrollEnd(scroller, scrollTop, 1200);
+  await scrollEnd;
+  assert_approx_equals(scroller.scrollTop, 1200, 1);
 }, "Snaps to page3 after pressing End at page1.");
 
 promise_test (async () => {
   await mouseClickOn(10, 10);
-  scroller.scrollTo(0, 1200);
+  await scrollTo(0, 1200);
+  const scrollEnd = scrollEndPromise();
   await keyPress("Home");
-  await waitForScrollEnd(scroller, scrollTop, 0);
+  await scrollEnd;
+  assert_approx_equals(scroller.scrollTop, 0, 1);
 }, "Snaps to page1 after pressing Home at page3.");
 </script>
diff --git a/third_party/blink/web_tests/platform/mac-mac14-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/neg.https.any_npu-expected.txt b/third_party/blink/web_tests/platform/mac-mac14-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/neg.https.any_npu-expected.txt
deleted file mode 100644
index 02ae569c..0000000
--- a/third_party/blink/web_tests/platform/mac-mac14-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/neg.https.any_npu-expected.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-This is a testharness.js-based test.
-[FAIL] neg int8 4D tensor
-  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
-[FAIL] neg int32 4D tensor
-  assert_less_than_equal: assert_array_approx_equals_ulp: test neg int32 actual -2147483648 should be close enough to expected -2147483646 by ULP distance: expected a number less than or equal to 0 but got 2
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/platform/mac-mac14-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/neg.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/mac-mac14-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/neg.https.any_gpu-expected.txt
deleted file mode 100644
index cca069b2..0000000
--- a/third_party/blink/web_tests/platform/mac-mac14-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/neg.https.any_gpu-expected.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-This is a testharness.js-based test.
-[FAIL] neg int8 4D tensor
-  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
-[FAIL] neg int32 4D tensor
-  assert_less_than_equal: assert_array_approx_equals_ulp: test neg int32 actual -2147483648 should be close enough to expected -2147483646 by ULP distance: expected a number less than or equal to 0n but got 2n
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/abs.https.any_cpu-expected.txt b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/abs.https.any_cpu-expected.txt
index 364ab1b..b55a7895 100644
--- a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/abs.https.any_cpu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/abs.https.any_cpu-expected.txt
@@ -3,5 +3,7 @@
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
 [FAIL] abs int32 4D tensor
   assert_less_than_equal: assert_array_approx_equals_ulp: test abs int32 actual 2147483647 should be close enough to expected 2147483646 by ULP distance: expected a number less than or equal to 0 but got 1
+[FAIL] abs int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/prelu.https.any_cpu-expected.txt b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/prelu.https.any_cpu-expected.txt
index 5b37deb..8fc1924 100644
--- a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/prelu.https.any_cpu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/prelu.https.any_cpu-expected.txt
@@ -1,4 +1,5 @@
 This is a testharness.js-based test.
-All subtests passed and are omitted for brevity.
-See https://chromium.googlesource.com/chromium/src/+/HEAD/docs/testing/writing_web_tests.md#Text-Test-Baselines for details.
+[FAIL] prelu int64 2D constant tensors
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, constant 'input' data type int64 must be one of [float32,float16,int32,uint32,int8,uint8,int4,uint4]."
 Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/relu.https.any_cpu-expected.txt b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/relu.https.any_cpu-expected.txt
index b059e7c..57bc4c8 100644
--- a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/relu.https.any_cpu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/relu.https.any_cpu-expected.txt
@@ -3,5 +3,7 @@
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
 [FAIL] relu int32 4D tensor
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int32 must be one of [float32,float16]."
+[FAIL] relu int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/abs.https.any_npu-expected.txt b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/abs.https.any_npu-expected.txt
index 364ab1b..b55a7895 100644
--- a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/abs.https.any_npu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/abs.https.any_npu-expected.txt
@@ -3,5 +3,7 @@
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
 [FAIL] abs int32 4D tensor
   assert_less_than_equal: assert_array_approx_equals_ulp: test abs int32 actual 2147483647 should be close enough to expected 2147483646 by ULP distance: expected a number less than or equal to 0 but got 1
+[FAIL] abs int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/prelu.https.any_npu-expected.txt b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/prelu.https.any_npu-expected.txt
index 5b37deb..8fc1924 100644
--- a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/prelu.https.any_npu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/prelu.https.any_npu-expected.txt
@@ -1,4 +1,5 @@
 This is a testharness.js-based test.
-All subtests passed and are omitted for brevity.
-See https://chromium.googlesource.com/chromium/src/+/HEAD/docs/testing/writing_web_tests.md#Text-Test-Baselines for details.
+[FAIL] prelu int64 2D constant tensors
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, constant 'input' data type int64 must be one of [float32,float16,int32,uint32,int8,uint8,int4,uint4]."
 Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/relu.https.any_npu-expected.txt b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/relu.https.any_npu-expected.txt
index b059e7c..57bc4c8 100644
--- a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/relu.https.any_npu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/relu.https.any_npu-expected.txt
@@ -3,5 +3,7 @@
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
 [FAIL] relu int32 4D tensor
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int32 must be one of [float32,float16]."
+[FAIL] relu int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/abs.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/abs.https.any_gpu-expected.txt
index 364ab1b..b55a7895 100644
--- a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/abs.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/abs.https.any_gpu-expected.txt
@@ -3,5 +3,7 @@
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
 [FAIL] abs int32 4D tensor
   assert_less_than_equal: assert_array_approx_equals_ulp: test abs int32 actual 2147483647 should be close enough to expected 2147483646 by ULP distance: expected a number less than or equal to 0 but got 1
+[FAIL] abs int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/prelu.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/prelu.https.any_gpu-expected.txt
index 5b37deb..8fc1924 100644
--- a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/prelu.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/prelu.https.any_gpu-expected.txt
@@ -1,4 +1,5 @@
 This is a testharness.js-based test.
-All subtests passed and are omitted for brevity.
-See https://chromium.googlesource.com/chromium/src/+/HEAD/docs/testing/writing_web_tests.md#Text-Test-Baselines for details.
+[FAIL] prelu int64 2D constant tensors
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, constant 'input' data type int64 must be one of [float32,float16,int32,uint32,int8,uint8,int4,uint4]."
 Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/relu.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/relu.https.any_gpu-expected.txt
index b059e7c..57bc4c8 100644
--- a/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/relu.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac-mac15-arm64/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/relu.https.any_gpu-expected.txt
@@ -3,5 +3,7 @@
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
 [FAIL] relu int32 4D tensor
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int32 must be one of [float32,float16]."
+[FAIL] relu int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/abs.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/abs.https.any_gpu-expected.txt
index 9c9b0dc..37f7567 100644
--- a/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/abs.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/abs.https.any_gpu-expected.txt
@@ -1,5 +1,7 @@
 This is a testharness.js-based test.
 [FAIL] abs int8 4D tensor
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
+[FAIL] abs int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/neg.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/neg.https.any_gpu-expected.txt
index 0836101c..d0d0357 100644
--- a/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/neg.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/neg.https.any_gpu-expected.txt
@@ -1,5 +1,7 @@
 This is a testharness.js-based test.
 [FAIL] neg int8 4D tensor
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
+[FAIL] neg int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/prelu.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/prelu.https.any_gpu-expected.txt
index 604abb1b..45c7a67 100644
--- a/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/prelu.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/prelu.https.any_gpu-expected.txt
@@ -11,5 +11,7 @@
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': The input and slope should have the same last dimension."
 [FAIL] prelu float16 broadcast 4D x 4D slope
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': The input and slope should have the same last dimension."
+[FAIL] prelu int64 2D constant tensors
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/platform/win/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/abs.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/win/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/abs.https.any_gpu-expected.txt
new file mode 100644
index 0000000..12e52855
--- /dev/null
+++ b/third_party/blink/web_tests/platform/win/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/abs.https.any_gpu-expected.txt
@@ -0,0 +1,5 @@
+This is a testharness.js-based test.
+[FAIL] abs int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32,int8]."
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/platform/win/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/neg.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/win/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/neg.https.any_gpu-expected.txt
index 11e58aa..dca7842 100644
--- a/third_party/blink/web_tests/platform/win/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/neg.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/platform/win/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/neg.https.any_gpu-expected.txt
@@ -1,5 +1,7 @@
 This is a testharness.js-based test.
 [FAIL] neg int32 4D tensor
   assert_less_than_equal: assert_array_approx_equals_ulp: test neg int32 actual -2147483648 should be close enough to expected -2147483646 by ULP distance: expected a number less than or equal to 0 but got 2
+[FAIL] neg int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32,int8]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/platform/win/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/prelu.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/win/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/prelu.https.any_gpu-expected.txt
new file mode 100644
index 0000000..22edfb9
--- /dev/null
+++ b/third_party/blink/web_tests/platform/win/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/prelu.https.any_gpu-expected.txt
@@ -0,0 +1,5 @@
+This is a testharness.js-based test.
+[FAIL] prelu int64 2D constant tensors
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16]."
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/abs.https.any_cpu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/abs.https.any_cpu-expected.txt
index 9c9b0dc..37f7567 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/abs.https.any_cpu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/abs.https.any_cpu-expected.txt
@@ -1,5 +1,7 @@
 This is a testharness.js-based test.
 [FAIL] abs int8 4D tensor
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
+[FAIL] abs int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/neg.https.any_cpu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/neg.https.any_cpu-expected.txt
index 0836101c..d0d0357 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/neg.https.any_cpu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/neg.https.any_cpu-expected.txt
@@ -1,5 +1,7 @@
 This is a testharness.js-based test.
 [FAIL] neg int8 4D tensor
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
+[FAIL] neg int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/prelu.https.any_cpu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/prelu.https.any_cpu-expected.txt
index 604abb1b..45c7a67 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/prelu.https.any_cpu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/prelu.https.any_cpu-expected.txt
@@ -11,5 +11,7 @@
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': The input and slope should have the same last dimension."
 [FAIL] prelu float16 broadcast 4D x 4D slope
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': The input and slope should have the same last dimension."
+[FAIL] prelu int64 2D constant tensors
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/relu.https.any_cpu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/relu.https.any_cpu-expected.txt
index 3d5939f6..19e2027 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/relu.https.any_cpu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/relu.https.any_cpu-expected.txt
@@ -3,5 +3,7 @@
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16]."
 [FAIL] relu int32 4D tensor
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int32 must be one of [float32,float16]."
+[FAIL] relu int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/abs.https.any_npu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/abs.https.any_npu-expected.txt
index 9c9b0dc..37f7567 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/abs.https.any_npu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/abs.https.any_npu-expected.txt
@@ -1,5 +1,7 @@
 This is a testharness.js-based test.
 [FAIL] abs int8 4D tensor
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
+[FAIL] abs int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/neg.https.any_npu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/neg.https.any_npu-expected.txt
index 0836101c..d0d0357 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/neg.https.any_npu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/neg.https.any_npu-expected.txt
@@ -1,5 +1,7 @@
 This is a testharness.js-based test.
 [FAIL] neg int8 4D tensor
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16,int32]."
+[FAIL] neg int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16,int32]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/prelu.https.any_npu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/prelu.https.any_npu-expected.txt
index 604abb1b..45c7a67 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/prelu.https.any_npu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/prelu.https.any_npu-expected.txt
@@ -11,5 +11,7 @@
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': The input and slope should have the same last dimension."
 [FAIL] prelu float16 broadcast 4D x 4D slope
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': The input and slope should have the same last dimension."
+[FAIL] prelu int64 2D constant tensors
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/relu.https.any_npu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/relu.https.any_npu-expected.txt
index 3d5939f6..19e2027 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/relu.https.any_npu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/relu.https.any_npu-expected.txt
@@ -3,5 +3,7 @@
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16]."
 [FAIL] relu int32 4D tensor
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int32 must be one of [float32,float16]."
+[FAIL] relu int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16]."
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/relu.https.any_gpu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/relu.https.any_gpu-expected.txt
index 3d5939f6..19e2027 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/relu.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/relu.https.any_gpu-expected.txt
@@ -3,5 +3,7 @@
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int8 must be one of [float32,float16]."
 [FAIL] relu int32 4D tensor
   promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int32 must be one of [float32,float16]."
+[FAIL] relu int64 4D tensor
+  promise_test: Unhandled rejection with value: object "TypeError: Unsupported data type, input 'input' data type int64 must be one of [float32,float16]."
 Harness: the test ran to completion.
 
diff --git a/third_party/glslang/src b/third_party/glslang/src
index 32f71d7..9323100 160000
--- a/third_party/glslang/src
+++ b/third_party/glslang/src
@@ -1 +1 @@
-Subproject commit 32f71d72a2643df675684e1795e987ac26e600df
+Subproject commit 93231001597dad1149a5d035af30eda50b9e6b6c
diff --git a/third_party/googletest/src b/third_party/googletest/src
index 16d4f8e..6aa03e6 160000
--- a/third_party/googletest/src
+++ b/third_party/googletest/src
@@ -1 +1 @@
-Subproject commit 16d4f8eff6d7cefca6975d82a53f8fc995a6feb7
+Subproject commit 6aa03e6774f8cb70da277c56efb24b44ce29d8d7
diff --git a/third_party/lens_server_proto/README.chromium b/third_party/lens_server_proto/README.chromium
index d9198677..42fc9233 100644
--- a/third_party/lens_server_proto/README.chromium
+++ b/third_party/lens_server_proto/README.chromium
@@ -1,8 +1,8 @@
 Name: Lens Protos
 Short Name: lens_overlay_proto
 URL: This is the canonical public repository
-Version: 741226731
-Date: 2025-03-27
+Version: 759676130
+Date: 2025-05-16
 License: BSD-3-Clause
 License File: LICENSE
 Shipped: yes
diff --git a/third_party/lens_server_proto/lens_overlay_client_logs.proto b/third_party/lens_server_proto/lens_overlay_client_logs.proto
index 101dd0a..b82333d0 100644
--- a/third_party/lens_server_proto/lens_overlay_client_logs.proto
+++ b/third_party/lens_server_proto/lens_overlay_client_logs.proto
@@ -23,6 +23,9 @@
     OMNIBOX_BUTTON = 4;
     TOOLBAR_BUTTON = 5;
     FIND_IN_PAGE = 6;
+    OMNIBOX_PAGE_ACTION = 7;
+    OMNIBOX_CONTEXTUAL_SUGGESTION = 8;
+    HOMEWORK_ACTION_CHIP = 9;
   }
 
   // The Lens Overlay entry point used to access lens.
diff --git a/third_party/perfetto b/third_party/perfetto
index d15aa8f..fa368a6 160000
--- a/third_party/perfetto
+++ b/third_party/perfetto
@@ -1 +1 @@
-Subproject commit d15aa8fb5bed6a9e04f7f93ebdc3573b6f7a366e
+Subproject commit fa368a66f714203d1232ff62ece403bd2f5e7c10
diff --git a/third_party/skia b/third_party/skia
index 3b2d2d0..18b85ac 160000
--- a/third_party/skia
+++ b/third_party/skia
@@ -1 +1 @@
-Subproject commit 3b2d2d0f73fcc9468a3bced6d6d17a56411a87f4
+Subproject commit 18b85aced9b7b381ce184703bdeeae53a2fbe294
diff --git a/third_party/spirv-tools/src b/third_party/spirv-tools/src
index 736e415..0102146 160000
--- a/third_party/spirv-tools/src
+++ b/third_party/spirv-tools/src
@@ -1 +1 @@
-Subproject commit 736e415ebaa4290d21e42e370db5e933b9dff06d
+Subproject commit 01021466b5e71deaac9054f56082566c782bfd51
diff --git a/third_party/vulkan-deps b/third_party/vulkan-deps
index 12211ed..d8d0687 160000
--- a/third_party/vulkan-deps
+++ b/third_party/vulkan-deps
@@ -1 +1 @@
-Subproject commit 12211edbca712355258047ef4f0de13f1da23ac3
+Subproject commit d8d0687affb24a8069b74b50af9cd9245a4af273
diff --git a/third_party/vulkan-validation-layers/src b/third_party/vulkan-validation-layers/src
index 7760c965..a440433 160000
--- a/third_party/vulkan-validation-layers/src
+++ b/third_party/vulkan-validation-layers/src
@@ -1 +1 @@
-Subproject commit 7760c965ea96a51e7d66433dc9f97b4cac7804f7
+Subproject commit a44043332c975777a7196406f08601f6099b24f0
diff --git a/third_party/webrtc b/third_party/webrtc
index 0f1742e..59d57888 160000
--- a/third_party/webrtc
+++ b/third_party/webrtc
@@ -1 +1 @@
-Subproject commit 0f1742e458d2b187a6bf4470352acb6d7387274e
+Subproject commit 59d578881fea163b8bd63aa056ed38feefd273de
diff --git a/tools/metrics/actions/actions.xml b/tools/metrics/actions/actions.xml
index 61330e4..0fce9a8 100644
--- a/tools/metrics/actions/actions.xml
+++ b/tools/metrics/actions/actions.xml
@@ -48450,7 +48450,8 @@
     The code that caused the issue this action was meant to track is no longer
     present.
   </obsolete>
-  <owner>bialpio@chromium.org</owner>
+  <owner>alcooper@chromium.org</owner>
+  <owner>xr-dev@chromium.org</owner>
   <description>
     Raised when XR service was unable to obtain necessary component. The action
     is not directly raised by the user's behavior as it should only be logged
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index dde00ed..60690a37 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -10093,6 +10093,8 @@
       label="SupervisedUserCommittedInterstitials:disabled"/>
   <int value="-1506664186"
       label="SafeBrowsingGooglePlayProtectPrompt:disabled"/>
+  <int value="-1505988176"
+      label="AutofillEnableMultipleRequestInVirtualCardDownstreamEnrollment:enabled"/>
   <int value="-1505836593" label="ImprovedIncognitoScreenshot:disabled"/>
   <int value="-1505222604" label="ClearIdentityInCanMakePaymentEvent:disabled"/>
   <int value="-1505076171" label="AutomaticUsbDetach:enabled"/>
@@ -18245,6 +18247,8 @@
   <int value="1570178909" label="NewOverviewLayout:enabled"/>
   <int value="1570786077" label="EnableFingerprintingProtectionFilter:enabled"/>
   <int value="1571640975" label="EcheLauncherIconsInMoreAppsButton:enabled"/>
+  <int value="1571706485"
+      label="AutofillEnableMultipleRequestInVirtualCardDownstreamEnrollment:disabled"/>
   <int value="1571998166" label="DetectingHeavyPages:disabled"/>
   <int value="1572371872" label="MostVisitedTilesNewScoring:enabled"/>
   <int value="1572464760" label="pwa-update-dialog-for-name-and-icon:disabled"/>
diff --git a/tools/metrics/histograms/metadata/blink/enums.xml b/tools/metrics/histograms/metadata/blink/enums.xml
index 6989c46..99aaff8 100644
--- a/tools/metrics/histograms/metadata/blink/enums.xml
+++ b/tools/metrics/histograms/metadata/blink/enums.xml
@@ -7808,6 +7808,7 @@
   <int value="855" label="row-rule"/>
   <int value="856" label="text-grow"/>
   <int value="857" label="text-shrink"/>
+  <int value="858" label="rule"/>
 </enum>
 
 <!-- LINT.ThenChange(//third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom:CSSSampleId) -->
diff --git a/tools/metrics/histograms/metadata/gpu/histograms.xml b/tools/metrics/histograms/metadata/gpu/histograms.xml
index b6bd4a0b..ff0d50db 100644
--- a/tools/metrics/histograms/metadata/gpu/histograms.xml
+++ b/tools/metrics/histograms/metadata/gpu/histograms.xml
@@ -1966,7 +1966,7 @@
 
 <histogram name="Viz.FrameSinkVideoCapturer.CaptureDuration" units="ms"
     expires_after="2023-07-15">
-  <owner>bialpio@chromium.org</owner>
+  <owner>jophba@chromium.org</owner>
   <owner>media-capture-dev@chromium.org</owner>
   <summary>
     The time it took from when FrameSinkVideoCapturerImpl sent a request for
@@ -1979,7 +1979,7 @@
 
 <histogram name="Viz.FrameSinkVideoCapturer.CaptureSucceeded"
     enum="BooleanSuccess" expires_after="2023-07-15">
-  <owner>bialpio@chromium.org</owner>
+  <owner>jophba@chromium.org</owner>
   <owner>media-capture-dev@chromium.org</owner>
   <summary>
     Whether a capture initiated by FrameSinkVideoCapturerImpl succeeded.
@@ -1991,7 +1991,7 @@
 
 <histogram name="Viz.FrameSinkVideoCapturer.FrameResurrected" enum="Boolean"
     expires_after="2023-07-15">
-  <owner>bialpio@chromium.org</owner>
+  <owner>jophba@chromium.org</owner>
   <owner>media-capture-dev@chromium.org</owner>
   <summary>
     True if the capturer has used a resurrected video frame, thus avoiding
@@ -2001,7 +2001,7 @@
 
 <histogram name="Viz.FrameSinkVideoCapturer.I420.CaptureDuration" units="ms"
     expires_after="2024-09-29">
-  <owner>bialpio@chromium.org</owner>
+  <owner>jophba@chromium.org</owner>
   <owner>media-capture-dev@chromium.org</owner>
   <summary>
     The time it took from when FrameSinkVideoCapturerImpl sent a request for an
@@ -2015,7 +2015,7 @@
 
 <histogram name="Viz.FrameSinkVideoCapturer.I420.CaptureSucceeded"
     enum="BooleanSuccess" expires_after="2025-05-26">
-  <owner>bialpio@chromium.org</owner>
+  <owner>jophba@chromium.org</owner>
   <owner>media-capture-dev@chromium.org</owner>
   <summary>
     Whether an I420 readback initiated by FrameSinkVideoCapturerImpl succeeded.
@@ -2027,7 +2027,7 @@
 
 <histogram name="Viz.FrameSinkVideoCapturer.I420.TotalDuration" units="ms"
     expires_after="2024-09-29">
-  <owner>bialpio@chromium.org</owner>
+  <owner>jophba@chromium.org</owner>
   <owner>media-capture-dev@chromium.org</owner>
   <summary>
     The time it took from when FrameSinkVideoCapturerImpl decided that a new
@@ -2039,7 +2039,7 @@
 
 <histogram name="Viz.FrameSinkVideoCapturer.NV12.CaptureDuration" units="ms"
     expires_after="2023-07-15">
-  <owner>bialpio@chromium.org</owner>
+  <owner>jophba@chromium.org</owner>
   <owner>media-capture-dev@chromium.org</owner>
   <summary>
     The time it took from when FrameSinkVideoCapturerImpl sent a request for an
@@ -2054,7 +2054,7 @@
 
 <histogram name="Viz.FrameSinkVideoCapturer.NV12.CaptureSucceeded"
     enum="BooleanSuccess" expires_after="2023-07-15">
-  <owner>bialpio@chromium.org</owner>
+  <owner>jophba@chromium.org</owner>
   <owner>media-capture-dev@chromium.org</owner>
   <summary>
     Whether an NV12 capture initiated by FrameSinkVideoCapturerImpl succeeded.
@@ -2063,7 +2063,7 @@
 
 <histogram name="Viz.FrameSinkVideoCapturer.NV12.TotalDuration" units="ms"
     expires_after="2023-07-15">
-  <owner>bialpio@chromium.org</owner>
+  <owner>jophba@chromium.org</owner>
   <owner>media-capture-dev@chromium.org</owner>
   <summary>
     The time it took from when FrameSinkVideoCapturerImpl decided that a new
@@ -2075,7 +2075,7 @@
 
 <histogram name="Viz.FrameSinkVideoCapturer.ReserveFrameDuration" units="ms"
     expires_after="2023-07-15">
-  <owner>bialpio@chromium.org</owner>
+  <owner>jophba@chromium.org</owner>
   <owner>media-capture-dev@chromium.org</owner>
   <summary>
     The time it took for a frame pool to reserve a video frame that would then
@@ -2087,7 +2087,6 @@
 <histogram name="Viz.FrameSinkVideoCapturer.RGBA.CaptureDuration" units="ms"
     expires_after="2023-07-15">
   <owner>jonross@chromium.org</owner>
-  <owner>bialpio@chromium.org</owner>
   <owner>viz-team-wat@google.com</owner>
   <summary>
     The time it took from when FrameSinkVideoCapturerImpl sent a request for an
@@ -2098,7 +2097,7 @@
 
 <histogram name="Viz.FrameSinkVideoCapturer.TotalDuration" units="ms"
     expires_after="2023-07-15">
-  <owner>bialpio@chromium.org</owner>
+  <owner>jophba@chromium.org</owner>
   <owner>media-capture-dev@chromium.org</owner>
   <summary>
     The time it took from when FrameSinkVideoCapturerImpl decided that a new
diff --git a/tools/metrics/histograms/metadata/lens/enums.xml b/tools/metrics/histograms/metadata/lens/enums.xml
index bee07f9..353e558 100644
--- a/tools/metrics/histograms/metadata/lens/enums.xml
+++ b/tools/metrics/histograms/metadata/lens/enums.xml
@@ -107,6 +107,9 @@
   <int value="6" label="LVF Shutter Button"/>
   <int value="7" label="LVF Gallery"/>
   <int value="8" label="Context Menu"/>
+  <int value="9" label="Omnibox Page Action"/>
+  <int value="10" label="Omnibox Contextual Suggestion"/>
+  <int value="11" label="Homework action chip"/>
 </enum>
 
 <!-- LINT.ThenChange(//components/lens/lens_overlay_invocation_source.h:LensOverlayInvocationSource) -->
diff --git a/tools/metrics/histograms/metadata/lens/histograms.xml b/tools/metrics/histograms/metadata/lens/histograms.xml
index 0dade76..7e1b92c0 100644
--- a/tools/metrics/histograms/metadata/lens/histograms.xml
+++ b/tools/metrics/histograms/metadata/lens/histograms.xml
@@ -36,9 +36,12 @@
   <variant name="ContentAreaContextMenuPage"/>
   <variant name="ContextMenu"/>
   <variant name="FindInPage"/>
+  <variant name="HomeworkActionChip"/>
   <variant name="LVFGallery"/>
   <variant name="LVFShutterButton"/>
   <variant name="Omnibox"/>
+  <variant name="OmniboxContextualSuggestion"/>
+  <variant name="OmniboxPageAction"/>
   <variant name="Toolbar"/>
   <variant name="Unknown"/>
 </variants>
diff --git a/tools/metrics/histograms/metadata/media/histograms.xml b/tools/metrics/histograms/metadata/media/histograms.xml
index 4a93dde..3ea80b0d9 100644
--- a/tools/metrics/histograms/metadata/media/histograms.xml
+++ b/tools/metrics/histograms/metadata/media/histograms.xml
@@ -84,6 +84,11 @@
   <variant name="Widevine" summary="Widevine key system"/>
 </variants>
 
+<variants name="KeySystemForHardwareSecureOnly">
+  <variant name="PlayReady" summary="PlayReady key system"/>
+  <variant name="Widevine" summary="Widevine key system"/>
+</variants>
+
 <variants name="KeySystemWithRobustness">
   <variant name="ClearKey" summary="Clear Key key system"/>
   <variant name="PlayReady.HardwareSecure"
@@ -3153,17 +3158,7 @@
   <summary>The time spent to load a library MediaFoundation CDM.</summary>
 </histogram>
 
-<histogram
-    name="Media.EME.MediaFoundationCdm.Widevine.HardwareSecure.FirstInitialize"
-    enum="Hresult" expires_after="2026-01-15">
-  <owner>xhwang@chromium.org</owner>
-  <owner>media-dev-uma@chromium.org</owner>
-  <summary>
-    HRESULT of the first (and only the first) MediaFoundationCdm initialization
-    in a MeidaFoundationService process when using Widevine in hardware secure
-    mode. Reported on both successes and failures.
-  </summary>
-</histogram>
+<!-- Widevine in hardware secure mode only -->
 
 <histogram
     name="Media.EME.MediaFoundationCdm.Widevine.HardwareSecure.Initialize"
@@ -3176,14 +3171,29 @@
   </summary>
 </histogram>
 
-<histogram name="Media.EME.MediaFoundationCdm.Widevine.HardwareSecure.{EmeApi}"
+<histogram
+    name="Media.EME.MediaFoundationCdm.{KeySystem}.HardwareSecure.FirstInitialize"
+    enum="Hresult" expires_after="2026-01-15">
+  <owner>xhwang@chromium.org</owner>
+  <owner>media-dev-uma@chromium.org</owner>
+  <summary>
+    HRESULT of the first (and only the first) MediaFoundationCdm initialization
+    in a MeidaFoundationService process when using {KeySystem} in hardware
+    secure mode. Reported on both successes and failures.
+  </summary>
+  <token key="KeySystem" variants="KeySystemForHardwareSecureOnly"/>
+</histogram>
+
+<histogram
+    name="Media.EME.MediaFoundationCdm.{KeySystem}.HardwareSecure.{EmeApi}"
     enum="Hresult" expires_after="2026-01-15">
   <owner>xhwang@chromium.org</owner>
   <owner>media-dev-uma@chromium.org</owner>
   <summary>
     HRESULT of MediaFoundationCdm and MediaFoundationCdmSession operations when
-    using Widevine in hardware secure mode.
+    using {KeySystem} in hardware secure mode.
   </summary>
+  <token key="KeySystem" variants="KeySystemForHardwareSecureOnly"/>
   <token key="EmeApi" variants="EmeApi"/>
 </histogram>
 
@@ -3250,7 +3260,7 @@
     The time spent to get CdmCapability for {KeySystem}. The value will be
     reported once per CDM instance for hw security in MediaFoundationService.
   </summary>
-  <token key="KeySystem" variants="KeySystem"/>
+  <token key="KeySystem" variants="KeySystemForHardwareSecureOnly"/>
 </histogram>
 
 <histogram
@@ -3265,7 +3275,7 @@
     browser session lifetime). IsKeySystemSupported() calls IsTypeSupported()
     multiple times, for different audio/video codecs and encryption schemes.
   </summary>
-  <token key="KeySystem" variants="KeySystem"/>
+  <token key="KeySystem" variants="KeySystemForHardwareSecureOnly"/>
 </histogram>
 
 <histogram name="Media.EME.MojoCdm.ConnectionError"
@@ -3351,61 +3361,6 @@
 </histogram>
 
 <histogram
-    name="Media.EME.Widevine.CdmCapabilityQueryStatus.CreateDummyMediaFoundationCdmHresult"
-    enum="Hresult" expires_after="2026-01-15">
-  <owner>sangbaekpark@chromium.org</owner>
-  <owner>media-dev-uma@chromium.org</owner>
-  <summary>
-    HRESULT code if CreateDummyMediaFoundationCdm call fails when querying
-    Widevine CDM capability.
-  </summary>
-</histogram>
-
-<histogram
-    name="Media.EME.Widevine.CdmCapabilityQueryStatus.MediaFoundationGetCdmFactoryHresult"
-    enum="Hresult" expires_after="2026-01-15">
-  <owner>sangbaekpark@chromium.org</owner>
-  <owner>media-dev-uma@chromium.org</owner>
-  <summary>
-    HRESULT code if MediaFoundationCdmModule::GetCdmFactory call fails when
-    querying Widevine CDM capability.
-  </summary>
-</histogram>
-
-<histogram name="Media.EME.Widevine.HardwareSecure.AllowedForSite"
-    enum="BooleanEnabled" expires_after="2026-01-15">
-  <owner>feras@chromium.org</owner>
-  <owner>media-dev-uma@chromium.org</owner>
-  <summary>
-    Whether Media Foundation Widevine CDM is enabled or disabled based on
-    previous disabled timestamps in a &quot;User Profile&quot; pref. This is
-    reported once per browsing session when user requests hardware secure
-    playback if Media Foundation Widevine CDM is enabled and available.
-  </summary>
-</histogram>
-
-<histogram name="Media.EME.Widevine.HardwareSecure.CdmCapabilityQueryStatus"
-    enum="CdmCapabilityQueryStatus" expires_after="2026-01-15">
-  <owner>sangbaekpark@chromium.org</owner>
-  <owner>media-dev-uma@chromium.org</owner>
-  <summary>
-    The status of the CDM capability query. This can be used to inspect the
-    reason when no capability reported. Reported at most once per browser
-    session per key system when EME query is triggered by a website.
-  </summary>
-</histogram>
-
-<histogram name="Media.EME.Widevine.HardwareSecure.CdmInfoStatus"
-    enum="CdmInfoStatus" expires_after="2026-01-15">
-  <owner>xhwang@chromium.org</owner>
-  <owner>media-dev-uma@chromium.org</owner>
-  <summary>
-    The CdmInfo::Status of Widevine hardware secure CDM capability. Reported at
-    most once per browser session when EME query is triggered by a website.
-  </summary>
-</histogram>
-
-<histogram
     name="Media.EME.Widevine.HardwareSecure.ClearLeadSupport.{VideoCodec}"
     enum="BooleanSupported" expires_after="2026-01-15">
   <owner>vpasupathy@chromium.org</owner>
@@ -3419,6 +3374,8 @@
   <token key="VideoCodec" variants="VideoCodec"/>
 </histogram>
 
+<!-- Widevine in hardware secure mode only -->
+
 <histogram name="Media.EME.Widevine.HardwareSecure.Pref" enum="BooleanEnabled"
     expires_after="2026-01-15">
   <owner>xhwang@chromium.org</owner>
@@ -3431,31 +3388,6 @@
   </summary>
 </histogram>
 
-<histogram name="Media.EME.Widevine.HardwareSecure.Support"
-    enum="BooleanSupported" expires_after="2026-01-15">
-  <owner>xhwang@chromium.org</owner>
-  <owner>media-dev-uma@chromium.org</owner>
-  <summary>
-    When hardware secure decryption is enabled, whether Widevine hardware secure
-    decryption is actually supported by the platform (device). Reported at most
-    once per browser session when EME query is triggered by a website, and when
-    Widevine hardware secure CDM was registered.
-  </summary>
-</histogram>
-
-<histogram name="Media.EME.Widevine.HardwareSecure.Support.{VideoCodec}"
-    enum="BooleanSupported" expires_after="2026-01-15">
-  <owner>xhwang@chromium.org</owner>
-  <owner>media-dev-uma@chromium.org</owner>
-  <summary>
-    When hardware secure decryption is enabled and supported by Widevine,
-    whether {VideoCodec} video codec is supported by the platform (device).
-    Reported at most once per browser session when EME query is triggered by a
-    website, and when Widevine hardware secure CDM was registered.
-  </summary>
-  <token key="VideoCodec" variants="VideoCodec"/>
-</histogram>
-
 <histogram name="Media.EME.Widevine.SoftwareSecure.SystemCode"
     enum="CdmSystemCode" expires_after="2026-01-15">
   <owner>xhwang@chromium.org</owner>
@@ -3477,6 +3409,30 @@
   </summary>
 </histogram>
 
+<histogram
+    name="Media.EME.{KeySystem}.CdmCapabilityQueryStatus.CreateDummyMediaFoundationCdmHresult"
+    enum="Hresult" expires_after="2026-01-15">
+  <owner>sangbaekpark@chromium.org</owner>
+  <owner>media-dev-uma@chromium.org</owner>
+  <summary>
+    HRESULT code if CreateDummyMediaFoundationCdm call fails when querying
+    hardware secure CDM capability.
+  </summary>
+  <token key="KeySystem" variants="KeySystemForHardwareSecureOnly"/>
+</histogram>
+
+<histogram
+    name="Media.EME.{KeySystem}.CdmCapabilityQueryStatus.MediaFoundationGetCdmFactoryHresult"
+    enum="Hresult" expires_after="2026-01-15">
+  <owner>sangbaekpark@chromium.org</owner>
+  <owner>media-dev-uma@chromium.org</owner>
+  <summary>
+    HRESULT code if MediaFoundationCdmModule::GetCdmFactory call fails when
+    querying Widevine CDM capability.
+  </summary>
+  <token key="KeySystem" variants="KeySystemForHardwareSecureOnly"/>
+</histogram>
+
 <histogram name="Media.EME.{KeySystem}.CreateCdm" enum="BooleanSuccess"
     expires_after="2026-01-15">
   <owner>xhwang@chromium.org</owner>
@@ -3527,6 +3483,69 @@
   <token key="KeySystem" variants="KeySystemWithRobustness"/>
 </histogram>
 
+<histogram name="Media.EME.{KeySystem}.HardwareSecure.AllowedForSite"
+    enum="BooleanEnabled" expires_after="2026-01-15">
+  <owner>feras@chromium.org</owner>
+  <owner>media-dev-uma@chromium.org</owner>
+  <summary>
+    Whether Media Foundation based CDM is enabled or disabled based on previous
+    disabled timestamps in a &quot;User Profile&quot; pref. This is reported
+    once per browsing session when user requests hardware secure playback if
+    Media Foundation based CDM is enabled and available.
+  </summary>
+  <token key="KeySystem" variants="KeySystemForHardwareSecureOnly"/>
+</histogram>
+
+<histogram name="Media.EME.{KeySystem}.HardwareSecure.CdmCapabilityQueryStatus"
+    enum="CdmCapabilityQueryStatus" expires_after="2026-01-15">
+  <owner>sangbaekpark@chromium.org</owner>
+  <owner>media-dev-uma@chromium.org</owner>
+  <summary>
+    The status of the CDM capability query. This can be used to inspect the
+    reason when no capability reported. Reported at most once per browser
+    session per key system when EME query is triggered by a website.
+  </summary>
+  <token key="KeySystem" variants="KeySystemForHardwareSecureOnly"/>
+</histogram>
+
+<histogram name="Media.EME.{KeySystem}.HardwareSecure.CdmInfoStatus"
+    enum="CdmInfoStatus" expires_after="2026-01-15">
+  <owner>xhwang@chromium.org</owner>
+  <owner>media-dev-uma@chromium.org</owner>
+  <summary>
+    The CdmInfo::Status of {KeySystem} hardware secure CDM capability. Reported
+    at most once per browser session when EME query is triggered by a website.
+  </summary>
+  <token key="KeySystem" variants="KeySystemForHardwareSecureOnly"/>
+</histogram>
+
+<histogram name="Media.EME.{KeySystem}.HardwareSecure.Support"
+    enum="BooleanSupported" expires_after="2026-01-15">
+  <owner>xhwang@chromium.org</owner>
+  <owner>media-dev-uma@chromium.org</owner>
+  <summary>
+    When hardware secure decryption is enabled, whether Widevine hardware secure
+    decryption is actually supported by the platform (device). Reported at most
+    once per browser session when EME query is triggered by a website, and when
+    Widevine hardware secure CDM was registered.
+  </summary>
+  <token key="KeySystem" variants="KeySystemForHardwareSecureOnly"/>
+</histogram>
+
+<histogram name="Media.EME.{KeySystem}.HardwareSecure.Support.{VideoCodec}"
+    enum="BooleanSupported" expires_after="2026-01-15">
+  <owner>xhwang@chromium.org</owner>
+  <owner>media-dev-uma@chromium.org</owner>
+  <summary>
+    When hardware secure decryption is enabled and supported by Widevine,
+    whether {VideoCodec} video codec is supported by the platform (device).
+    Reported at most once per browser session when EME query is triggered by a
+    website, and when Widevine hardware secure CDM was registered.
+  </summary>
+  <token key="KeySystem" variants="KeySystemForHardwareSecureOnly"/>
+  <token key="VideoCodec" variants="VideoCodec"/>
+</histogram>
+
 <histogram name="Media.EME.{KeySystem}.InitialKeyStatusMix"
     enum="CdmKeyStatusMix" expires_after="2026-01-15">
   <owner>xhwang@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/page/enums.xml b/tools/metrics/histograms/metadata/page/enums.xml
index 2d50426..3b2ef0a 100644
--- a/tools/metrics/histograms/metadata/page/enums.xml
+++ b/tools/metrics/histograms/metadata/page/enums.xml
@@ -296,6 +296,9 @@
   <int value="29" label="Lens Overlay"/>
   <int value="30" label="Discounts"/>
   <int value="31" label="Optimization Guide"/>
+  <int value="32" label="Collaboration Messaging"/>
+  <int value="33" label="Change Password"/>
+  <int value="34" label="Lens Overlay Homework"/>
 </enum>
 
 <!-- LINT.ThenChange(//chrome/browser/ui/page_action/page_action_icon_type.h:PageActionIconType) -->
diff --git a/tools/metrics/ukm/ukm.xml b/tools/metrics/ukm/ukm.xml
index 95e6e515..8e541b7 100644
--- a/tools/metrics/ukm/ukm.xml
+++ b/tools/metrics/ukm/ukm.xml
@@ -9133,14 +9133,15 @@
   </metric>
   <metric name="CompletionStatus.ErrorCode">
     <summary>
-      An integer representing the error code of the request when it completes or
-      fails. This comes from network::URLLoaderCompletionStatus.error_code.
+      Deprecated as of 2025/05/22. An integer representing the error code of the
+      request when it completes or fails. This comes from
+      network::URLLoaderCompletionStatus.error_code.
     </summary>
   </metric>
   <metric name="CompletionStatus.ExtendedErrorCode">
     <summary>
-      An integer representing the error code of the request when it completes or
-      fails. This comes from
+      Deprecated as of 2025/05/22. An integer representing the error code of the
+      request when it completes or fails. This comes from
       network::URLLoaderCompletionStatus.extended_error_code.
     </summary>
   </metric>
@@ -9167,6 +9168,20 @@
       Whether the Context is detached or not when this event is logged.
     </summary>
   </metric>
+  <metric name="LoaderCompleted.ErrorCode">
+    <summary>
+      An integer representing the error code of the request when it reaches the
+      kLoaderCompleted stage. This comes from
+      network::URLLoaderCompletionStatus.error_code.
+    </summary>
+  </metric>
+  <metric name="LoaderCompleted.ExtendedErrorCode">
+    <summary>
+      An integer representing the extended error code of the request when it
+      reaches the kLoaderCompleted stage. This comes from
+      network::URLLoaderCompletionStatus.extended_error_code.
+    </summary>
+  </metric>
   <metric name="NumRedirects">
     <summary>
       The number of redirects a fetch keepalive request loader has experienced
@@ -9185,6 +9200,20 @@
       this is also set to &quot;LoaderCreated&quot; stage.
     </summary>
   </metric>
+  <metric name="RequestFailed.ErrorCode">
+    <summary>
+      An integer representing the error code of the request when it reaches the
+      kRequestfailed stage. This comes from
+      network::URLLoaderCompletionStatus.error_code.
+    </summary>
+  </metric>
+  <metric name="RequestFailed.ExtendedErrorCode">
+    <summary>
+      An integer representing the extended error code of the request when it
+      reaches the kRequestFailed stage. This comes from
+      network::URLLoaderCompletionStatus.extended_error_code.
+    </summary>
+  </metric>
   <metric name="RequestType" enum="FetchKeepAliveRequestType">
     <summary>
       An enum representing the type of the fetch keepalive request. It tells
@@ -12822,6 +12851,32 @@
       </history>
     </aggregation>
   </metric>
+  <metric name="OmniboxContextualSuggestion">
+    <summary>
+      Time to first interaction when overlay invoked through the omnibox
+      contextual suggestion.
+    </summary>
+    <aggregation>
+      <history>
+        <statistics>
+          <quantiles type="std-percentiles"/>
+        </statistics>
+      </history>
+    </aggregation>
+  </metric>
+  <metric name="OmniboxPageAction">
+    <summary>
+      Time to first interaction when overlay invoked through the omnibox Lens
+      page action suggestion.
+    </summary>
+    <aggregation>
+      <history>
+        <statistics>
+          <quantiles type="std-percentiles"/>
+        </statistics>
+      </history>
+    </aggregation>
+  </metric>
   <metric name="Toolbar">
     <summary>
       Time to first interaction when overlay invoked through the toolbar.
@@ -26811,10 +26866,8 @@
 </event>
 
 <event name="XR.WebXR.Session">
-  <owner>billorr@chromium.org</owner>
-  <owner>bialpio@chromium.org</owner>
+  <owner>alcooper@chromium.org</owner>
   <owner>xr-dev@chromium.org</owner>
-  <owner>cassew@google.com</owner>
   <summary>
     When session ends, records data for a WebXR / WebVR session.
   </summary>
diff --git a/tools/perf/core/perfetto_binary_roller/binary_deps.json b/tools/perf/core/perfetto_binary_roller/binary_deps.json
index 4f25a83..50e8bb4 100644
--- a/tools/perf/core/perfetto_binary_roller/binary_deps.json
+++ b/tools/perf/core/perfetto_binary_roller/binary_deps.json
@@ -5,8 +5,8 @@
             "full_remote_path": "perfetto-luci-artifacts/v50.1/linux-arm64/trace_processor_shell"
         },
         "win": {
-            "hash": "3aa49ee87d544c6dc520b31d415891d73c33c7da",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/d15aa8fb5bed6a9e04f7f93ebdc3573b6f7a366e/trace_processor_shell.exe"
+            "hash": "9e46664964572e7522fecfc6c6790562a652dccb",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/8435ff3dc1847a6e86d7c854a1fab08f875f9cf2/trace_processor_shell.exe"
         },
         "linux_arm": {
             "hash": "99f971ca131f6d11c73f4b918099d434bdd8093c",
@@ -21,8 +21,8 @@
             "full_remote_path": "perfetto-luci-artifacts/v50.1/mac-arm64/trace_processor_shell"
         },
         "linux": {
-            "hash": "9d97f6c8b52c68187a0be9f14c77b086687e859f",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/d15aa8fb5bed6a9e04f7f93ebdc3573b6f7a366e/trace_processor_shell"
+            "hash": "69bfe1bb72ec83b3b83ed45183d49d1a60064f2f",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/fa368a66f714203d1232ff62ece403bd2f5e7c10/trace_processor_shell"
         }
     },
     "power_profile.sql": {
diff --git a/ui/android/overscroll_glow.cc b/ui/android/overscroll_glow.cc
index 52d0426..415b480 100644
--- a/ui/android/overscroll_glow.cc
+++ b/ui/android/overscroll_glow.cc
@@ -2,15 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ui/android/overscroll_glow.h"
 
 #include <stddef.h>
 
+#include <array>
+
 #include "cc/slim/layer.h"
 #include "ui/android/edge_effect.h"
 #include "ui/android/window_android_compositor.h"
@@ -223,7 +220,7 @@
 
   gfx::Vector2dF overscroll_pull =
       gfx::ScaleVector2d(overscroll_delta, inv_width, inv_height);
-  const float edge_pull[EDGE_COUNT] = {
+  const std::array<float, EDGE_COUNT> edge_pull = {
       min(overscroll_pull.y(), 0.f),  // Top
       min(overscroll_pull.x(), 0.f),  // Left
       max(overscroll_pull.y(), 0.f),  // Bottom
@@ -234,7 +231,7 @@
       gfx::ScaleVector2d(overscroll_location, inv_width, inv_height);
   displacement.set_x(max(0.f, min(1.f, displacement.x())));
   displacement.set_y(max(0.f, min(1.f, displacement.y())));
-  const float edge_displacement[EDGE_COUNT] = {
+  const std::array<float, EDGE_COUNT> edge_displacement = {
       1.f - displacement.x(),  // Top
       displacement.y(),        // Left
       displacement.x(),        // Bottom
@@ -259,7 +256,7 @@
   DCHECK(!velocity.IsZero());
 
   // Only trigger on initial overscroll at a non-zero velocity
-  const float overscroll_velocities[EDGE_COUNT] = {
+  const std::array<float, EDGE_COUNT> overscroll_velocities = {
       y_overscroll_started ? min(velocity.y(), 0.f) : 0,  // Top
       x_overscroll_started ? min(velocity.x(), 0.f) : 0,  // Left
       y_overscroll_started ? max(velocity.y(), 0.f) : 0,  // Bottom
diff --git a/ui/android/overscroll_glow.h b/ui/android/overscroll_glow.h
index a083eaa..acd2af1 100644
--- a/ui/android/overscroll_glow.h
+++ b/ui/android/overscroll_glow.h
@@ -5,6 +5,7 @@
 #ifndef UI_ANDROID_OVERSCROLL_GLOW_H_
 #define UI_ANDROID_OVERSCROLL_GLOW_H_
 
+#include <array>
 #include <memory>
 
 #include "base/memory/raw_ptr.h"
@@ -100,10 +101,10 @@
   EdgeEffect* GetOppositeEdge(int edge_index);
 
   raw_ptr<OverscrollGlowClient> client_;
-  std::unique_ptr<EdgeEffect> edge_effects_[EDGE_COUNT];
+  std::array<std::unique_ptr<EdgeEffect>, EDGE_COUNT> edge_effects_;
 
   gfx::SizeF viewport_size_;
-  float edge_offsets_[EDGE_COUNT];
+  std::array<float, EDGE_COUNT> edge_offsets_;
   bool initialized_;
   bool allow_horizontal_overscroll_;
   bool allow_vertical_overscroll_;
diff --git a/ui/android/resources/resource_manager_impl.cc b/ui/android/resources/resource_manager_impl.cc
index 7fcb2c9..01d32651 100644
--- a/ui/android/resources/resource_manager_impl.cc
+++ b/ui/android/resources/resource_manager_impl.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ui/android/resources/resource_manager_impl.h"
 
 #include <inttypes.h>
diff --git a/ui/android/resources/resource_manager_impl.h b/ui/android/resources/resource_manager_impl.h
index f811ad20..22d1f570e 100644
--- a/ui/android/resources/resource_manager_impl.h
+++ b/ui/android/resources/resource_manager_impl.h
@@ -5,6 +5,7 @@
 #ifndef UI_ANDROID_RESOURCES_RESOURCE_MANAGER_IMPL_H_
 #define UI_ANDROID_RESOURCES_RESOURCE_MANAGER_IMPL_H_
 
+#include <array>
 #include <memory>
 #include <unordered_map>
 #include <unordered_set>
@@ -96,7 +97,7 @@
       std::unordered_map<SkColor, std::unique_ptr<ResourceMap>>;
 
   raw_ptr<cc::UIResourceManager> ui_resource_manager_;
-  ResourceMap resources_[ANDROID_RESOURCE_TYPE_COUNT];
+  std::array<ResourceMap, ANDROID_RESOURCE_TYPE_COUNT> resources_;
   TintedResourceMap tinted_resources_;
 
   // The set of tints that are used for resources in the current frame.
diff --git a/ui/display/manager/managed_display_info.cc b/ui/display/manager/managed_display_info.cc
index 1673cf28..daea1db 100644
--- a/ui/display/manager/managed_display_info.cc
+++ b/ui/display/manager/managed_display_info.cc
@@ -393,7 +393,9 @@
       clear_overscan_insets_(false),
       bits_per_channel_(0),
       variable_refresh_rate_state_(VariableRefreshRateState::kVrrNotCapable),
-      vsync_rate_min_(std::nullopt) {}
+      vsync_rate_min_(std::nullopt) {
+  has_overscan_ = true;
+}
 
 ManagedDisplayInfo::ManagedDisplayInfo(const ManagedDisplayInfo& other) =
     default;
diff --git a/ui/events/android/motion_event_android.cc b/ui/events/android/motion_event_android.cc
index dda3bc3e..05623ff 100644
--- a/ui/events/android/motion_event_android.cc
+++ b/ui/events/android/motion_event_android.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ui/events/android/motion_event_android.h"
 
 #include <android/input.h>
diff --git a/ui/events/android/motion_event_android.h b/ui/events/android/motion_event_android.h
index 74a8c37..0ebbf4e 100644
--- a/ui/events/android/motion_event_android.h
+++ b/ui/events/android/motion_event_android.h
@@ -9,6 +9,7 @@
 #include <stddef.h>
 #include <stdint.h>
 
+#include <array>
 #include <memory>
 
 #include "base/android/scoped_java_ref.h"
@@ -143,7 +144,9 @@
     float tilt_x = 0;
     float tilt_y = 0;
     ToolType tool_type = ToolType::UNKNOWN;
-  } cached_pointers_[MAX_POINTERS_TO_CACHE];
+  };
+
+  std::array<CachedPointer, MAX_POINTERS_TO_CACHE> cached_pointers_;
 
   static ToolType FromAndroidToolType(int android_tool_type);
   static base::TimeTicks FromAndroidTime(base::TimeTicks time);
diff --git a/ui/events/android/motion_event_android_java.cc b/ui/events/android/motion_event_android_java.cc
index c76b073..a79be062 100644
--- a/ui/events/android/motion_event_android_java.cc
+++ b/ui/events/android/motion_event_android_java.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ui/events/android/motion_event_android_java.h"
 
 #include <android/input.h>
diff --git a/ui/events/android/motion_event_android_native.cc b/ui/events/android/motion_event_android_native.cc
index 15b8d402..425f21d2 100644
--- a/ui/events/android/motion_event_android_native.cc
+++ b/ui/events/android/motion_event_android_native.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ui/events/android/motion_event_android_native.h"
 
 #include <android/input.h>
diff --git a/ui/views/button_drag_utils.cc b/ui/views/button_drag_utils.cc
index 88f96b6..ba784f0 100644
--- a/ui/views/button_drag_utils.cc
+++ b/ui/views/button_drag_utils.cc
@@ -10,6 +10,8 @@
 #include "base/memory/raw_ptr.h"
 #include "base/strings/utf_string_conversions.h"
 #include "ui/base/dragdrop/os_exchange_data.h"
+#include "ui/base/metadata/metadata_header_macros.h"
+#include "ui/base/metadata/metadata_impl_macros.h"
 #include "ui/base/models/image_model.h"
 #include "ui/base/resource/resource_bundle.h"
 #include "ui/color/color_id.h"
@@ -30,6 +32,29 @@
 #include "ui/views/widget/widget.h"
 #include "url/gurl.h"
 
+namespace {
+
+class DragContentsButton : public views::LabelButton {
+  METADATA_HEADER(DragContentsButton, views::LabelButton)
+
+ public:
+  DragContentsButton(PressedCallback callback, std::u16string title)
+      : LabelButton(std::move(callback), title) {
+#if BUILDFLAG(IS_WIN)
+    // For windows, label button paints icon to a layer by default, which
+    // causes the drag image to not render correctly. Disable this behavior.
+    // This is a workaround for crbug.com/394380766
+    image_container_view()->DestroyLayer();
+#endif
+  }
+  ~DragContentsButton() override = default;
+};
+
+BEGIN_METADATA(DragContentsButton)
+END_METADATA
+
+}  // namespace
+
 namespace button_drag_utils {
 
 // Maximum width of the link drag image in pixels.
@@ -80,8 +105,8 @@
   drag_widget->Init(std::move(params));
 
   // Create a button to render the drag image for us.
-  views::LabelButton* button =
-      drag_widget->SetContentsView(std::make_unique<views::LabelButton>(
+  DragContentsButton* button =
+      drag_widget->SetContentsView(std::make_unique<DragContentsButton>(
           views::Button::PressedCallback(),
           title.empty() ? base::UTF8ToUTF16(url.spec()) : title));
   button->SetTextSubpixelRenderingEnabled(false);
diff --git a/ui/webui/resources/images/BUILD.gn b/ui/webui/resources/images/BUILD.gn
index d4b003f..6fb66cd 100644
--- a/ui/webui/resources/images/BUILD.gn
+++ b/ui/webui/resources/images/BUILD.gn
@@ -50,6 +50,7 @@
   if (!is_ios) {
     input_files += [
       "chrome_logo_dark.svg",
+      "icon_arrow_back.svg",
       "icon_edit.svg",
     ]
   }
@@ -63,7 +64,6 @@
       "chevron_down.svg",
       "dark/arrow_down.svg",
       "dark/chevron_down.svg",
-      "icon_arrow_back.svg",
       "icon_arrow_drop_down_cr23.svg",
       "icon_arrow_drop_up_cr23.svg",
       "icon_bookmark.svg",
diff --git a/v8 b/v8
index 7d13261..5f0be56 160000
--- a/v8
+++ b/v8
@@ -1 +1 @@
-Subproject commit 7d1326123f37e10d3cf0a8ff6efc2d330e1f2b96
+Subproject commit 5f0be56704de0f9fd5ee6bb6a7a780976a5bfcb7