diff --git a/core/profiles/demo_umami/themes/umami/css/components/forms/container-inline.module.css b/core/profiles/demo_umami/themes/umami/css/components/forms/container-inline.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..280c480c2e7eb5efc5c199b5c5a366440e1c5dcf
--- /dev/null
+++ b/core/profiles/demo_umami/themes/umami/css/components/forms/container-inline.module.css
@@ -0,0 +1,10 @@
+/**
+ * @file
+ * Inline items.
+ */
+
+/* @todo revisit in https://drupal.org/node/3115710 */
+.container-inline div,
+.container-inline label {
+  display: inline;
+}
diff --git a/core/profiles/demo_umami/themes/umami/umami.libraries.yml b/core/profiles/demo_umami/themes/umami/umami.libraries.yml
index 254d6bda49fb03eca188034daebf97fe91b9e681..2e0a0071929c9f47e211d713c14d0e757c5f5775 100644
--- a/core/profiles/demo_umami/themes/umami/umami.libraries.yml
+++ b/core/profiles/demo_umami/themes/umami/umami.libraries.yml
@@ -34,6 +34,7 @@ global:
       css/components/fields/recipe-instruction.css: {}
       css/components/fields/summary.css: {}
       css/components/forms/buttons.css: {}
+      css/components/forms/container-inline.module.css: {}
       css/components/views/promoted-items.css: {}
       css/components/views/frontpage.css: {}
       css/components/navigation/menu-footer/menu-footer.css: {}
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/StableDecoupledTest.php b/core/tests/Drupal/KernelTests/Core/Theme/StableDecoupledTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..94e4e0039f32d97decb2d24b9d80f3d17fd25da1
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Theme/StableDecoupledTest.php
@@ -0,0 +1,305 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Theme;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests that themes do not depend on Stable assets.
+ *
+ * These tests exist to facilitate the process of decoupling theme from Stable.
+ * The decoupling process includes:
+ *   1. Identifying Stable assets that differ from core ones
+ *   2. Of those differing assets, identify those that are inherited by
+ *     the themes being tested.
+ *   3. Determine if a copy of that Stable asset must be added to the theme,
+ *     or if it's acceptable for the theme to inherit the more recent version
+ *     of the asset from core.
+ * This test will identify these assets and confirm that the theme has properly
+ * decoupled itself from them by either having a theme-specific copy of the
+ * asset, or it being determined that it's acceptable for the theme to use
+ * core's version (in which case it will be skipped by this test).
+ *
+ * @group Theme
+ */
+class StableDecoupledTest extends KernelTestBase {
+
+  /**
+   * Populates an array with CSS and templates used within a given directory.
+   *
+   * @param string[] &$assets
+   *   Modified parameter, an array of asset filenames.
+   * @param string $path
+   *   The path being checked.
+   */
+  protected function getAssets(array &$assets, $path) {
+    $directory = new \RecursiveDirectoryIterator($path, \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS);
+    $iterator = new \RecursiveIteratorIterator($directory);
+    foreach ($iterator as $fileinfo) {
+      if ($fileinfo->getExtension() !== 'twig' && $fileinfo->getExtension() !== 'css') {
+        continue;
+      }
+      $filename = $fileinfo->getFilename();
+      $filepath = $fileinfo->getPathname();
+      // Tests can have assets with the same filename, so skip assets in those
+      // directories.
+      if (strpos($filepath, '/test') === FALSE) {
+        $assets[$filename] = file_get_contents($filepath);
+      }
+    }
+  }
+
+  /**
+   * Returns a list of asset filenames that are actually different in Stable.
+   *
+   * @return string[]
+   *   Filenames of Stable assets that differ form their core equivalents.
+   */
+  protected function assetsThatDifferInStable() {
+    static $non_identical_files = [];
+    if (!empty($non_identical_files)) {
+      return $non_identical_files;
+    }
+    $core_assets = [];
+    $stable_assets = [];
+    $non_identical_files = [];
+
+    $this->getAssets($core_assets, 'core/modules');
+    $this->getAssets($core_assets, 'core/misc');
+    $this->getAssets($stable_assets, 'core/themes/stable');
+
+    // Assets that are in core but not Stable.
+    $in_core_only = array_diff(array_keys($core_assets), array_keys($stable_assets));
+
+    // Files that are included in Stable but never loaded by core themes. This
+    // files file.admin.css and filter.admin.css were removed in core so
+    // Stable's libraries-override never replaces them with core's version. The
+    // simpletest-result-summary.html.twig template is skipped because the
+    // Simpletest module was removed from core.
+    $in_stable_but_never_loaded = [
+      'file.admin.css',
+      'filter.admin.css',
+      'simpletest-result-summary.html.twig',
+    ];
+
+    // Files related to normalize.css can be skipped. These files are related to
+    // the core/normalize library and not inherited in the same manner.
+    $normalize_css_assets = ['normalize.css', 'normalize-fixes.css'];
+
+    // Create an array of skippable asset files.
+    $assets_to_skip = array_merge($in_core_only, $in_stable_but_never_loaded, $normalize_css_assets);
+
+    foreach ($stable_assets as $filename => $stable_asset_contents) {
+      if (in_array($filename, $assets_to_skip)) {
+        continue;
+      }
+
+      $core_asset_contents = $core_assets[$filename];
+
+      // Most of the remaining logic in this loop is replacing expected
+      // differences between assets so the string comparison only surfaces
+      // functional differences between the two.
+      // Off canvas and layout builder CSS have different relative paths to
+      // image files than all other CSS files being tested.
+      if (strpos($filename, 'off-canvas') !== FALSE) {
+        $core_asset_contents = str_replace('(../icons', '(../../../images/core/icons', $core_asset_contents);
+      }
+      elseif (strpos($filename, 'layout-builder') !== FALSE || strpos($filename, 'settings_tray') !== FALSE) {
+        $core_asset_contents = str_replace('../misc', '../../misc', $core_asset_contents);
+      }
+      else {
+        $core_asset_contents = str_replace('(../../../../misc/', '(../../../images/core/', $core_asset_contents);
+        $core_asset_contents = str_replace('(../../../misc/', '(../../images/core/', $core_asset_contents);
+      }
+
+      // Account for several additional differences between core assets and the
+      // Stable equivalents.
+      $before = [
+        '* Default theme implementation',
+        'Default template for',
+        "*\n * @ingroup themeable\n */",
+        "*\n* @ingroup themeable\n*/",
+        'Default theme override',
+        'Default view template',
+        "* Theme override of a container used to wrap the media library's\n * modal dialog interface.",
+      ];
+      $after = [
+        '* Theme override',
+        'Theme override for',
+        '*/',
+        '*/',
+        'Theme override',
+        'Theme override',
+        "* Theme override of a container used to wrap the media library's modal dialog\n * interface.",
+      ];
+      $core_asset_contents = str_replace($before, $after, $core_asset_contents);
+
+      // Account for the module-specific subdirectories used by Stable CSS
+      // files that consume image files.
+      foreach (['shortcut', 'quickedit', 'color', 'views_ui', 'image'] as $module) {
+        $stable_asset_contents = str_replace("/../images/$module/", '/images/', $stable_asset_contents);
+      }
+
+      // If the asset contents still differ after being changed to account for
+      // expected differences between core and stable, they are not identical
+      // and should be added to the array returned by this method.
+      if ($stable_asset_contents !== $core_asset_contents) {
+        $non_identical_files[] = $filename;
+      }
+    }
+
+    return $non_identical_files;
+  }
+
+  /**
+   * Confirms that theme assets are decoupled from Stable.
+   *
+   * @param string $path
+   *   The path to the theme being tested.
+   * @param string[] $to_skip
+   *   Assets to skip in the test.
+   *
+   * @dataProvider providerTestDecoupledStable
+   */
+  public function testDecoupledStable($path, array $to_skip) {
+    $assets = [];
+
+    // Get a list of asset files that differ between core and Stable.
+    $files_to_check = $this->assetsThatDifferInStable();
+
+    // Get all non-test .twig and .css assets within $path.
+    $this->getAssets($assets, $path);
+
+    // Of the assets to check, exclude any that appear within the theme being
+    // tested as that means they're already overridden. Also exclude those
+    // listed in $to_skip.
+    $files_inherited_from_stable = array_diff($files_to_check, array_keys($assets), $to_skip);
+    $this->assertEmpty($files_inherited_from_stable);
+  }
+
+  /**
+   * Data provider.
+   *
+   * The to-skip arrays should become increasingly smaller as issues that
+   * address Stable library dependencies are completed.
+   *
+   * @return array[]
+   *   Themes and the asset filenames to be ignored.
+   */
+  public function providerTestDecoupledStable() {
+    return [
+      'claro' => [
+        'path' => 'core/themes/claro',
+        'to-skip' => [
+          // The changes in the core version have acceptable results.
+          'block.admin.css',
+          // The only difference is to address an issue with normalize.css 3.0.3
+          // that is fixed by updating to version 8.0.1.
+          // @see https://drupal.org/node/2821525
+          'filter.caption.css',
+          // The only difference is overridden in Claro CSS.
+          'progress.module.css',
+          // This template is not used by core themes.
+          'status-report.html.twig',
+          // The following two templates can be skipped as the only difference
+          // is the removal of an unnecessary data-striping attribute in core's
+          // version.
+          'system-modules-details.html.twig',
+          'system-modules-uninstall.html.twig',
+        ],
+      ],
+      'seven' => [
+        'path' => 'core/themes/seven',
+        'to-skip' => [
+          // The changes in the core version have acceptable results.
+          'block.admin.css',
+          // The only difference is to address an issue with normalize.css 3.0.3
+          // that is fixed by updating to version 8.0.1.
+          // @see https://drupal.org/node/2821525
+          'filter.caption.css',
+          // The only difference is overridden in Seven CSS.
+          'progress.module.css',
+          // This template is not used by core themes.
+          'status-report.html.twig',
+          // The difference is a desired markup change.
+          // @see https://drupal.org/node/3113211
+          'views-ui-views-listing-table.html.twig',
+          // The following two templates can be skipped as the only difference
+          // is the removal of an unnecessary data-striping attribute in core's
+          // version.
+          'system-modules-details.html.twig',
+          'system-modules-uninstall.html.twig',
+        ],
+      ],
+      'bartik' => [
+        'path' => 'core/themes/bartik',
+        'to-skip' => [
+          // The only difference is overridden in Bartik CSS.
+          'container-inline.module.css',
+          // The only difference is overridden in Bartik CSS.
+          'progress.module.css',
+          // The changes are acceptable as Bartik is not an admin theme.
+          'block.admin.css',
+          // The only difference is to address an issue with normalize.css 3.0.3
+          // that is fixed by updating to version 8.0.1.
+          // @see https://drupal.org/node/2821525
+          'filter.caption.css',
+          // The only difference is a desired markup change.
+          // @see https://drupal.org/node/2528420
+          'install-page.html.twig',
+          // This template is not used by core themes.
+          'status-report.html.twig',
+          // The differences in Stable vs. core are acceptable ones.
+          'status-report-counter.html.twig',
+          // The only difference is one column of inconsequential whitespace.
+          'status-report-general-info.html.twig',
+          // The differences in Stable vs. core are acceptable ones.
+          'system-status-counter.css',
+          // The difference is a desired markup change.
+          // @see https://drupal.org/node/3113211
+          'views-ui-views-listing-table.html.twig',
+          // The following two templates can be skipped as the only difference
+          // is the removal of an unnecessary data-striping attribute in core's
+          // version.
+          'system-modules-details.html.twig',
+          'system-modules-uninstall.html.twig',
+        ],
+      ],
+      'umami' => [
+        'path' => 'core/profiles/demo_umami/themes/umami',
+        'to-skip' => [
+          // The only difference is overridden in Umami CSS.
+          'container-inline.module.css',
+          // The only difference is overridden in Umami CSS.
+          'progress.module.css',
+          // The changes are acceptable as Umami is not an admin theme.
+          'block.admin.css',
+          // The only difference is to address an issue with normalize.css 3.0.3
+          // that is fixed by updating to version 8.0.1.
+          // @see https://drupal.org/node/2821525
+          'filter.caption.css',
+          // The only difference is a desired markup change.
+          // @see https://drupal.org/node/2528420
+          'install-page.html.twig',
+          // This template is not used by core themes.
+          'status-report.html.twig',
+          // The differences in Stable vs. core are acceptable ones.
+          'status-report-counter.html.twig',
+          // The only difference is one column of inconsequential whitespace.
+          'status-report-general-info.html.twig',
+          // The differences in Stable vs. core are acceptable ones.
+          'system-status-counter.css',
+          // The difference is a desired markup change.
+          // @see https://drupal.org/node/3113211
+          'views-ui-views-listing-table.html.twig',
+          // The following two templates can be skipped as the only difference
+          // is the removal of an unnecessary data-striping attribute in core's
+          // version.
+          'system-modules-details.html.twig',
+          'system-modules-uninstall.html.twig',
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/themes/bartik/bartik.libraries.yml b/core/themes/bartik/bartik.libraries.yml
index f6bf822d8ed2397bbe8cf2bb50027396910fb412..62e9cb3220282e03805777196f413d9bb3ce611e 100644
--- a/core/themes/bartik/bartik.libraries.yml
+++ b/core/themes/bartik/bartik.libraries.yml
@@ -18,6 +18,7 @@ global-styling:
       css/components/breadcrumb.css: {}
       css/components/captions.css: {}
       css/components/comments.css: {}
+      css/components/container-inline.module.css: {}
       css/components/contextual.css: {}
       css/components/demo-block.css: {}
       # @see https://www.drupal.org/node/2389735
diff --git a/core/themes/bartik/css/components/container-inline.module.css b/core/themes/bartik/css/components/container-inline.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..f6feb7758640fa5df06522223ea8c5d87a1d2015
--- /dev/null
+++ b/core/themes/bartik/css/components/container-inline.module.css
@@ -0,0 +1,9 @@
+/**
+ * @file
+ * Inline items.
+ */
+
+.container-inline div,
+.container-inline label {
+  display: inline;
+}