diff --git a/composer.lock b/composer.lock
index be3b54a79f..8c6000c89a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -496,7 +496,7 @@
         },
         {
             "name": "drupal/core",
-            "version": "9.1.x-dev",
+            "version": "9.2.x-dev",
             "dist": {
                 "type": "path",
                 "url": "core",
@@ -745,11 +745,11 @@
         },
         {
             "name": "drupal/core-project-message",
-            "version": "9.1.x-dev",
+            "version": "9.2.x-dev",
             "dist": {
                 "type": "path",
                 "url": "composer/Plugin/ProjectMessage",
-                "reference": "cad5c2853f5b733663dcc1be328f80ee233acc30"
+                "reference": "b4efdbe26634b41a1b89e4f3770a8074769088a6"
             },
             "require": {
                 "composer-plugin-api": "^1.1 || ^2",
@@ -778,11 +778,11 @@
         },
         {
             "name": "drupal/core-vendor-hardening",
-            "version": "9.1.x-dev",
+            "version": "9.2.x-dev",
             "dist": {
                 "type": "path",
                 "url": "composer/Plugin/VendorHardening",
-                "reference": "3732b3fa7c1db63f41bc72d847793ea8aebf7735"
+                "reference": "d54f0b3cc8b4237f3a41a0860a808db242f9da9e"
             },
             "require": {
                 "composer-plugin-api": "^1.1 || ^2",
@@ -4598,9 +4598,6 @@
                 "ext-zip": "Enabling the zip extension allows you to unzip archives",
                 "ext-zlib": "Allow gzip compression of HTTP requests"
             },
-            "bin": [
-                "bin/composer"
-            ],
             "type": "library",
             "extra": {
                 "branch-alias": {
diff --git a/composer/Metapackage/CoreRecommended/composer.json b/composer/Metapackage/CoreRecommended/composer.json
index 43298d8455..e992c061d9 100644
--- a/composer/Metapackage/CoreRecommended/composer.json
+++ b/composer/Metapackage/CoreRecommended/composer.json
@@ -7,7 +7,7 @@
         "webflo/drupal-core-strict": "*"
     },
     "require": {
-        "drupal/core": "9.1.x-dev",
+        "drupal/core": "9.2.x-dev",
         "asm89/stack-cors": "1.3.0",
         "composer/semver": "1.7.1",
         "doctrine/annotations": "1.10.4",
diff --git a/composer/Metapackage/PinnedDevDependencies/composer.json b/composer/Metapackage/PinnedDevDependencies/composer.json
index a91a293d8f..85caa87570 100644
--- a/composer/Metapackage/PinnedDevDependencies/composer.json
+++ b/composer/Metapackage/PinnedDevDependencies/composer.json
@@ -7,7 +7,7 @@
         "webflo/drupal-core-require-dev": "*"
     },
     "require": {
-        "drupal/core": "9.1.x-dev",
+        "drupal/core": "9.2.x-dev",
         "behat/mink": "v1.8.1",
         "behat/mink-browserkit-driver": "v1.3.4",
         "behat/mink-goutte-driver": "v1.2.1",
diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php
index c2fd03d1ee..6fee486e7f 100644
--- a/core/lib/Drupal.php
+++ b/core/lib/Drupal.php
@@ -80,7 +80,7 @@ class Drupal {
   /**
    * The current system version.
    */
-  const VERSION = '9.1.0-dev';
+  const VERSION = '9.2.0-dev';
 
   /**
    * Core API compatibility.
diff --git a/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php b/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php
index 79900c5f34..0d09afb850 100644
--- a/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php
+++ b/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php
@@ -7,6 +7,9 @@
 use Drupal\Core\Cache\CacheableDependencyTrait;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines a fallback plugin for missing block plugins.
@@ -17,16 +20,56 @@
  *   category = @Translation("Block"),
  * )
  */
-class Broken extends PluginBase implements BlockPluginInterface {
+class Broken extends PluginBase implements BlockPluginInterface, ContainerFactoryPluginInterface {
 
   use BlockPluginTrait;
   use CacheableDependencyTrait;
 
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * Creates a Broken Block instance.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin ID for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, AccountInterface $current_user) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->currentUser = $current_user;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('current_user')
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
   public function build() {
-    return $this->brokenMessage();
+    $build = [];
+    if ($this->currentUser->hasPermission('administer blocks')) {
+      $build += $this->brokenMessage();
+    }
+    return $build;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php
index 51fc9c0bbe..542c2848f7 100644
--- a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php
+++ b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php
@@ -292,24 +292,6 @@ private function parseDefinition($id, $service, $file)
             $definition->setAutowired($service['autowire']);
         }
 
-        if (isset($service['autowiring_types'])) {
-            if (is_string($service['autowiring_types'])) {
-                $definition->addAutowiringType($service['autowiring_types']);
-            } else {
-                if (!is_array($service['autowiring_types'])) {
-                    throw new InvalidArgumentException(sprintf('Parameter "autowiring_types" must be a string or an array for service "%s" in %s. Check your YAML syntax.', $id, $file));
-                }
-
-                foreach ($service['autowiring_types'] as $autowiringType) {
-                    if (!is_string($autowiringType)) {
-                        throw new InvalidArgumentException(sprintf('A "autowiring_types" attribute must be of type string for service "%s" in %s. Check your YAML syntax.', $id, $file));
-                    }
-
-                    $definition->addAutowiringType($autowiringType);
-                }
-            }
-        }
-
         $this->container->setDefinition($id, $definition);
     }
 
diff --git a/core/lib/Drupal/Core/Utility/token.api.php b/core/lib/Drupal/Core/Utility/token.api.php
index 2b62ff4cfa..4d1417d173 100644
--- a/core/lib/Drupal/Core/Utility/token.api.php
+++ b/core/lib/Drupal/Core/Utility/token.api.php
@@ -149,16 +149,6 @@ function hook_tokens($type, $tokens, array $data, array $options, \Drupal\Core\R
  * @see hook_tokens()
  */
 function hook_tokens_alter(array &$replacements, array $context, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata) {
-  $options = $context['options'];
-
-  if (isset($options['langcode'])) {
-    $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
-    $langcode = $options['langcode'];
-  }
-  else {
-    $langcode = NULL;
-  }
-
   if ($context['type'] == 'node' && !empty($context['data']['node'])) {
     $node = $context['data']['node'];
 
diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt
index eb17e6768c..cccdc84e97 100644
--- a/core/misc/cspell/dictionary.txt
+++ b/core/misc/cspell/dictionary.txt
@@ -117,7 +117,6 @@ autosave
 autosubmit
 autowire
 autowired
-autowiring
 backend's
 backlink
 backlinks
diff --git a/core/modules/block/tests/src/Functional/BlockUiTest.php b/core/modules/block/tests/src/Functional/BlockUiTest.php
index f396a306cd..008a132647 100644
--- a/core/modules/block/tests/src/Functional/BlockUiTest.php
+++ b/core/modules/block/tests/src/Functional/BlockUiTest.php
@@ -382,4 +382,35 @@ public function testRouteProtection() {
     $this->assertSession()->statusCodeEquals(403);
   }
 
+  /**
+   * Tests that users without permission are not able to view broken blocks.
+   */
+  public function testBrokenBlockVisibility() {
+    $assert_session = $this->assertSession();
+
+    $this->drupalPlaceBlock('broken');
+
+    // Login as an admin user to the site.
+    $this->drupalLogin($this->adminUser);
+    $this->drupalGet('');
+    $assert_session->statusCodeEquals(200);
+    // Check that this user can view the Broken Block message.
+    $assert_session->pageTextContains('This block is broken or missing. You may be missing content or you might need to enable the original module.');
+    $this->drupalLogout();
+
+    // Visit the same page as anonymous.
+    $this->drupalGet('');
+    $assert_session->statusCodeEquals(200);
+    // Check that this user cannot view the Broken Block message.
+    $assert_session->pageTextNotContains('This block is broken or missing. You may be missing content or you might need to enable the original module.');
+
+    // Visit same page as an authorized user that does not have access to
+    // administer blocks.
+    $this->drupalLogin($this->drupalCreateUser(['access administration pages']));
+    $this->drupalGet('');
+    $assert_session->statusCodeEquals(200);
+    // Check that this user cannot view the Broken Block message.
+    $assert_session->pageTextNotContains('This block is broken or missing. You may be missing content or you might need to enable the original module.');
+  }
+
 }
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_element_test/src/EventSubscriber/TestPrepareLayout.php b/core/modules/layout_builder/tests/modules/layout_builder_element_test/src/EventSubscriber/TestPrepareLayout.php
index b1ac1369c9..cec4af43bb 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_element_test/src/EventSubscriber/TestPrepareLayout.php
+++ b/core/modules/layout_builder/tests/modules/layout_builder_element_test/src/EventSubscriber/TestPrepareLayout.php
@@ -81,6 +81,7 @@ public function onBeforePrepareLayout(PrepareLayoutEvent $event) {
           'id' => 'static_block',
           'label' => 'Test static block title',
           'label_display' => 'visible',
+          'provider' => 'fake_provider',
         ]));
         $section_storage->appendSection($section);
       }
@@ -113,6 +114,7 @@ public function onAfterPrepareLayout(PrepareLayoutEvent $event) {
           'id' => 'static_block_two',
           'label' => 'Test second static block title',
           'label_display' => 'visible',
+          'provider' => 'fake_provider',
         ]));
         $section_storage->appendSection($section);
       }
diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderPrepareLayoutTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderPrepareLayoutTest.php
index 414a768840..e991d909e4 100644
--- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderPrepareLayoutTest.php
+++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderPrepareLayoutTest.php
@@ -78,6 +78,7 @@ public function testAlterPrepareLayout() {
 
     $this->drupalLogin($this->drupalCreateUser([
       'access content',
+      'administer blocks',
       'configure any layout',
       'administer node display',
       'configure all bundle_with_section_field node layout overrides',
diff --git a/core/modules/simpletest/simpletest.install b/core/modules/simpletest/simpletest.install
index 3d9c80a95a..7143573c97 100644
--- a/core/modules/simpletest/simpletest.install
+++ b/core/modules/simpletest/simpletest.install
@@ -21,7 +21,7 @@ function simpletest_requirements($phase) {
     $requirements['simpletest'] = [
       'title' => t('SimpleTest'),
       'severity' => REQUIREMENT_ERROR,
-      'description' => t('SimpleTest is has been removed from Drupal 9.0.0 and can no longer be installed. A contributed module is available for those who wish to continue using SimpleTest during the transition from Drupal 8 to 9. <a href=":change-record">See the change record for more information</a>.', [
+      'description' => t('SimpleTest has been removed from Drupal 9.0.0 and can no longer be installed. A contributed module is available for those who wish to continue using SimpleTest during the transition from Drupal 8 to 9. <a href=":change-record">See the change record for more information</a>.', [
         ':change-record' => 'https://www.drupal.org/node/3091784',
       ]),
     ];
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index d917af8343..476dd1b61c 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1268,8 +1268,9 @@ function _system_is_claro_admin_and_not_active() {
  */
 function system_library_info_alter(&$libraries, $extension) {
   // If Claro is the admin theme but not the active theme, grant Claro the
-  // ability to override the toolbar library with its own assets.
-  if ($extension === 'toolbar' && _system_is_claro_admin_and_not_active()) {
+  // ability to override the toolbar and contextual libraries with its own
+  //assets.
+  if (in_array($extension, ['toolbar', 'contextual']) && _system_is_claro_admin_and_not_active()) {
     require_once DRUPAL_ROOT . '/core/themes/claro/claro.theme';
     claro_system_module_invoked_library_info_alter($libraries, $extension);
   }
diff --git a/core/modules/system/tests/src/Functional/Theme/ContextualClaroOverridesTest.php b/core/modules/system/tests/src/Functional/Theme/ContextualClaroOverridesTest.php
new file mode 100644
index 0000000000..74160bf248
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/Theme/ContextualClaroOverridesTest.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Theme;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the loading of Contextual's Claro assets on a non-Claro default theme.
+ *
+ * @group Theme
+ */
+class ContextualClaroOverridesTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['node', 'contextual'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * The theme installer used in this test for enabling themes.
+   *
+   * @var \Drupal\Core\Extension\ThemeInstallerInterface
+   */
+  protected $themeInstaller;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->themeInstaller = $this->container->get('theme_installer');
+    $this->themeManager = $this->container->get('theme.manager');
+    $this->themeInstaller->install(['claro']);
+
+    $this->drupalCreateContentType([
+      'type' => 'page',
+      'name' => 'Basic page',
+      'display_submitted' => FALSE,
+    ]);
+
+    // Create a node.
+    $this->drupalCreateNode();
+
+    $this->drupalLogin($this->drupalCreateUser([
+      'edit any page content',
+      'delete any page content',
+      'access contextual links',
+    ]));
+  }
+
+  /**
+   * Confirm Contextual's Claro assets load on a non-Claro default theme.
+   */
+  public function testClaroAssets() {
+    $default_stylesheets = [
+      'core/modules/contextual/css/contextual.theme.css',
+      'core/modules/contextual/css/contextual.icons.theme.css',
+    ];
+
+    $claro_stylesheets = [
+      'core/themes/claro/css/theme/contextual.theme.css',
+      'core/themes/claro/css/theme/contextual.icons.theme.css',
+      'core/themes/claro/css/components/icon-link.css',
+    ];
+
+    $this->config('system.theme')->set('admin', 'stark')->save();
+    $admin_theme = \Drupal::configFactory()->get('system.theme')->get('admin');
+    $default_theme = \Drupal::configFactory()->get('system.theme')->get('default');
+    $this->assertEquals('stark', $admin_theme);
+    $this->assertEquals('stark', $default_theme);
+
+    $this->drupalGet('node/1');
+    $head = $this->getSession()->getPage()->find('css', 'head')->getHtml();
+
+    // Confirm that Claro's Contextual assets are not loading, and the ones they
+    // would override if Claro was the admin theme are still loading.
+    $stylesheet_positions = [];
+    foreach ($default_stylesheets as $stylesheet) {
+      $this->assertStringContainsString($stylesheet, $head);
+      $stylesheet_positions[] = strpos($head, $stylesheet);
+    }
+    $sorted_stylesheet_positions = $stylesheet_positions;
+    sort($sorted_stylesheet_positions);
+    $this->assertEquals($sorted_stylesheet_positions, $stylesheet_positions);
+
+    foreach ($claro_stylesheets as $stylesheet) {
+      $this->assertStringNotContainsString($stylesheet, $head);
+    }
+    $this->assertSession()->elementNotExists('css', 'script[src*="core/themes/claro/js/contextual.js"]');
+
+    $this->config('system.theme')->set('admin', 'claro')->save();
+    $admin_theme = \Drupal::configFactory()->get('system.theme')->get('admin');
+    $default_theme = \Drupal::configFactory()->get('system.theme')->get('default');
+    $this->assertEquals('claro', $admin_theme);
+    $this->assertEquals('stark', $default_theme);
+
+    $this->drupalGet('node/1');
+    $head = $this->getSession()->getPage()->find('css', 'head')->getHtml();
+
+    // Confirm that Claro's Contextual assets are loading, and the ones they
+    // override are not loading.
+    $stylesheet_positions = [];
+    foreach ($claro_stylesheets as $stylesheet) {
+      $this->assertStringContainsString($stylesheet, $head);
+      $stylesheet_positions[] = strpos($head, $stylesheet);
+    }
+    $sorted_stylesheet_positions = $stylesheet_positions;
+    sort($sorted_stylesheet_positions);
+    $this->assertEquals($sorted_stylesheet_positions, $stylesheet_positions);
+
+    foreach ($default_stylesheets as $stylesheet) {
+      $this->assertStringNotContainsString($stylesheet, $head);
+    }
+
+    $this->assertSession()->elementExists('css', 'script[src*="core/themes/claro/js/contextual.js"]');
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Block/BlockManagerTest.php b/core/tests/Drupal/Tests/Core/Block/BlockManagerTest.php
index 824cc19900..3f373fae3e 100644
--- a/core/tests/Drupal/Tests/Core/Block/BlockManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Block/BlockManagerTest.php
@@ -7,8 +7,10 @@
 use Drupal\Core\Block\Plugin\Block\Broken;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\Tests\UnitTestCase;
 use Psr\Log\LoggerInterface;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
 
 /**
  * @coversDefaultClass \Drupal\Core\Block\BlockManager
@@ -37,6 +39,11 @@ class BlockManagerTest extends UnitTestCase {
   protected function setUp(): void {
     parent::setUp();
 
+    $container = new ContainerBuilder();
+    $current_user = $this->prophesize(AccountInterface::class);
+    $container->set('current_user', $current_user->reveal());
+    \Drupal::setContainer($container);
+
     $cache_backend = $this->prophesize(CacheBackendInterface::class);
     $module_handler = $this->prophesize(ModuleHandlerInterface::class);
     $this->logger = $this->prophesize(LoggerInterface::class);
diff --git a/core/themes/claro/claro.info.yml b/core/themes/claro/claro.info.yml
index 635cabfdd8..5de096e61b 100644
--- a/core/themes/claro/claro.info.yml
+++ b/core/themes/claro/claro.info.yml
@@ -38,6 +38,12 @@ libraries-override:
       theme:
         css/system.admin.css: false
 
+  contextual/drupal.contextual-links:
+    css:
+      theme:
+        css/contextual.theme.css: css/theme/contextual.theme.css
+        css/contextual.icons.theme.css: css/theme/contextual.icons.theme.css
+
   core/drupal.dialog.off_canvas:
     css:
       base:
@@ -130,6 +136,8 @@ libraries-extend:
     - claro/vertical-tabs
   core/jquery.ui:
     - claro/claro.jquery.ui
+  contextual/drupal.contextual-links:
+    - claro/contextual-trigger
   file/drupal.file:
     - claro/file
   filter/drupal.filter.admin:
diff --git a/core/themes/claro/claro.libraries.yml b/core/themes/claro/claro.libraries.yml
index 02ed758bc1..5a7a2415cd 100644
--- a/core/themes/claro/claro.libraries.yml
+++ b/core/themes/claro/claro.libraries.yml
@@ -186,6 +186,19 @@ checkbox:
   dependencies:
     - core/drupal
 
+contextual-trigger:
+  version: VERSION
+  js:
+    js/contextual.js: {}
+  dependencies:
+    - claro/icon-link
+
+icon-link:
+  version: VERSION
+  css:
+    component:
+      css/components/icon-link.css: {}
+
 dropbutton:
   version: VERSION
   js:
diff --git a/core/themes/claro/claro.theme b/core/themes/claro/claro.theme
index 8576c0e7f9..6794c29879 100644
--- a/core/themes/claro/claro.theme
+++ b/core/themes/claro/claro.theme
@@ -1613,7 +1613,8 @@ function claro_preprocess_toolbar(&$variables, $hook, $info) {
  * Called by system.module via its hook_library_info_alter().
  *
  * If the active theme is not Claro, but Claro is the admin theme, this alters
- * the toolbar library config so Claro's toolbar stylesheets are used.
+ * the library config for specific extensions so Claro's toolbar stylesheets are
+ * used.
  *
  * @see system_library_info_alter()
  */
@@ -1638,6 +1639,30 @@ function claro_system_module_invoked_library_info_alter(&$libraries, $extension)
       }
     }
   }
+
+  if ($extension === 'contextual') {
+    $claro_libraries = \Drupal::service('library.discovery')->getLibrariesByExtension('claro');
+    $claro_info = \Drupal::service('theme_handler')->listInfo()['claro']->info;
+    $path_prefix = '/core/themes/claro/';
+    $claro_contexutal_overrides = $claro_info['libraries-override']['contextual/drupal.contextual-links'];
+    $claro_contexutal_extend = $claro_info['libraries-extend']['contextual/drupal.contextual-links'];
+
+    foreach ($claro_contexutal_overrides['css'] as $concern => $overrides) {
+      foreach ($claro_contexutal_overrides['css'][$concern] as $key => $value) {
+        $config = $libraries['drupal.contextual-links']['css'][$concern][$key];
+        $libraries['drupal.contextual-links']['css'][$concern][$path_prefix . $value] = $config;
+        unset($libraries['drupal.contextual-links']['css'][$concern][$key]);
+      }
+    }
+
+    foreach ($claro_contexutal_extend as $library => $extendee_library_name) {
+      $extendee_library = $claro_libraries[str_replace('claro/', '', $extendee_library_name)] ?? FALSE;
+      if ($extendee_library) {
+        extend_library($libraries['drupal.contextual-links'], $extendee_library_name, $extendee_library, $claro_libraries);
+      }
+    }
+  }
+
 }
 
 /**
@@ -1655,3 +1680,30 @@ function claro_system_module_invoked_theme_registry_alter(array &$theme_registry
     }
   }
 }
+
+/**
+ * Adds library dependencies.
+ *
+ * @param array $library_to_extend
+ *   The library being extended.
+ * @param string $extendee_library_name
+ *   The name of the library being added.
+ * @param array $extendee_library
+ *   The library being added.
+ * @param array $claro_libraries
+ *   All libraries defined by Claro
+ */
+function extend_library(array &$library_to_extend, $extendee_library_name, array $extendee_library, array $claro_libraries) {
+  $library_to_extend['dependencies'][] = $extendee_library_name;
+
+  // If this library has other Claro dependencies, recursively call this
+  // function to add the assets from those libraries as well.
+  if (isset($extendee_library['dependencies'])) {
+    foreach ($extendee_library['dependencies'] as $dependee_library_name) {
+      $dependee_library = $claro_libraries[str_replace('claro/', '', $dependee_library_name)] ?? FALSE;
+      if ($dependee_library) {
+        extend_library($library_to_extend, $dependee_library_name, $dependee_library, $claro_libraries);
+      }
+    }
+  }
+}
diff --git a/core/themes/claro/css/base/variables.css b/core/themes/claro/css/base/variables.css
index 00a95eca56..f353ad87f8 100644
--- a/core/themes/claro/css/base/variables.css
+++ b/core/themes/claro/css/base/variables.css
@@ -52,4 +52,10 @@
   /**
    * Breadcrumb.
    */
+  /**
+  * Dropbutton.
+  */
+  /**
+   * Icon links.
+   */
 }
diff --git a/core/themes/claro/css/base/variables.pcss.css b/core/themes/claro/css/base/variables.pcss.css
index a11c984ad0..b8017bff7e 100644
--- a/core/themes/claro/css/base/variables.pcss.css
+++ b/core/themes/claro/css/base/variables.pcss.css
@@ -206,4 +206,32 @@
    * Breadcrumb.
    */
   --breadcrumb-height: 1.25rem;
+  /**
+  * Dropbutton.
+  */
+  --dropbutton-spacing-size: var(--space-m);
+  --dropbutton-font-size: var(--font-size-base);
+  --dropbutton-line-height: var(--space-m);
+  --dropbutton-toggle-size: 3rem;
+  --dropbutton-border-size: 1px;
+  --dropbutton-toggle-size-spacing: var(--dropbutton-border-size);
+  --dropbutton-border-radius-size: 2px;
+  --dropbutton-small-spacing-size: 0.625rem;
+  --dropbutton-small-font-size: var(--font-size-xs);
+  --dropbutton-small-line-height: 0.75rem;
+  --dropbutton-small-toggle-size: calc((2 * var(--dropbutton-small-spacing-size)) + var(--dropbutton-small-line-height));
+  --dropbutton-extrasmall-spacing-size: 0.375rem;
+  --dropbutton-extrasmall-font-size: var(--font-size-xs);
+  --dropbutton-extrasmall-line-height: 0.75rem;
+  --dropbutton-extrasmall-toggle-size: calc((2 * var(--dropbutton-extrasmall-spacing-size)) + var(--dropbutton-extrasmall-line-height));
+  /**
+   * Icon links.
+   */
+  --icon-link-bg-color: var(--color-white);
+  --icon-link-border-color: var(--color-lightgray);
+  --icon-link-border-size: 1px;
+  --icon-link--active-bg-color: var(--color-absolutezero);
+  --icon-link--active-border-color: var(--color-absolutezero);
+  --icon-link--hover-bg-color: var(--color-bgblue-hover);
+  --icon-link--hover-border-color: var(--color-lightgray-o-80);
 }
diff --git a/core/themes/claro/css/components/button.css b/core/themes/claro/css/components/button.css
index 197e201840..a1dacb6657 100644
--- a/core/themes/claro/css/components/button.css
+++ b/core/themes/claro/css/components/button.css
@@ -187,6 +187,7 @@ a.button--danger:active {
 
 .button.is-disabled {
   -webkit-user-select: none;
+  -moz-user-select: none;
   -ms-user-select: none;
   user-select: none;
   pointer-events: none;
diff --git a/core/themes/claro/css/components/dropbutton.css b/core/themes/claro/css/components/dropbutton.css
index 6d297e2f73..7757aa17e5 100644
--- a/core/themes/claro/css/components/dropbutton.css
+++ b/core/themes/claro/css/components/dropbutton.css
@@ -14,14 +14,6 @@
  *    contrast mode Firefox.
  */
 
-:root {
-  /**
-  * Dropbutton
-  */
-  /* Variant variables: small. */
-  /* Variant variables: extra small. */
-}
-
 .dropbutton-wrapper {
   display: inline-flex;
   border-radius: 2px;
diff --git a/core/themes/claro/css/components/dropbutton.pcss.css b/core/themes/claro/css/components/dropbutton.pcss.css
index 2da904bd59..ca8d5a1b67 100644
--- a/core/themes/claro/css/components/dropbutton.pcss.css
+++ b/core/themes/claro/css/components/dropbutton.pcss.css
@@ -9,29 +9,6 @@
 
 @import "../base/variables.pcss.css";
 
-:root {
-  /**
-  * Dropbutton
-  */
-  --dropbutton-spacing-size: var(--space-m);
-  --dropbutton-font-size: var(--font-size-base);
-  --dropbutton-line-height: var(--space-m);
-  --dropbutton-toggle-size: 3rem;
-  --dropbutton-border-size: 1px;
-  --dropbutton-toggle-size-spacing: var(--dropbutton-border-size);
-  --dropbutton-border-radius-size: 2px;
-  /* Variant variables: small. */
-  --dropbutton-small-spacing-size: 0.625rem;
-  --dropbutton-small-font-size: var(--font-size-xs);
-  --dropbutton-small-line-height: 0.75rem;
-  --dropbutton-small-toggle-size: calc((2 * var(--dropbutton-small-spacing-size)) + var(--dropbutton-small-line-height));
-  /* Variant variables: extra small. */
-  --dropbutton-extrasmall-spacing-size: 0.375rem;
-  --dropbutton-extrasmall-font-size: var(--font-size-xs);
-  --dropbutton-extrasmall-line-height: 0.75rem;
-  --dropbutton-extrasmall-toggle-size: calc((2 * var(--dropbutton-extrasmall-spacing-size)) + var(--dropbutton-extrasmall-line-height));
-}
-
 .dropbutton-wrapper {
   display: inline-flex;
   border-radius: var(--button-border-radius-size);
diff --git a/core/themes/claro/css/components/icon-link.css b/core/themes/claro/css/components/icon-link.css
new file mode 100644
index 0000000000..710ef335c6
--- /dev/null
+++ b/core/themes/claro/css/components/icon-link.css
@@ -0,0 +1,37 @@
+/*
+ * DO NOT EDIT THIS FILE.
+ * See the following change record for more information,
+ * https://www.drupal.org/node/3084859
+ * @preserve
+ */
+
+/**
+ * @file
+ * Icon link component.
+ */
+
+.icon-link {
+  border: 1px solid #d4d4d8;
+  border-radius: 50%;
+  background-color: #fff;
+}
+
+.icon-link:hover {
+  border-color: rgba(212, 212, 218, 0.8);
+  background-color: #f0f5fd;
+}
+
+.icon-link:focus {
+  outline: none;
+  box-shadow: 0 0 0 1.5px #fff, 0 0 0 3.5px #26a769;
+}
+
+.icon-link:active,
+.open > .icon-link {
+  border-color: #003cc5;
+  background-color: #003cc5;
+}
+
+.icon-link--small:focus {
+  box-shadow: 0 0 0 1px #fff, 0 0 0 3px #26a769;
+}
diff --git a/core/themes/claro/css/components/icon-link.pcss.css b/core/themes/claro/css/components/icon-link.pcss.css
new file mode 100644
index 0000000000..74452bf028
--- /dev/null
+++ b/core/themes/claro/css/components/icon-link.pcss.css
@@ -0,0 +1,32 @@
+/**
+ * @file
+ * Icon link component.
+ */
+
+@import "../base/variables.pcss.css";
+
+.icon-link {
+  border: var(--icon-link-border-size) solid var(--icon-link-border-color);
+  border-radius: 50%;
+  background-color: var(--icon-link-bg-color);
+}
+
+.icon-link:hover {
+  border-color: var(--icon-link--hover-border-color);
+  background-color: var(--icon-link--hover-bg-color);
+}
+
+.icon-link:focus {
+  outline: none;
+  box-shadow: 0 0 0 1.5px var(--color-white), 0 0 0 3.5px var(--color-focus);
+}
+
+.icon-link:active,
+.open > .icon-link {
+  border-color: var(--icon-link--active-border-color);
+  background-color: var(--icon-link--active-bg-color);
+}
+
+.icon-link--small:focus {
+  box-shadow: 0 0 0 1px var(--color-white), 0 0 0 3px var(--color-focus);
+}
diff --git a/core/themes/claro/css/layout/card-list.css b/core/themes/claro/css/layout/card-list.css
index 2a2bd4f651..230b5dfee0 100644
--- a/core/themes/claro/css/layout/card-list.css
+++ b/core/themes/claro/css/layout/card-list.css
@@ -41,8 +41,8 @@
 
 @media screen and (min-width: 36.75rem) {
   .card-list--four-cols .card-list__item {
-    flex-basis: calc(49.95% - 0.5rem);
-    max-width: calc(49.95% - 0.5rem);
+    flex-basis: calc((99.9% + 1rem)/2 - 1rem);
+    max-width: calc((99.9% + 1rem)/2 - 1rem);
   }
 
   .card-list--four-cols .card-list__item {
@@ -75,8 +75,8 @@
 
 @media screen and (min-width: 70rem) {
   .card-list--four-cols .card-list__item {
-    flex-basis: calc(33.3% - 0.66667rem);
-    max-width: calc(33.3% - 0.66667rem);
+    flex-basis: calc((99.9% + 1rem)/3 - 1rem);
+    max-width: calc((99.9% + 1rem)/3 - 1rem);
   }
 
   .card-list--four-cols .card-list__item:nth-child(even) {
@@ -98,8 +98,8 @@
 
 @media screen and (min-width: 85.375rem) {
   .card-list--two-cols .card-list__item {
-    flex-basis: calc(49.95% - 0.5rem);
-    max-width: calc(49.95% - 0.5rem);
+    flex-basis: calc((99.9% + 1rem)/2 - 1rem);
+    max-width: calc((99.9% + 1rem)/2 - 1rem);
     margin-right: 1rem;
   }
   [dir="rtl"] .card-list--two-cols .card-list__item {
@@ -116,8 +116,8 @@
   }
 
   .card-list--four-cols .card-list__item {
-    flex-basis: calc(24.975% - 0.75rem);
-    max-width: calc(24.975% - 0.75rem);
+    flex-basis: calc((99.9% + 1rem)/4 - 1rem);
+    max-width: calc((99.9% + 1rem)/4 - 1rem);
   }
 
   .card-list--four-cols .card-list__item:nth-child(even) {
diff --git a/core/themes/claro/css/theme/contextual.icons.theme.css b/core/themes/claro/css/theme/contextual.icons.theme.css
new file mode 100644
index 0000000000..4970d4d929
--- /dev/null
+++ b/core/themes/claro/css/theme/contextual.icons.theme.css
@@ -0,0 +1,45 @@
+/*
+ * DO NOT EDIT THIS FILE.
+ * See the following change record for more information,
+ * https://www.drupal.org/node/3084859
+ * @preserve
+ */
+
+/**
+ * @file
+ * Styling for contextual module icons.
+ */
+
+.contextual__trigger,
+.contextual__trigger:focus {
+  width: 2rem;
+  height: 2rem;
+}
+
+.contextual__trigger::after {
+  position: absolute;
+  top: calc(0.5rem - 1px);
+  left: calc(0.5rem - 1px);
+  width: 1rem;
+  height: 1rem;
+  content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23545560' d='M14.545 3.042l-1.586-1.585c-.389-.389-1.025-.389-1.414 0l-1.293 1.293 3 3 1.293-1.293c.389-.389.389-1.026 0-1.415z'/%3e%3crect fill='%23545560' x='5.129' y='3.8' transform='matrix(-.707 -.707 .707 -.707 6.189 20.064)' width='4.243' height='9.899'/%3e%3cpath fill='%23545560' d='M.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z'/%3e%3c/g%3e%3c/svg%3e");
+}
+
+.contextual__trigger:active::after,
+.open > .contextual__trigger::after {
+  content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23ffffff' d='M14.545 3.042l-1.586-1.585c-.389-.389-1.025-.389-1.414 0l-1.293 1.293 3 3 1.293-1.293c.389-.389.389-1.026 0-1.415z'/%3e%3crect fill='%23ffffff' x='5.129' y='3.8' transform='matrix(-.707 -.707 .707 -.707 6.189 20.064)' width='4.243' height='9.899'/%3e%3cpath fill='%23ffffff' d='M.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z'/%3e%3c/g%3e%3c/svg%3e");
+}
+
+@media screen and (-ms-high-contrast: active) {
+  .contextual__trigger::after {
+    content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23ffffff' d='M14.545 3.042l-1.586-1.585c-.389-.389-1.025-.389-1.414 0l-1.293 1.293 3 3 1.293-1.293c.389-.389.389-1.026 0-1.415z'/%3e%3crect fill='%23ffffff' x='5.129' y='3.8' transform='matrix(-.707 -.707 .707 -.707 6.189 20.064)' width='4.243' height='9.899'/%3e%3cpath fill='%23ffffff' d='M.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z'/%3e%3c/g%3e%3c/svg%3e");
+  }
+  .contextual__trigger:active,
+  .open > .contextual__trigger {
+    background-color: #fff;
+  }
+  .contextual__trigger:active::after,
+  .open > .contextual__trigger::after {
+    content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cg%3e%3cpath fill='%23000000' d='M14.545 3.042l-1.586-1.585c-.389-.389-1.025-.389-1.414 0l-1.293 1.293 3 3 1.293-1.293c.389-.389.389-1.026 0-1.415z'/%3e%3crect fill='%23000000' x='5.129' y='3.8' transform='matrix(-.707 -.707 .707 -.707 6.189 20.064)' width='4.243' height='9.899'/%3e%3cpath fill='%23000000' d='M.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z'/%3e%3c/g%3e%3c/svg%3e");
+  }
+}
diff --git a/core/themes/claro/css/theme/contextual.icons.theme.pcss.css b/core/themes/claro/css/theme/contextual.icons.theme.pcss.css
new file mode 100644
index 0000000000..5696fba33b
--- /dev/null
+++ b/core/themes/claro/css/theme/contextual.icons.theme.pcss.css
@@ -0,0 +1,47 @@
+/**
+ * @file
+ * Styling for contextual module icons.
+ */
+
+@import "../base/variables.pcss.css";
+
+:root {
+  --contextual-trigger-width: 2rem;
+  --contextual-trigger-height: 2rem;
+  --contextual-icon-width: calc(var(--contextual-trigger-width) / 2);
+  --contextual-icon-height: calc(var(--contextual-trigger-height) / 2);
+}
+
+.contextual__trigger,
+.contextual__trigger:focus {
+  width: var(--contextual-trigger-width);
+  height: var(--contextual-trigger-height);
+}
+
+.contextual__trigger::after {
+  position: absolute;
+  top: calc(var(--contextual-icon-height) / 2 - var(--icon-link-border-size));
+  left: calc(var(--contextual-icon-width) / 2 - var(--icon-link-border-size));
+  width: var(--contextual-icon-width);
+  height: var(--contextual-icon-height);
+  content: url(../../images/icons/545560/pencil.svg);
+}
+
+.contextual__trigger:active::after,
+.open > .contextual__trigger::after {
+  content: url(../../images/icons/ffffff/pencil.svg);
+}
+
+@media screen and (-ms-high-contrast: active) {
+  .contextual__trigger::after {
+    content: url(../../images/icons/ffffff/pencil.svg);
+  }
+  .contextual__trigger:active,
+  .open > .contextual__trigger {
+    background-color: var(--color-white);
+  }
+  .contextual__trigger:active::after,
+  .open > .contextual__trigger::after {
+    content: url(../../images/icons/000000/pencil.svg);
+  }
+}
diff --git a/core/themes/claro/css/theme/contextual.theme.css b/core/themes/claro/css/theme/contextual.theme.css
new file mode 100644
index 0000000000..cc67631ca6
--- /dev/null
+++ b/core/themes/claro/css/theme/contextual.theme.css
@@ -0,0 +1,118 @@
+/*
+ * DO NOT EDIT THIS FILE.
+ * See the following change record for more information,
+ * https://www.drupal.org/node/3084859
+ * @preserve
+ */
+
+/**
+ * @file
+ * Styling for contextual module.
+ */
+
+:root { /* 4px */
+}
+
+.contextual {
+  position: absolute;
+  z-index: 500;
+  top: 0.25rem;
+  right: 0.25rem;
+}
+
+[dir="rtl"] .contextual {
+  right: auto;
+  left: 0.25rem;
+}
+
+.contextual.open {
+  z-index: 501;
+}
+
+.contextual-region.focus {
+  outline: 1px dashed #8e929c;
+  outline-offset: 1px;
+}
+
+.contextual__trigger {
+  position: relative;
+  float: right; /* LTR */
+  overflow: hidden;
+  cursor: pointer;
+}
+
+[dir="rtl"] .contextual__trigger {
+  float: left;
+}
+
+.open > .contextual__trigger {
+  z-index: 2;
+}
+
+.contextual-links {
+  position: relative;
+  top: 0.125rem; /* 2px */
+  right: 0; /* LTR */
+  float: right; /* LTR */
+  clear: both;
+  margin: 0;
+  padding: 0;
+  white-space: nowrap;
+  border: 1px solid #d4d4d8;
+  border-radius: 2px;
+  background: #fff;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
+}
+
+[dir="rtl"] .contextual-links {
+  right: auto;
+  left: 0;
+  float: left;
+}
+
+.contextual-links a {
+  position: relative;
+  display: block;
+  box-sizing: border-box;
+  width: 100%;
+  padding: calc(1rem - 1px);
+  color: #545560;
+  border-radius: 2px;
+  background: #fff;
+  font-size: 1rem;
+  line-height: 1rem;
+  -webkit-font-smoothing: antialiased;
+}
+
+[dir="rtl"] .contextual-links a {
+  text-align: right;
+}
+
+.contextual-links a,
+.contextual-links a:hover {
+  text-decoration: none;
+}
+
+.touchevents .contextual-links a {
+  font-size: large;
+}
+
+.contextual-links li {
+  list-style: none;
+}
+
+.contextual-links li a:hover {
+  color: #222330;
+  background: #f3f4f9;
+}
+
+.contextual-links li a:focus {
+  z-index: 2;
+  box-shadow: inset 0 0 0 1px #26a769, 0 0 0 1px #26a769;
+  text-decoration: none;
+}
+
+.contextual-links li a:active,
+.contextual-links li [aria-pressed="true"] {
+  color: #545560;
+}
diff --git a/core/themes/claro/css/theme/contextual.theme.pcss.css b/core/themes/claro/css/theme/contextual.theme.pcss.css
new file mode 100644
index 0000000000..1c5164a0e3
--- /dev/null
+++ b/core/themes/claro/css/theme/contextual.theme.pcss.css
@@ -0,0 +1,112 @@
+/**
+ * @file
+ * Styling for contextual module.
+ */
+
+@import "../base/variables.pcss.css";
+
+:root {
+  --contextual-z-index: 500;
+  --contextual-icon-spacing-size: 0.25rem; /* 4px */
+  --contextual-outline-bg-color: var(--color-grayblue);
+  --contextual-links-spacing-size: var(--dropbutton-spacing-size);
+  --contextual-links-border-size: var(--dropbutton-border-size);
+  --contextual-links-border-radius-size: var(--dropbutton-border-radius-size);
+  --contextual-links-font-size: var(--dropbutton-font-size);
+  --contextual-links-line-height: var(--dropbutton-line-height);
+}
+
+.contextual {
+  position: absolute;
+  z-index: var(--contextual-z-index);
+  top: var(--contextual-icon-spacing-size);
+  right: var(--contextual-icon-spacing-size);
+}
+[dir="rtl"] .contextual {
+  right: auto;
+  left: var(--contextual-icon-spacing-size);
+}
+
+.contextual.open {
+  z-index: calc(var(--contextual-z-index) + 1);
+}
+
+.contextual-region.focus {
+  outline: 1px dashed var(--contextual-outline-bg-color);
+  outline-offset: 1px;
+}
+
+.contextual__trigger {
+  position: relative;
+  float: right; /* LTR */
+  overflow: hidden;
+  cursor: pointer;
+}
+[dir="rtl"] .contextual__trigger {
+  float: left;
+}
+.open > .contextual__trigger {
+  z-index: 2;
+}
+
+.contextual-links {
+  position: relative;
+  top: 0.125rem; /* 2px */
+  right: 0; /* LTR */
+  float: right; /* LTR */
+  clear: both;
+  margin: 0;
+  padding: 0;
+  white-space: nowrap;
+  border: var(--contextual-links-border-size) solid var(--color-lightgray);
+  border-radius: var(--dropbutton-border-radius-size);
+  background: var(--color-white);
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
+}
+[dir="rtl"] .contextual-links {
+  right: auto;
+  left: 0;
+  float: left;
+}
+
+.contextual-links a {
+  position: relative;
+  display: block;
+  box-sizing: border-box;
+  width: 100%;
+  padding: calc(var(--contextual-links-spacing-size) - var(--contextual-links-border-size));
+  color: var(--color-davysgray);
+  border-radius: var(--contextual-links-border-radius-size);
+  background: var(--color-white);
+  font-size: var(--contextual-links-font-size);
+  line-height: var(--contextual-links-line-height);
+  -webkit-font-smoothing: antialiased;
+}
+[dir="rtl"] .contextual-links a {
+  text-align: right;
+}
+.contextual-links a,
+.contextual-links a:hover {
+  text-decoration: none;
+}
+.touchevents .contextual-links a {
+  font-size: large;
+}
+
+.contextual-links li {
+  list-style: none;
+}
+
+.contextual-links li a:hover {
+  color: var(--color-text);
+  background: var(--color-whitesmoke);
+}
+.contextual-links li a:focus {
+  z-index: 2;
+  box-shadow: inset 0 0 0 1px var(--color-focus), 0 0 0 1px var(--color-focus);
+  text-decoration: none;
+}
+.contextual-links li a:active,
+.contextual-links li [aria-pressed="true"] {
+  color: var(--color-davysgray);
+}
diff --git a/core/themes/claro/images/icons/000000/pencil.svg b/core/themes/claro/images/icons/000000/pencil.svg
new file mode 100644
index 0000000000..c3d7892796
--- /dev/null
+++ b/core/themes/claro/images/icons/000000/pencil.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g><path fill="#000000" d="M14.545 3.042l-1.586-1.585c-.389-.389-1.025-.389-1.414 0l-1.293 1.293 3 3 1.293-1.293c.389-.389.389-1.026 0-1.415z"/><rect fill="#000000" x="5.129" y="3.8" transform="matrix(-.707 -.707 .707 -.707 6.189 20.064)" width="4.243" height="9.899"/><path fill="#000000" d="M.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z"/></g></svg>
diff --git a/core/themes/claro/images/icons/545560/pencil.svg b/core/themes/claro/images/icons/545560/pencil.svg
new file mode 100644
index 0000000000..2808993ddf
--- /dev/null
+++ b/core/themes/claro/images/icons/545560/pencil.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g><path fill="#545560" d="M14.545 3.042l-1.586-1.585c-.389-.389-1.025-.389-1.414 0l-1.293 1.293 3 3 1.293-1.293c.389-.389.389-1.026 0-1.415z"/><rect fill="#545560" x="5.129" y="3.8" transform="matrix(-.707 -.707 .707 -.707 6.189 20.064)" width="4.243" height="9.899"/><path fill="#545560" d="M.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z"/></g></svg>
diff --git a/core/themes/claro/images/icons/ffffff/pencil.svg b/core/themes/claro/images/icons/ffffff/pencil.svg
new file mode 100644
index 0000000000..229e480913
--- /dev/null
+++ b/core/themes/claro/images/icons/ffffff/pencil.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g><path fill="#ffffff" d="M14.545 3.042l-1.586-1.585c-.389-.389-1.025-.389-1.414 0l-1.293 1.293 3 3 1.293-1.293c.389-.389.389-1.026 0-1.415z"/><rect fill="#ffffff" x="5.129" y="3.8" transform="matrix(-.707 -.707 .707 -.707 6.189 20.064)" width="4.243" height="9.899"/><path fill="#ffffff" d="M.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z"/></g></svg>
diff --git a/core/themes/claro/js/contextual.es6.js b/core/themes/claro/js/contextual.es6.js
new file mode 100644
index 0000000000..30ea605779
--- /dev/null
+++ b/core/themes/claro/js/contextual.es6.js
@@ -0,0 +1,52 @@
+/**
+ * @file
+ * Theme overrides for contextual trigger.
+ */
+
+(Drupal => {
+  /**
+   * Override Contextual's AuralView render() so contextual trigger text can be
+   * processed with a theme function.
+   *
+   * @todo this entire override can be removed after
+   *   https://drupal.org/node/3172956
+   */
+  Drupal.contextual.AuralView = Drupal.contextual.AuralView.extend({
+    render: function render() {
+      const isOpen = this.model.get('isOpen');
+      this.$el.find('.contextual-links').prop('hidden', !isOpen);
+      const triggerText = Drupal.t('@action @title configuration options', {
+        '@action': !isOpen
+          ? this.options.strings.open
+          : this.options.strings.close,
+        '@title': this.model.get('title'),
+      });
+
+      this.$el
+        .find('.trigger')
+        .html(Drupal.theme('contextualTriggerText', triggerText))
+        .attr('aria-pressed', isOpen);
+    },
+  });
+
+  /**
+   * Constructs a contextual trigger element.
+   *
+   * @return {string}
+   *   A string representing a DOM fragment.
+   */
+  Drupal.theme.contextualTrigger = () =>
+    `<button class="contextual__trigger trigger visually-hidden focusable icon-link icon-link--small" type="button"></button>`;
+
+  /**
+   * Contextual link trigger text, typically seen only by screenreaders.
+   *
+   * @param {string} text
+   *   The contextual link trigger text.
+   *
+   * @return {string}
+   *   A string representing a DOM fragment.
+   */
+  Drupal.theme.contextualTriggerText = text =>
+    `<span class="visually-hidden">${text}</span>`;
+})(Drupal, Backbone);
diff --git a/core/themes/claro/js/contextual.js b/core/themes/claro/js/contextual.js
new file mode 100644
index 0000000000..e8ae50a9b7
--- /dev/null
+++ b/core/themes/claro/js/contextual.js
@@ -0,0 +1,28 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function (Drupal) {
+  Drupal.contextual.AuralView = Drupal.contextual.AuralView.extend({
+    render: function render() {
+      var isOpen = this.model.get('isOpen');
+      this.$el.find('.contextual-links').prop('hidden', !isOpen);
+      var triggerText = Drupal.t('@action @title configuration options', {
+        '@action': !isOpen ? this.options.strings.open : this.options.strings.close,
+        '@title': this.model.get('title')
+      });
+      this.$el.find('.trigger').html(Drupal.theme('contextualTriggerText', triggerText)).attr('aria-pressed', isOpen);
+    }
+  });
+
+  Drupal.theme.contextualTrigger = function () {
+    return "<button class=\"contextual__trigger trigger visually-hidden focusable icon-link icon-link--small\" type=\"button\"></button>";
+  };
+
+  Drupal.theme.contextualTriggerText = function (text) {
+    return "<span class=\"visually-hidden\">".concat(text, "</span>");
+  };
+})(Drupal, Backbone);
\ No newline at end of file
