diff --git a/core/assets/vendor/ckeditor5/style/style.js b/core/assets/vendor/ckeditor5/style/style.js
new file mode 100644
index 0000000000000000000000000000000000000000..a47a10cdcfcd7f1ecfee8588e37e82f46a7c4a2a
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/style.js
@@ -0,0 +1,5 @@
+!function(e){const t=e.en=e.en||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Block styles","Multiple styles":"Multiple styles",Styles:"Styles","Text styles":"Text styles"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})),
+/*!
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */(()=>{var e={529:(e,t,n)=>{"use strict";n.d(t,{Z:()=>i});var s=n(609),o=n.n(s)()((function(e){return e[1]}));o.push([e.id,".ck.ck-dropdown.ck-style-dropdown.ck-style-dropdown_multiple-active>.ck-button>.ck-button__label{font-style:italic}",""]);const i=o},945:(e,t,n)=>{"use strict";n.d(t,{Z:()=>i});var s=n(609),o=n.n(s)()((function(e){return e[1]}));o.push([e.id,":root{--ck-style-panel-columns:3}.ck.ck-style-panel .ck-style-grid{display:grid;grid-template-columns:repeat(var(--ck-style-panel-columns),auto);justify-content:start}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button{display:flex;flex-direction:column;justify-content:space-between}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button .ck-style-grid__button__preview{align-content:center;align-items:center;display:flex;flex-basis:100%;flex-grow:1;justify-content:flex-start}:root{--ck-style-panel-button-width:120px;--ck-style-panel-button-height:80px;--ck-style-panel-button-shadow-color:rgba(0,0,0,.1);--ck-style-panel-button-shadow:0px 0px 6px var(--ck-style-panel-button-shadow-color);--ck-style-panel-button-label-background:#e6e6e6;--ck-style-panel-button-hover-label-background:#ccc;--ck-style-panel-button-hover-border-color:#b3b3b3}.ck.ck-style-panel .ck-style-grid{column-gap:var(--ck-spacing-large);row-gap:var(--ck-spacing-large)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button{--ck-color-button-default-hover-background:var(--ck-color-base-background);--ck-color-button-default-active-background:var(--ck-color-base-background);height:var(--ck-style-panel-button-height);padding:0;width:var(--ck-style-panel-button-width)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:not(:focus){border:1px solid var(--ck-color-base-border);box-shadow:var(--ck-style-panel-button-shadow)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button .ck-button__label{background:var(--ck-style-panel-button-label-background);flex-shrink:0;height:22px;line-height:22px;overflow:hidden;padding:0 var(--ck-spacing-medium);text-overflow:ellipsis;width:100%}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button .ck-style-grid__button__preview{border:2px solid var(--ck-color-base-background);opacity:.9;overflow:hidden;padding:var(--ck-spacing-medium);width:100%}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled{--ck-color-button-default-disabled-background:var(--ck-color-base-foreground)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled:not(:focus){border-color:var(--ck-style-panel-button-label-background);box-shadow:none}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled .ck-button__label{background:var(--ck-style-panel-button-label-background)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled .ck-style-grid__button__preview{border-color:var(--ck-color-base-foreground);filter:saturate(.3);opacity:.4}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-on{--ck-color-button-on-background:var(--ck-color-base-background);--ck-color-button-on-hover-background:var(--ck-color-base-background);--ck-color-button-on-active-background:var(--ck-color-base-background);--ck-style-panel-button-shadow-color:rgba(25,140,240,.1);border-color:var(--ck-color-base-active)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-on .ck-button__label{background:var(--ck-color-base-active);color:var(--ck-color-base-background)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-on:hover{border-color:var(--ck-color-base-active-focus)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-on:hover .ck-button__label{background:var(--ck-color-base-active-focus)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:hover:not(.ck-disabled):not(.ck-on){border-color:var(--ck-style-panel-button-hover-border-color)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:hover:not(.ck-disabled):not(.ck-on) .ck-style-grid__button__preview{opacity:1}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:hover:not(.ck-disabled):not(.ck-on) .ck-button__label{background:var(--ck-style-panel-button-hover-label-background)}",""]);const i=o},561:(e,t,n)=>{"use strict";n.d(t,{Z:()=>i});var s=n(609),o=n.n(s)()((function(e){return e[1]}));o.push([e.id,".ck.ck-style-panel .ck-style-panel__style-group>.ck-label{margin:var(--ck-spacing-large) 0}.ck.ck-style-panel .ck-style-panel__style-group:first-child>.ck-label{margin-top:0}",""]);const i=o},662:(e,t,n)=>{"use strict";n.d(t,{Z:()=>i});var s=n(609),o=n.n(s)()((function(e){return e[1]}));o.push([e.id,":root{--ck-style-panel-max-height:470px}.ck.ck-style-panel{max-height:var(--ck-style-panel-max-height);overflow-y:auto;padding:var(--ck-spacing-large)}",""]);const i=o},609:e=>{"use strict";e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n=e(t);return t[2]?"@media ".concat(t[2]," {").concat(n,"}"):n})).join("")},t.i=function(e,n,s){"string"==typeof e&&(e=[[null,e,""]]);var o={};if(s)for(var i=0;i<this.length;i++){var l=this[i][0];null!=l&&(o[l]=!0)}for(var r=0;r<e.length;r++){var c=[].concat(e[r]);s&&o[c[0]]||(n&&(c[2]?c[2]="".concat(n," and ").concat(c[2]):c[2]=n),t.push(c))}},t}},62:(e,t,n)=>{"use strict";var s,o=function(){return void 0===s&&(s=Boolean(window&&document&&document.all&&!window.atob)),s},i=function(){var e={};return function(t){if(void 0===e[t]){var n=document.querySelector(t);if(window.HTMLIFrameElement&&n instanceof window.HTMLIFrameElement)try{n=n.contentDocument.head}catch(e){n=null}e[t]=n}return e[t]}}(),l=[];function r(e){for(var t=-1,n=0;n<l.length;n++)if(l[n].identifier===e){t=n;break}return t}function c(e,t){for(var n={},s=[],o=0;o<e.length;o++){var i=e[o],c=t.base?i[0]+t.base:i[0],a=n[c]||0,d="".concat(c," ").concat(a);n[c]=a+1;var u=r(d),k={css:i[1],media:i[2],sourceMap:i[3]};-1!==u?(l[u].references++,l[u].updater(k)):l.push({identifier:d,updater:g(k,t),references:1}),s.push(d)}return s}function a(e){var t=document.createElement("style"),s=e.attributes||{};if(void 0===s.nonce){var o=n.nc;o&&(s.nonce=o)}if(Object.keys(s).forEach((function(e){t.setAttribute(e,s[e])})),"function"==typeof e.insert)e.insert(t);else{var l=i(e.insert||"head");if(!l)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");l.appendChild(t)}return t}var d,u=(d=[],function(e,t){return d[e]=t,d.filter(Boolean).join("\n")});function k(e,t,n,s){var o=n?"":s.media?"@media ".concat(s.media," {").concat(s.css,"}"):s.css;if(e.styleSheet)e.styleSheet.cssText=u(t,o);else{var i=document.createTextNode(o),l=e.childNodes;l[t]&&e.removeChild(l[t]),l.length?e.insertBefore(i,l[t]):e.appendChild(i)}}function b(e,t,n){var s=n.css,o=n.media,i=n.sourceMap;if(o?e.setAttribute("media",o):e.removeAttribute("media"),i&&"undefined"!=typeof btoa&&(s+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(i))))," */")),e.styleSheet)e.styleSheet.cssText=s;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(s))}}var h=null,y=0;function g(e,t){var n,s,o;if(t.singleton){var i=y++;n=h||(h=a(t)),s=k.bind(null,n,i,!1),o=k.bind(null,n,i,!0)}else n=a(t),s=b.bind(null,n,t),o=function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(n)};return s(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;s(e=t)}else o()}}e.exports=function(e,t){(t=t||{}).singleton||"boolean"==typeof t.singleton||(t.singleton=o());var n=c(e=e||[],t);return function(e){if(e=e||[],"[object Array]"===Object.prototype.toString.call(e)){for(var s=0;s<n.length;s++){var o=r(n[s]);l[o].references--}for(var i=c(e,t),a=0;a<n.length;a++){var d=r(n[a]);0===l[d].references&&(l[d].updater(),l.splice(d,1))}n=i}}}},704:(e,t,n)=>{e.exports=n(79)("./src/core.js")},273:(e,t,n)=>{e.exports=n(79)("./src/ui.js")},209:(e,t,n)=>{e.exports=n(79)("./src/utils.js")},79:e=>{"use strict";e.exports=CKEditor5.dll}},t={};function n(s){var o=t[s];if(void 0!==o)return o.exports;var i=t[s]={id:s,exports:{}};return e[s](i,i.exports,n),i.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var s in t)n.o(t,s)&&!n.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nc=void 0;var s={};(()=>{"use strict";n.r(s),n.d(s,{Style:()=>V,StyleEditing:()=>x,StyleUI:()=>w});var e=n(704),t=n(273),o=n(209);const i=["caption","colgroup","dd","dt","figcaption","legend","li","optgroup","option","rp","rt","summary","tbody","td","tfoot","th","thead","tr"];class l extends t.ButtonView{constructor(e,t){super(e),this.styleDefinition=t,this.previewView=this._createPreview(),this.set({label:t.name,class:"ck-style-grid__button",withText:!0}),this.extendTemplate({attributes:{role:"option"}}),this.children.add(this.previewView,0)}_createPreview(){const{element:e,classes:n}=this.styleDefinition,s=new t.View(this.locale);return s.setTemplate({tag:"div",attributes:{class:["ck","ck-reset_all-excluded","ck-style-grid__button__preview","ck-content"]},children:[{tag:this._isPreviewable(e)?e:"div",attributes:{class:n},children:[{text:"AaBbCcDdEeFfGgHhIiJj"}]}]}),s}_isPreviewable(e){return!i.includes(e)}}var r=n(62),c=n.n(r),a=n(945),d={injectType:"singletonStyleTag",attributes:{"data-cke":!0},insert:"head",singleton:!0};c()(a.Z,d);a.Z.locals;class u extends t.View{constructor(e,t){super(e),this.set("activeStyles",[]),this.set("enabledStyles",[]),this.children=this.createCollection(),this.children.delegate("execute").to(this);for(const n of t){const t=new l(e,n);this.children.add(t)}this.on("change:activeStyles",(()=>{for(const e of this.children)e.isOn=this.activeStyles.includes(e.styleDefinition.name)})),this.on("change:enabledStyles",(()=>{for(const e of this.children)e.isEnabled=this.enabledStyles.includes(e.styleDefinition.name)})),this.setTemplate({tag:"div",attributes:{class:["ck","ck-style-grid"],role:"listbox"},children:this.children})}}var k=n(561),b={injectType:"singletonStyleTag",attributes:{"data-cke":!0},insert:"head",singleton:!0};c()(k.Z,b);k.Z.locals;class h extends t.View{constructor(e,n,s){super(e),this.labelView=new t.LabelView(e),this.labelView.text=n,this.gridView=new u(e,s),this.setTemplate({tag:"div",attributes:{class:["ck","ck-style-panel__style-group"],role:"group","aria-labelledby":this.labelView.id},children:[this.labelView,this.gridView]})}}var y=n(662),g={injectType:"singletonStyleTag",attributes:{"data-cke":!0},insert:"head",singleton:!0};c()(y.Z,g);y.Z.locals;class p extends t.View{constructor(e,n){super(e);const s=e.t;this.focusTracker=new o.FocusTracker,this.keystrokes=new o.KeystrokeHandler,this.children=this.createCollection(),this.blockStylesGroupView=new h(e,s("Block styles"),n.block),this.inlineStylesGroupView=new h(e,s("Text styles"),n.inline),this.set("activeStyles",[]),this.set("enabledStyles",[]),this._focusables=new t.ViewCollection,this._focusCycler=new t.FocusCycler({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:["arrowup","arrowleft"],focusNext:["arrowdown","arrowright"]}}),n.block.length&&this.children.add(this.blockStylesGroupView),n.inline.length&&this.children.add(this.inlineStylesGroupView),this.blockStylesGroupView.gridView.delegate("execute").to(this),this.inlineStylesGroupView.gridView.delegate("execute").to(this),this.blockStylesGroupView.gridView.bind("activeStyles","enabledStyles").to(this),this.inlineStylesGroupView.gridView.bind("activeStyles","enabledStyles").to(this),this.setTemplate({tag:"div",attributes:{class:["ck","ck-style-panel"]},children:this.children})}render(){super.render();[...this.blockStylesGroupView.gridView.children,...this.inlineStylesGroupView.gridView.children].forEach((e=>{this._focusables.add(e),this.focusTracker.add(e.element)})),this.keystrokes.listenTo(this.element)}focus(){this._focusCycler.focusFirst()}focusLast(){this._focusCycler.focusLast()}}function f(e,t=[]){const n={block:[],inline:[]};for(const s of t){const t=[],o=[];for(const n of e.getDefinitionsForView(s.element))n.isBlock?t.push(n.model):o.push(n.model);t.length?n.block.push({...s,modelElements:t,isBlock:!0}):n.inline.push({...s,ghsAttributes:o})}return n}var v=n(529),m={injectType:"singletonStyleTag",attributes:{"data-cke":!0},insert:"head",singleton:!0};c()(v.Z,m);v.Z.locals;class w extends e.Plugin{static get pluginName(){return"StyleUI"}init(){const e=this.editor,n=f(e.plugins.get("DataSchema"),e.config.get("style.definitions"));e.ui.componentFactory.add("style",(s=>{const o=s.t,i=(0,t.createDropdown)(s),l=new p(s,n),r=e.commands.get("style");return i.bind("isEnabled").to(r),i.panelView.children.add(l),i.buttonView.withText=!0,i.buttonView.bind("label").to(r,"value",(e=>e.length>1?o("Multiple styles"):1===e.length?e[0]:o("Styles"))),i.bind("class").to(r,"value",(e=>{const t=["ck-style-dropdown"];return e.length>1&&t.push("ck-style-dropdown_multiple-active"),t.join(" ")})),l.delegate("execute").to(i),i.on("execute",(t=>{e.execute("style",{styleName:t.source.styleDefinition.name}),e.editing.view.focus()})),l.bind("activeStyles").to(r,"value"),l.bind("enabledStyles").to(r,"enabledStyles"),i}))}}class _ extends e.Command{constructor(e,t){super(e),this.set("value",[]),this.set("enabledStyles",[]),this._styleDefinitions=t}refresh(){const e=this.editor.model,t=e.document.selection,n=new Set,s=new Set;for(const o of this._styleDefinitions.inline)for(const i of o.ghsAttributes){e.schema.checkAttributeInSelection(t,i)&&s.add(o.name);S(this._getValueFromFirstAllowedNode(i),o.classes)&&n.add(o.name)}const i=(0,o.first)(t.getSelectedBlocks());if(i){const t=i.getAncestors({includeSelf:!0,parentFirst:!0});for(const o of t){if(e.schema.isLimit(o))break;if(e.schema.checkAttribute(o,"htmlAttributes"))for(const e of this._styleDefinitions.block){if(!e.modelElements.includes(o.name))continue;s.add(e.name);S(o.getAttribute("htmlAttributes"),e.classes)&&n.add(e.name)}}}this.enabledStyles=Array.from(s).sort(),this.isEnabled=this.enabledStyles.length>0,this.value=this.isEnabled?Array.from(n).sort():[]}execute({styleName:e,forceValue:t}){if(!this.enabledStyles.includes(e))return void(0,o.logWarning)("style-command-executed-with-incorrect-style-name");const n=this.editor.model,s=n.document.selection,i=this.editor.plugins.get("GeneralHtmlSupport"),l=[...this._styleDefinitions.inline,...this._styleDefinitions.block].find((({name:t})=>t==e)),r=void 0===t?!this.value.includes(l.name):t;n.change((()=>{let e;e=l.isBlock?function(e,t,n){const s=new Set;for(const o of e){const e=o.getAncestors({includeSelf:!0,parentFirst:!0});for(const o of e){if(n.isLimit(o))break;if(t.includes(o.name)){s.add(o);break}}}return s}(s.getSelectedBlocks(),l.modelElements,n.schema):[s];for(const t of e)r?i.addModelHtmlClass(l.element,l.classes,t):i.removeModelHtmlClass(l.element,l.classes,t)}))}_getValueFromFirstAllowedNode(e){const t=this.editor.model,n=t.schema,s=t.document.selection;if(s.isCollapsed)return s.getAttribute(e);for(const t of s.getRanges())for(const s of t.getItems())if(n.checkAttribute(s,e))return s.getAttribute(e);return null}}function S(e,t){return!(!e||!e.classes)&&t.every((t=>e.classes.includes(t)))}class x extends e.Plugin{static get pluginName(){return"StyleEditing"}static get requires(){return["GeneralHtmlSupport"]}init(){const e=this.editor,t=f(e.plugins.get("DataSchema"),e.config.get("style.definitions"));e.commands.add("style",new _(e,t)),this._configureGHSDataFilter(t)}_configureGHSDataFilter({block:e,inline:t}){const n=this.editor.plugins.get("DataFilter");n.loadAllowedConfig(e.map(T)),n.loadAllowedConfig(t.map(T))}}function T({element:e,classes:t}){return{name:e,classes:t}}class V extends e.Plugin{static get pluginName(){return"Style"}static get requires(){return[x,w]}}})(),(window.CKEditor5=window.CKEditor5||{}).style=s})();
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/ar.js b/core/assets/vendor/ckeditor5/style/translations/ar.js
new file mode 100644
index 0000000000000000000000000000000000000000..93fad6b5ffb6e2edb425129cf92d28ec11fbcabe
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/ar.js
@@ -0,0 +1 @@
+!function(t){const s=t.ar=t.ar||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"أنماط الكتل","Multiple styles":"أنماط متعددة",Styles:"الأنماط","Text styles":"أنماط النصوص"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/bg.js b/core/assets/vendor/ckeditor5/style/translations/bg.js
new file mode 100644
index 0000000000000000000000000000000000000000..91e98237aeec12d448d75c7cb05bf55d08960582
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/bg.js
@@ -0,0 +1 @@
+!function(t){const s=t.bg=t.bg||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Блокови стилове","Multiple styles":"Множество стилове",Styles:"Стилове","Text styles":"Текстови стилове"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/bn.js b/core/assets/vendor/ckeditor5/style/translations/bn.js
new file mode 100644
index 0000000000000000000000000000000000000000..9ec8fa1e2bbf9f4ce22432b23aa0ec38ced4c2bc
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/bn.js
@@ -0,0 +1 @@
+!function(t){const n=t.bn=t.bn||{};n.dictionary=Object.assign(n.dictionary||{},{"Block styles":"ব্লক স্টাইল","Multiple styles":"একাধিক স্টাইল",Styles:"স্টাইলস","Text styles":"টেস্কট স্টাইল"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/ca.js b/core/assets/vendor/ckeditor5/style/translations/ca.js
new file mode 100644
index 0000000000000000000000000000000000000000..cc8ecb1b8ff04ac838cac5554f7462df397f441d
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/ca.js
@@ -0,0 +1 @@
+!function(s){const t=s.ca=s.ca||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Estils de bloc","Multiple styles":"Estils múltiples",Styles:"Estils","Text styles":"Estils de text"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/cs.js b/core/assets/vendor/ckeditor5/style/translations/cs.js
new file mode 100644
index 0000000000000000000000000000000000000000..a1eb1690d2f44502aa5b1b5e9d8333d5f19aa216
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/cs.js
@@ -0,0 +1 @@
+!function(t){const s=t.cs=t.cs||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Styly bloků","Multiple styles":"Více stylů",Styles:"Styly","Text styles":"Styly textu"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/da.js b/core/assets/vendor/ckeditor5/style/translations/da.js
new file mode 100644
index 0000000000000000000000000000000000000000..a8b1356cdf7f14cec3c7c37a5d813831f3d9dc3e
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/da.js
@@ -0,0 +1 @@
+!function(t){const e=t.da=t.da||{};e.dictionary=Object.assign(e.dictionary||{},{"Block styles":"Blokstile","Multiple styles":"Flere stile",Styles:"Stile","Text styles":"Tekststile"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/de.js b/core/assets/vendor/ckeditor5/style/translations/de.js
new file mode 100644
index 0000000000000000000000000000000000000000..df2dd650d6e814ae80c99edbb6fc012f6581d4a1
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/de.js
@@ -0,0 +1 @@
+!function(e){const t=e.de=e.de||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Block-Stile","Multiple styles":"Mehrere Stile",Styles:"Stile","Text styles":"Text-Stile"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/el.js b/core/assets/vendor/ckeditor5/style/translations/el.js
new file mode 100644
index 0000000000000000000000000000000000000000..393f7ba372c70f64c2a4b1cc4301973a5a44befe
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/el.js
@@ -0,0 +1 @@
+!function(t){const s=t.el=t.el||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Στυλ για μπλοκ","Multiple styles":"Πολλαπλά στυλ",Styles:"Στυλ","Text styles":"Στυλ για κείμενο"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/en-au.js b/core/assets/vendor/ckeditor5/style/translations/en-au.js
new file mode 100644
index 0000000000000000000000000000000000000000..10c98586ed5a9522b9d5add117b25d39f8ff7b99
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/en-au.js
@@ -0,0 +1 @@
+!function(s){const t=s["en-au"]=s["en-au"]||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Block styles","Multiple styles":"Multiple styles",Styles:"Styles","Text styles":"Text styles"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/es.js b/core/assets/vendor/ckeditor5/style/translations/es.js
new file mode 100644
index 0000000000000000000000000000000000000000..4135e93c53f4f94959851c701559fa12042a3781
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/es.js
@@ -0,0 +1 @@
+!function(s){const t=s.es=s.es||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Estilos de bloque","Multiple styles":"Múltiples estilos",Styles:"Estilos","Text styles":"Estilos de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/et.js b/core/assets/vendor/ckeditor5/style/translations/et.js
new file mode 100644
index 0000000000000000000000000000000000000000..5d89f84547ac0a1ed75af201467d5d50e5173240
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/et.js
@@ -0,0 +1 @@
+!function(i){const t=i.et=i.et||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Ploki stiilid","Multiple styles":"Mitu stiili",Styles:"Stiilid","Text styles":"Teksti stiilid"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/fi.js b/core/assets/vendor/ckeditor5/style/translations/fi.js
new file mode 100644
index 0000000000000000000000000000000000000000..417f4e170e94f6aa3bb064245993afde00d16d9a
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/fi.js
@@ -0,0 +1 @@
+!function(t){const i=t.fi=t.fi||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Lohkotyylit","Multiple styles":"Useita tyylejä",Styles:"Tyylit","Text styles":"Tekstityylit"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/fr.js b/core/assets/vendor/ckeditor5/style/translations/fr.js
new file mode 100644
index 0000000000000000000000000000000000000000..6b71b0569bfc1c5f8d00f607cef16073d5929379
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/fr.js
@@ -0,0 +1 @@
+!function(t){const e=t.fr=t.fr||{};e.dictionary=Object.assign(e.dictionary||{},{"Block styles":"Styles de bloc","Multiple styles":"Styles multiples",Styles:"Styles","Text styles":"Styles de texte"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/gl.js b/core/assets/vendor/ckeditor5/style/translations/gl.js
new file mode 100644
index 0000000000000000000000000000000000000000..4f325f10429904f0a64d533622ea279e847c78b2
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/gl.js
@@ -0,0 +1 @@
+!function(s){const t=s.gl=s.gl||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Estilos de bloque","Multiple styles":"Múltiples estilos",Styles:"Estilos","Text styles":"Estilos de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/he.js b/core/assets/vendor/ckeditor5/style/translations/he.js
new file mode 100644
index 0000000000000000000000000000000000000000..074d954b4528d8c4d52dbc3744c8316d8b6b854d
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/he.js
@@ -0,0 +1 @@
+!function(t){const s=t.he=t.he||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"סגנונות בלוקים","Multiple styles":"סגנונות מרובים",Styles:"סגנונות","Text styles":"עיצוב טקסט"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/hi.js b/core/assets/vendor/ckeditor5/style/translations/hi.js
new file mode 100644
index 0000000000000000000000000000000000000000..f66d7a4e75e21b3dc32b3df2661b38aa463fcc70
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/hi.js
@@ -0,0 +1 @@
+!function(i){const t=i.hi=i.hi||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"ब्लॉक स्टाइल्स","Multiple styles":"कई स्टाइल्स",Styles:"स्टाइल्स","Text styles":"टेक्स्ट स्टाइल्स"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/hr.js b/core/assets/vendor/ckeditor5/style/translations/hr.js
new file mode 100644
index 0000000000000000000000000000000000000000..2ae2f8c700597fe05b446757ed90af01cb3d59f7
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/hr.js
@@ -0,0 +1 @@
+!function(i){const t=i.hr=i.hr||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Blok stilovi","Multiple styles":"Više stilova",Styles:"Stilovi","Text styles":"Tekstualni stilovi"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/hu.js b/core/assets/vendor/ckeditor5/style/translations/hu.js
new file mode 100644
index 0000000000000000000000000000000000000000..02de59f58adfa5f6283b4f374fa9697ac6d74810
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/hu.js
@@ -0,0 +1 @@
+!function(s){const t=s.hu=s.hu||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Blokkstílusok","Multiple styles":"Többféle stílus",Styles:"Stílusok","Text styles":"Szövegstílusok"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/id.js b/core/assets/vendor/ckeditor5/style/translations/id.js
new file mode 100644
index 0000000000000000000000000000000000000000..60908c8907bf734f079d57c5598ee92399621165
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/id.js
@@ -0,0 +1 @@
+!function(a){const t=a.id=a.id||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Gaya blok","Multiple styles":"Banyak gaya",Styles:"Gaya","Text styles":"Gaya teks"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/it.js b/core/assets/vendor/ckeditor5/style/translations/it.js
new file mode 100644
index 0000000000000000000000000000000000000000..ed8671f22f7476903b5a0ff482338ba1664d1b5a
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/it.js
@@ -0,0 +1 @@
+!function(i){const t=i.it=i.it||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Stili per blocchi","Multiple styles":"Stili multipli",Styles:"Stili","Text styles":"Stili per testi"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/ja.js b/core/assets/vendor/ckeditor5/style/translations/ja.js
new file mode 100644
index 0000000000000000000000000000000000000000..037872263210481e51a5ae97743929d70f878b62
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/ja.js
@@ -0,0 +1 @@
+!function(t){const s=t.ja=t.ja||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"ブロックスタイル","Multiple styles":"複数のスタイル",Styles:"スタイル","Text styles":"テキストスタイル"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/ko.js b/core/assets/vendor/ckeditor5/style/translations/ko.js
new file mode 100644
index 0000000000000000000000000000000000000000..d40b801b05465dce7530c04c581718d4fb31edd2
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/ko.js
@@ -0,0 +1 @@
+!function(t){const s=t.ko=t.ko||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"블록 스타일","Multiple styles":"다중 스타일",Styles:"스타일","Text styles":"텍스트 스타일"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/lt.js b/core/assets/vendor/ckeditor5/style/translations/lt.js
new file mode 100644
index 0000000000000000000000000000000000000000..8772e3931c746d8ea0d675e8dda4dba8bc8479e7
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/lt.js
@@ -0,0 +1 @@
+!function(i){const t=i.lt=i.lt||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Blokuoti stilius","Multiple styles":"Daug stilių",Styles:"Stiliai","Text styles":"Teksto stiliai"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/lv.js b/core/assets/vendor/ckeditor5/style/translations/lv.js
new file mode 100644
index 0000000000000000000000000000000000000000..21f954db04dd2a47794923700b9d28f41d976a8d
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/lv.js
@@ -0,0 +1 @@
+!function(i){const t=i.lv=i.lv||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Bloka stili","Multiple styles":"Vairāki stili",Styles:"Stili","Text styles":"Teksta stili"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/ms.js b/core/assets/vendor/ckeditor5/style/translations/ms.js
new file mode 100644
index 0000000000000000000000000000000000000000..4310f2c021fc93b5e7b3be229a369d1ae051a89c
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/ms.js
@@ -0,0 +1 @@
+!function(s){const a=s.ms=s.ms||{};a.dictionary=Object.assign(a.dictionary||{},{"Block styles":"Gaya blok","Multiple styles":"Gaya berbilang",Styles:"Gaya","Text styles":"Gaya teks"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/nl.js b/core/assets/vendor/ckeditor5/style/translations/nl.js
new file mode 100644
index 0000000000000000000000000000000000000000..55087d59bc8f21d5045794c0acc40c8143d6dca0
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/nl.js
@@ -0,0 +1 @@
+!function(e){const t=e.nl=e.nl||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Blok stijlen","Multiple styles":"Meerdere stijlen",Styles:"Stijlen","Text styles":"Tekst stijlen"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/no.js b/core/assets/vendor/ckeditor5/style/translations/no.js
new file mode 100644
index 0000000000000000000000000000000000000000..1ee6bc2576feebec94ebb3642e47a767cd202fec
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/no.js
@@ -0,0 +1 @@
+!function(t){const i=t.no=t.no||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Blokkstiler","Multiple styles":"Multiple stiler",Styles:"Stiler","Text styles":"Tekststiler"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/pl.js b/core/assets/vendor/ckeditor5/style/translations/pl.js
new file mode 100644
index 0000000000000000000000000000000000000000..53e79ada91e7c7c7da7b7a7f8c4ec788aeab53fa
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/pl.js
@@ -0,0 +1 @@
+!function(t){const e=t.pl=t.pl||{};e.dictionary=Object.assign(e.dictionary||{},{"Block styles":"Style tekstu blokowego","Multiple styles":"Wiele stylów",Styles:"Style","Text styles":"Style tekstu"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/pt-br.js b/core/assets/vendor/ckeditor5/style/translations/pt-br.js
new file mode 100644
index 0000000000000000000000000000000000000000..442b3da01988c776e5280e5cd62d089f2ebc3941
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/pt-br.js
@@ -0,0 +1 @@
+!function(t){const s=t["pt-br"]=t["pt-br"]||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Estilos de bloco","Multiple styles":"Múltiplos estilos",Styles:"Estilos","Text styles":"Estilos de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/pt.js b/core/assets/vendor/ckeditor5/style/translations/pt.js
new file mode 100644
index 0000000000000000000000000000000000000000..ebc0a534b07ae5501f22c2221a38d15481a7912a
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/pt.js
@@ -0,0 +1 @@
+!function(s){const t=s.pt=s.pt||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Estilos de blocos","Multiple styles":"Vários estilos",Styles:"Estilos","Text styles":"Estilos de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/ro.js b/core/assets/vendor/ckeditor5/style/translations/ro.js
new file mode 100644
index 0000000000000000000000000000000000000000..cf6c754d1cbe5387771f98da29eaad0e49875cd3
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/ro.js
@@ -0,0 +1 @@
+!function(t){const i=t.ro=t.ro||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Stiluri pentru blocuri","Multiple styles":"Stiluri multiple",Styles:"Stiluri","Text styles":"Stiluri pentru text"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/ru.js b/core/assets/vendor/ckeditor5/style/translations/ru.js
new file mode 100644
index 0000000000000000000000000000000000000000..08a4e2aee000e4fce847efd48161dc138d0d0bf7
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/ru.js
@@ -0,0 +1 @@
+!function(t){const s=t.ru=t.ru||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Блочные стили","Multiple styles":"Несколько стилей",Styles:"Стили","Text styles":"Стиль текста"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/sk.js b/core/assets/vendor/ckeditor5/style/translations/sk.js
new file mode 100644
index 0000000000000000000000000000000000000000..96d02cb631bd80754f4c363bcf2ecf814bdd9d22
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/sk.js
@@ -0,0 +1 @@
+!function(t){const l=t.sk=t.sk||{};l.dictionary=Object.assign(l.dictionary||{},{"Block styles":"Štýly bloku","Multiple styles":"Viacero štýlov",Styles:"Štýly","Text styles":"Štýly textu"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/sr-latn.js b/core/assets/vendor/ckeditor5/style/translations/sr-latn.js
new file mode 100644
index 0000000000000000000000000000000000000000..44cd2cc6646b0b9379af388d9159f37a24aceeeb
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/sr-latn.js
@@ -0,0 +1 @@
+!function(t){const i=t["sr-latn"]=t["sr-latn"]||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Blok stilovi","Multiple styles":"Više stilova",Styles:"Stilovi","Text styles":"Stilovi teksta"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/sr.js b/core/assets/vendor/ckeditor5/style/translations/sr.js
new file mode 100644
index 0000000000000000000000000000000000000000..485c4e3d50818cdb8ac193a0759340d9db24dd7c
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/sr.js
@@ -0,0 +1 @@
+!function(s){const t=s.sr=s.sr||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Блок стилови","Multiple styles":"Више стилова",Styles:"Стилови","Text styles":"Стилови текста"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/sv.js b/core/assets/vendor/ckeditor5/style/translations/sv.js
new file mode 100644
index 0000000000000000000000000000000000000000..27d4d9eb7fee8ec1f2633f4bfc72cb82ad5fbe9c
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/sv.js
@@ -0,0 +1 @@
+!function(t){const s=t.sv=t.sv||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Blockstilar","Multiple styles":"Flera stilar",Styles:"Stilar","Text styles":"Texttyper"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/th.js b/core/assets/vendor/ckeditor5/style/translations/th.js
new file mode 100644
index 0000000000000000000000000000000000000000..d2af76bb83e4a0753057cec220c7a443d83e1dbb
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/th.js
@@ -0,0 +1 @@
+!function(t){const s=t.th=t.th||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"รูปแบบบล็อก","Multiple styles":"มีหลายรูปแบบ",Styles:"รูปแบบ","Text styles":"รูปแบบข้อความ"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/tr.js b/core/assets/vendor/ckeditor5/style/translations/tr.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc79d240f939feb634378674b648068967d1aa81
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/tr.js
@@ -0,0 +1 @@
+!function(t){const i=t.tr=t.tr||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Blok stilleri","Multiple styles":"Birden fazla stil",Styles:"Stiller","Text styles":"Metin stilleri"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/uk.js b/core/assets/vendor/ckeditor5/style/translations/uk.js
new file mode 100644
index 0000000000000000000000000000000000000000..bdd86ff2cd1d331aa6060607ca8be892b086f375
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/uk.js
@@ -0,0 +1 @@
+!function(t){const s=t.uk=t.uk||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Стилі блоку","Multiple styles":"Кілька стилів",Styles:"Стилі","Text styles":"Стилі тексту"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/vi.js b/core/assets/vendor/ckeditor5/style/translations/vi.js
new file mode 100644
index 0000000000000000000000000000000000000000..be0388e8db4e47682116a0821fe1c2ed164d2e47
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/vi.js
@@ -0,0 +1 @@
+!function(i){const t=i.vi=i.vi||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Kiểu của khối","Multiple styles":"Nhiều kiểu",Styles:"Kiểu","Text styles":"Kiểu văn bản"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/zh-cn.js b/core/assets/vendor/ckeditor5/style/translations/zh-cn.js
new file mode 100644
index 0000000000000000000000000000000000000000..e8d2ad492d0dd52a50d086e8b0a1710f2ee774f4
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/zh-cn.js
@@ -0,0 +1 @@
+!function(t){const n=t["zh-cn"]=t["zh-cn"]||{};n.dictionary=Object.assign(n.dictionary||{},{"Block styles":"块级样式","Multiple styles":"多样式",Styles:"样式","Text styles":"文本样式"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/assets/vendor/ckeditor5/style/translations/zh.js b/core/assets/vendor/ckeditor5/style/translations/zh.js
new file mode 100644
index 0000000000000000000000000000000000000000..bb265efe617aaa3b4534005902e56ac31f531ec9
--- /dev/null
+++ b/core/assets/vendor/ckeditor5/style/translations/zh.js
@@ -0,0 +1 @@
+!function(t){const s=t.zh=t.zh||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"區塊樣式","Multiple styles":"多重樣式",Styles:"樣式","Text styles":"文字樣式"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
\ No newline at end of file
diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index 34131a2554ae63ea566636e681320fb50a3e4772..8bcb611deb16926ae3b1f0d235e6d701739f4891 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -302,6 +302,19 @@ ckeditor5.codeBlock:
     - core/ckeditor5
     - core/ckeditor5.translations
 
+ckeditor5.style:
+  remote: https://github.com/ckeditor/ckeditor5
+  version: "35.0.1"
+  license:
+    name: GNU-GPL-2.0-or-later
+    url: https://github.com/ckeditor/ckeditor5/blob/v35.0.1/LICENSE.md
+    gpl-compatible: true
+  js:
+    assets/vendor/ckeditor5/style/style.js: { minified: true }
+  dependencies:
+    - core/ckeditor5
+    - core/ckeditor5.translations
+
 ckeditor5.translations:
   # No sensible version can be specified, since the translations may change at
   # any time.
diff --git a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
index 8ed9c5733c35e5c8b73be7b2fd9abc793985588a..a8115170d792528cc2935a8f0818d8ad7c6b4d6d 100644
--- a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
+++ b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
@@ -62,6 +62,27 @@ ckeditor5_heading:
       - <h5>
       - <h6>
 
+ckeditor5_style:
+  ckeditor5:
+    plugins: [style.Style]
+  drupal:
+    label: Style
+    library: core/ckeditor5.style
+    admin_library: ckeditor5/admin.style
+    class: Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style
+    toolbar_items:
+      style:
+        label: Style
+    # This plugin is able to add any configured class on any tag that can be
+    # created by some other CKEditor 5 plugin. Hence it indicates it allows all
+    # classes on all tags. Its subset then restricts this to a concrete set of
+    # tags, and a concrete set of classes.
+    # @todo Update in https://www.drupal.org/project/drupal/issues/3280124
+    # @see \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style::getElementsSubset()
+    # @see \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator
+    elements:
+      - <$any-html5-element class>
+
 ckeditor5_arbitraryHtmlSupport:
   ckeditor5:
     plugins: [htmlSupport.GeneralHtmlSupport]
diff --git a/core/modules/ckeditor5/ckeditor5.libraries.yml b/core/modules/ckeditor5/ckeditor5.libraries.yml
index cbe1d455ccd7c0786b2d4de855467a8a7d0a3820..61e1d9e63cd6c2dba2de8fd64dce843023dfdf82 100644
--- a/core/modules/ckeditor5/ckeditor5.libraries.yml
+++ b/core/modules/ckeditor5/ckeditor5.libraries.yml
@@ -177,6 +177,17 @@ admin.sourceEditing:
     theme:
       css/source-editing.admin.css: { }
 
+admin.style:
+  js:
+    js/ckeditor5.style.admin.js: { }
+  css:
+    theme:
+      css/style.admin.css: { }
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/drupal.vertical-tabs
+
 admin.table:
   css:
     theme:
diff --git a/core/modules/ckeditor5/ckeditor5.module b/core/modules/ckeditor5/ckeditor5.module
index 1eb0293a8535137ee4da3fda8e97badd5f2d33d6..5be20aab8a7b374716e7a8a6f91b9912f183e3fd 100644
--- a/core/modules/ckeditor5/ckeditor5.module
+++ b/core/modules/ckeditor5/ckeditor5.module
@@ -133,19 +133,6 @@ function ckeditor5_form_filter_format_form_alter(array &$form, FormStateInterfac
     if (isset($form['filters']['settings']['filter_html']['allowed_html'])) {
       $filter_allowed_html = &$form['filters']['settings']['filter_html']['allowed_html'];
 
-      if (isset($form['editor']['settings']['subform']['plugins']['ckeditor5_sourceEditing']['allowed_tags'])) {
-        $source_allowed_tags = &$form['editor']['settings']['subform']['plugins']['ckeditor5_sourceEditing']['allowed_tags'];
-        // @todo if this triggers the callback via keyboard navigation such as
-        //   tab, focus should move to the next element, not to the rebuilt
-        //   "allowed tags" field
-        //   https://www.drupal.org/project/ckeditor5/issues/3231321.
-        $source_allowed_tags['#ajax'] = [
-          'callback' => '_update_ckeditor5_html_filter',
-          'trigger_as' => ['name' => 'editor_configure'],
-          'event' => 'change',
-        ];
-      }
-
       $filter_allowed_html['#value_callback'] = [CKEditor5::class, 'getGeneratedAllowedHtmlValue'];
       // Set readonly and add the form-disabled wrapper class as using #disabled
       // or the disabled attribute will prevent the new values from being
@@ -214,6 +201,7 @@ function _add_ajax_listeners_to_plugin_inputs(array &$plugins_config_form): void
         'checkbox',
         'select',
         'radios',
+        'textarea',
       ];
       if (isset($plugins_config_form['#type']) && in_array($plugins_config_form['#type'], $field_types) && !isset($plugins_config_form['#ajax'])) {
         $plugins_config_form['#ajax'] = [
@@ -267,17 +255,22 @@ function _add_ajax_listeners_to_plugin_inputs(array &$plugins_config_form): void
 function ckeditor5_filter_format_edit_form_submit(array $form, FormStateInterface $form_state) {
   $limit_allowed_html_tags = isset($form['filters']['settings']['filter_html']['allowed_html']);
   $manually_editable_tags = $form_state->getValue(['editor', 'settings', 'plugins', 'ckeditor5_sourceEditing', 'allowed_tags']);
-  if ($limit_allowed_html_tags && is_array($manually_editable_tags)) {
-    // When "Manually editable tags" and "limit allowed HTML tags" are both
-    // configured, the former informs the value of the latter. This dependent
+  $styles = $form_state->getValue(['editor', 'settings', 'plugins', 'ckeditor5_style', 'styles']);
+  if ($limit_allowed_html_tags && is_array($manually_editable_tags) || is_array($styles)) {
+    // When "Manually editable tags", "Style" and "limit allowed HTML tags" are
+    // all configured, the latter is dependent on the others. This dependent
     // value is typically updated via AJAX, but it's possible for "Manually
     // editable tags" to update without triggering the AJAX rebuild. That value
     // is recalculated here on save to ensure it happens even if the AJAX
     // rebuild doesn't happen.
-    $manually_editable_tags_restrictions = HTMLRestrictions::fromString(implode($manually_editable_tags));
+    $manually_editable_tags_restrictions = HTMLRestrictions::fromString(implode($manually_editable_tags ?? []));
+    $styles_restrictions = HTMLRestrictions::fromString(implode($styles ? array_column($styles, 'element') : []));
     $format = $form_state->get('ckeditor5_validated_pair')->getFilterFormat();
     $allowed_html = HTMLRestrictions::fromTextFormat($format);
-    $combined_tags_string = $manually_editable_tags_restrictions->merge($allowed_html)->toFilterHtmlAllowedTagsString();
+    $combined_tags_string = $allowed_html
+      ->merge($manually_editable_tags_restrictions)
+      ->merge($styles_restrictions)
+      ->toFilterHtmlAllowedTagsString();
     $form_state->setValue(['filters', 'filter_html', 'settings', 'allowed_html'], $combined_tags_string);
   }
 }
diff --git a/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml b/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml
index c58dce3fed5ba9f56b08756678969f0ca529d5ae..7b358f1e2adb301cee0546516c1b3c5b938ace12 100644
--- a/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml
+++ b/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml
@@ -73,6 +73,7 @@ ckeditor5.plugin.ckeditor5_imageResize:
       constraints:
         NotNull: []
 
+# Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing
 ckeditor5.plugin.ckeditor5_sourceEditing:
   type: mapping
   label: Source Editing
@@ -134,3 +135,35 @@ ckeditor5.plugin.media_media:
       label: 'Allow view mode override'
       constraints:
         NotNull: []
+
+# Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style
+ckeditor5.plugin.ckeditor5_style:
+  type: mapping
+  label: Style
+  mapping:
+    styles:
+      type: sequence
+      label: 'Styles'
+      constraints:
+        NotBlank:
+          message: "Enable at least one style, otherwise disable the Style plugin."
+        UniqueLabelInList:
+          labelKey: label
+      sequence:
+        type: mapping
+        label: 'Style'
+        mapping:
+          label:
+            type: label
+            label: 'Style label'
+          element:
+            type: ckeditor5.element
+            constraints:
+              # Validate that this contains exactly 1 attribute (class) and >=1 class attr value.
+              CKEditor5Element:
+                requiredAttributes:
+                  -
+                    attributeName: class
+                    minAttributeValueCount: 1
+              StyleSensibleElement: []
+            label: 'Style tag + classes'
diff --git a/core/modules/ckeditor5/css/style.admin.css b/core/modules/ckeditor5/css/style.admin.css
new file mode 100644
index 0000000000000000000000000000000000000000..6cb5b6bd5303dcc122c913593f889e70727f4901
--- /dev/null
+++ b/core/modules/ckeditor5/css/style.admin.css
@@ -0,0 +1,30 @@
+.ckeditor5-toolbar-button-style {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 110px;
+  color: #000;
+}
+.ckeditor5-toolbar-button-style::before {
+  margin-left: 10px;
+  content: "Style";
+  font-size: 14px;
+}
+[dir="rtl"] .ckeditor5-toolbar-button-style::before {
+  margin-right: 10px;
+  margin-left: 0;
+}
+.ckeditor5-toolbar-button-style::after {
+  display: inline-block;
+  width: 7px;
+  height: 7px;
+  margin-right: 10px;
+  content: "";
+  transform: rotate(135deg);
+  border-width: 2px 2px 0 0;
+  border-style: solid;
+}
+[dir="rtl"] .ckeditor5-toolbar-button-style::after {
+  margin-right: 0;
+  margin-left: 10px;
+}
diff --git a/core/modules/ckeditor5/js/build/ckeditor5.types.jsdoc b/core/modules/ckeditor5/js/build/ckeditor5.types.jsdoc
index 188458461d8573c926fa6717ac06244c0b68ec58..64d6266a285bccd1843281c78819dccb5b80920c 100644
--- a/core/modules/ckeditor5/js/build/ckeditor5.types.jsdoc
+++ b/core/modules/ckeditor5/js/build/ckeditor5.types.jsdoc
@@ -1942,6 +1942,54 @@
  * @typedef {module:special-characters/ui/specialcharactersnavigationview} module:special-characters/ui/specialcharactersnavigationview~SpecialCharactersNavigationView
  */
 
+/**
+ * Declared in file @ckeditor/ckeditor5-style/src/style.js
+ *
+ * @typedef {module:style/style} module:style/style~Style
+ */
+
+/**
+ * Declared in file @ckeditor/ckeditor5-style/src/stylecommand.js
+ *
+ * @typedef {module:style/stylecommand} module:style/stylecommand~StyleCommand
+ */
+
+/**
+ * Declared in file @ckeditor/ckeditor5-style/src/styleediting.js
+ *
+ * @typedef {module:style/styleediting} module:style/styleediting~StyleEditing
+ */
+
+/**
+ * Declared in file @ckeditor/ckeditor5-style/src/styleui.js
+ *
+ * @typedef {module:style/styleui} module:style/styleui~StyleUI
+ */
+
+/**
+ * Declared in file @ckeditor/ckeditor5-style/src/ui/stylegridbuttonview.js
+ *
+ * @typedef {module:style/ui/stylegridbuttonview} module:style/ui/stylegridbuttonview~StyleGridButtonView
+ */
+
+/**
+ * Declared in file @ckeditor/ckeditor5-style/src/ui/stylegridview.js
+ *
+ * @typedef {module:style/ui/stylegridview} module:style/ui/stylegridview~StyleGridView
+ */
+
+/**
+ * Declared in file @ckeditor/ckeditor5-style/src/ui/stylegroupview.js
+ *
+ * @typedef {module:style/ui/stylegroupview} module:style/ui/stylegroupview~StyleGroupView
+ */
+
+/**
+ * Declared in file @ckeditor/ckeditor5-style/src/ui/stylepanelview.js
+ *
+ * @typedef {module:style/ui/stylepanelview} module:style/ui/stylepanelview~StylePanelView
+ */
+
 /**
  * Declared in file @ckeditor/ckeditor5-table/src/commands/insertcolumncommand.js
  *
diff --git a/core/modules/ckeditor5/js/ckeditor5.style.admin.es6.js b/core/modules/ckeditor5/js/ckeditor5.style.admin.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..f44521613513125938c0575774537efd180d553e
--- /dev/null
+++ b/core/modules/ckeditor5/js/ckeditor5.style.admin.es6.js
@@ -0,0 +1,39 @@
+/**
+ * @file
+ * CKEditor 5 Style admin behavior.
+ */
+
+(function ($, Drupal) {
+  /**
+   * Provides the summary for the "style" plugin settings vertical tab.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches summary behavior to the plugin settings vertical tab.
+   */
+  Drupal.behaviors.ckeditor5StyleSettingsSummary = {
+    attach() {
+      $('[data-ckeditor5-plugin-id="ckeditor5_style"]').drupalSetSummary(
+        (context) => {
+          const stylesElement = document.querySelector(
+            '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]',
+          );
+          const styleCount = stylesElement.value
+            .split('\n')
+            // Minimum length is 5: "p.z|Z" is the shortest possible style definition.
+            .filter((line) => line.trim().length >= 5).length;
+
+          if (styleCount === 0) {
+            return Drupal.t('No styles configured');
+          }
+          return Drupal.formatPlural(
+            styleCount,
+            'One style configured',
+            '@count styles configured',
+          );
+        },
+      );
+    },
+  };
+})(jQuery, Drupal);
diff --git a/core/modules/ckeditor5/js/ckeditor5.style.admin.js b/core/modules/ckeditor5/js/ckeditor5.style.admin.js
new file mode 100644
index 0000000000000000000000000000000000000000..ffdf5db5c46812d4e783bcb254904c6c243807de
--- /dev/null
+++ b/core/modules/ckeditor5/js/ckeditor5.style.admin.js
@@ -0,0 +1,24 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function ($, Drupal) {
+  Drupal.behaviors.ckeditor5StyleSettingsSummary = {
+    attach() {
+      $('[data-ckeditor5-plugin-id="ckeditor5_style"]').drupalSetSummary(context => {
+        const stylesElement = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
+        const styleCount = stylesElement.value.split('\n').filter(line => line.trim().length >= 5).length;
+
+        if (styleCount === 0) {
+          return Drupal.t('No styles configured');
+        }
+
+        return Drupal.formatPlural(styleCount, 'One style configured', '@count styles configured');
+      });
+    }
+
+  };
+})(jQuery, Drupal);
\ No newline at end of file
diff --git a/core/modules/ckeditor5/src/HTMLRestrictions.php b/core/modules/ckeditor5/src/HTMLRestrictions.php
index 4057271530be61279290e1f5beed2f065ffcbdc3..bf81037fef9d75c1aca95aae9e03e7859974785c 100644
--- a/core/modules/ckeditor5/src/HTMLRestrictions.php
+++ b/core/modules/ckeditor5/src/HTMLRestrictions.php
@@ -9,6 +9,7 @@
 use Drupal\filter\FilterFormatInterface;
 use Drupal\filter\Plugin\Filter\FilterHtml;
 use Drupal\filter\Plugin\FilterInterface;
+use Masterminds\HTML5\Elements;
 
 /**
  * Represents a set of HTML restrictions.
@@ -66,6 +67,7 @@ final class HTMLRestrictions {
    * @var string[]
    */
   private const WILDCARD_ELEMENT_METHODS = [
+    '$any-html5-element' => 'getHtml5ElementList',
     '$text-container' => 'getTextContainerElementList',
   ];
 
@@ -1188,6 +1190,16 @@ private static function getTextContainerElementList(): array {
     ];
   }
 
+  /**
+   * Gets a list of all known HTML5 elements.
+   *
+   * @return string[]
+   *   An array of HTML5 element tags.
+   */
+  private static function getHtml5ElementList(): array {
+    return array_keys(Elements::$html5);
+  }
+
   /**
    * Computes the tags that match the provided wildcard.
    *
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor4To5Upgrade/Core.php b/core/modules/ckeditor5/src/Plugin/CKEditor4To5Upgrade/Core.php
index 25e132222a18da49870e17d3df4813d420a292ec..14b3c74eebb0df87f043060911e217a815ec0e7a 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor4To5Upgrade/Core.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor4To5Upgrade/Core.php
@@ -6,6 +6,7 @@
 
 use Drupal\ckeditor5\HTMLRestrictions;
 use Drupal\ckeditor5\Plugin\CKEditor4To5UpgradePluginInterface;
+use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style;
 use Drupal\Core\Plugin\PluginBase;
 use Drupal\filter\FilterFormatInterface;
 
@@ -58,10 +59,11 @@
  *     "language",
  *   },
  *   cke5_plugin_elements_subset_configuration = {
- *    "ckeditor5_heading",
- *    "ckeditor5_alignment",
- *    "ckeditor5_list",
- *    "media_media",
+ *     "ckeditor5_heading",
+ *     "ckeditor5_alignment",
+ *     "ckeditor5_list",
+ *     "ckeditor5_style",
+ *     "media_media",
  *   }
  * )
  *
@@ -163,8 +165,7 @@ public function mapCKEditor4ToolbarButtonToCKEditor5ToolbarItem(string $cke4_but
 
       // @see \Drupal\ckeditor\Plugin\CKEditorPlugin\StylesCombo
       case 'Styles':
-        // @todo Change in https://www.drupal.org/project/ckeditor5/issues/3222797
-        return NULL;
+        return ['style'];
 
       // @see \Drupal\ckeditor5\Plugin\CKEditor5Plugin\specialCharacters
       case 'SpecialChar':
@@ -190,8 +191,17 @@ public function mapCKEditor4SettingsToCKEditor5Configuration(string $cke4_plugin
     switch ($cke4_plugin_id) {
       // @see \Drupal\ckeditor\Plugin\CKEditorPlugin\StylesCombo
       case 'stylescombo':
-        // @todo Change in https://www.drupal.org/project/ckeditor5/issues/3222797
-        return NULL;
+        if (!isset($cke4_plugin_settings['styles'])) {
+          $styles = [];
+        }
+        else {
+          [$styles] = Style::parseStylesFormValue($cke4_plugin_settings['styles']);
+        }
+        return [
+          'ckeditor5_style' => [
+            'styles' => $styles,
+          ],
+        ];
 
       // @see \Drupal\ckeditor\Plugin\CKEditorPlugin\Language
       case 'language':
@@ -284,6 +294,10 @@ public function computeCKEditor5PluginSubsetConfiguration(string $cke5_plugin_id
         $configuration['allow_view_mode_override'] = !empty($restrictions['allowed']['drupal-media']['data-view-mode']);
         return $configuration;
 
+      case 'ckeditor5_style':
+        // @see mapCKEditor4SettingsToCKEditor5Configuration()
+        return NULL;
+
       default:
         throw new \OutOfBoundsException();
     }
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php
index 3adfaa3040ba3f1d02f0f70ca3cfde0dbafdb884..6e9ea5bf2807b91925d0bc829afbc4300762c1dd 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php
@@ -40,12 +40,12 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
    * {@inheritdoc}
    */
   public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
-    // Match the config schema structure at ckeditor5.plugin.ckeditor5_heading.
+    // Match the config schema structure at
+    // ckeditor5.plugin.ckeditor5_sourceEditing.
     $form_value = $form_state->getValue('allowed_tags');
-    if (!is_array($form_value)) {
-      $config_value = HTMLRestrictions::fromString($form_value)->toCKEditor5ElementsArray();
-      $form_state->setValue('allowed_tags', $config_value);
-    }
+    assert(is_string($form_value));
+    $config_value = HTMLRestrictions::fromString($form_value)->toCKEditor5ElementsArray();
+    $form_state->setValue('allowed_tags', $config_value);
   }
 
   /**
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Style.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Style.php
new file mode 100644
index 0000000000000000000000000000000000000000..89e2d23c94166c44cccc6ee147bf9581d076f8cf
--- /dev/null
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Style.php
@@ -0,0 +1,185 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\ckeditor5\Plugin\CKEditor5Plugin;
+
+use Drupal\ckeditor5\HTMLRestrictions;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginElementsSubsetInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\editor\EditorInterface;
+
+/**
+ * CKEditor 5 Style plugin configuration.
+ *
+ * @internal
+ *   Plugin classes are internal.
+ */
+class Style extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface, CKEditor5PluginElementsSubsetInterface {
+
+  use CKEditor5PluginConfigurableTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form['styles'] = [
+      '#title' => $this->t('Styles'),
+      '#type' => 'textarea',
+      '#description' => $this->t('A list of classes that will be provided in the "Style" dropdown. Enter one or more classes on each line in the format: element.classA.classB|Label. Example: h1.title|Title. Advanced example: h1.fancy.title|Fancy title.<br />These styles should be available in your theme\'s CSS file.'),
+    ];
+    if (!empty($this->configuration['styles'])) {
+      $as_selectors = '';
+      foreach ($this->configuration['styles'] as $style) {
+        [$tag, $classes] = self::getTagAndClasses(HTMLRestrictions::fromString($style['element']));
+        $as_selectors .= sprintf("%s.%s|%s\n", $tag, implode('.', $classes), $style['label']);
+      }
+      $form['styles']['#default_value'] = $as_selectors;
+    }
+
+    return $form;
+  }
+
+  /**
+   * Gets the tag and classes for a parsed style element.
+   *
+   * @param \Drupal\ckeditor5\HTMLRestrictions $style_element
+   *   A parsed style element.
+   *
+   * @return array
+   *   An array containing two values:
+   *   - a HTML tag name
+   *   - a list of classes
+   *
+   * @internal
+   */
+  public static function getTagAndClasses(HTMLRestrictions $style_element): array {
+    $tag = array_keys($style_element->getAllowedElements())[0];
+    $classes = array_keys($style_element->getAllowedElements()[$tag]['class']);
+    return [$tag, $classes];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    // Match the config schema structure at ckeditor5.plugin.ckeditor5_style.
+    $form_value = $form_state->getValue('styles');
+    [$styles, $unparseable_lines] = self::parseStylesFormValue($form_value);
+    if (!empty($unparseable_lines)) {
+      $line_numbers = array_keys($unparseable_lines);
+      $form_state->setError($form['styles'], $this->formatPlural(
+        count($unparseable_lines),
+        'Line @line-number does not contain a valid value. Enter a valid CSS selector containing one or more classes, followed by a pipe symbol and a label.',
+        'Lines @line-numbers do not contain a valid value. Enter a valid CSS selector containing one or more classes, followed by a pipe symbol and a label.',
+        [
+          '@line-number' => reset($line_numbers),
+          '@line-numbers' => implode(', ', $line_numbers),
+        ]
+      ));
+    }
+    $form_state->setValue('styles', $styles);
+  }
+
+  /**
+   * Parses the line-based (for form) style configuration.
+   *
+   * @param string $form_value
+   *   A string containing >=1 lines with on each line a CSS selector targeting
+   *   1 tag with >=1 classes, a pipe symbol and a label. An example of a single
+   *   line: `p.foo.bar|Foo bar paragraph`.
+   *
+   * @return array
+   *   The parsed equivalent: a list of arrays with each containing:
+   *   - label: the label after the pipe symbol, with whitespace trimmed
+   *   - element: the CKEditor 5 element equivalent of the tag + classes
+   *
+   * @internal
+   *   This method is public only to allow the CKEditor 4 to 5 upgrade path to
+   *   reuse this logic. Mark this private in https://www.drupal.org/i/3239012.
+   *
+   * @see \Drupal\ckeditor5\Plugin\CKEditor4To5Upgrade\Core::mapCKEditor4SettingsToCKEditor5Configuration()
+   */
+  public static function parseStylesFormValue(string $form_value): array {
+    $unparseable_lines = [];
+
+    $lines = explode("\n", $form_value);
+    $styles = [];
+    foreach ($lines as $index => $line) {
+      if (empty(trim($line))) {
+        continue;
+      }
+
+      // Parse the line.
+      [$selector, $label] = array_map('trim', explode('|', $line));
+
+      // Validate the selector.
+      $selector_matches = [];
+      // @see https://www.w3.org/TR/CSS2/syndata.html#:~:text=In%20CSS%2C%20identifiers%20(including%20element,hyphen%20followed%20by%20a%20digit
+      if (!preg_match('/^([a-z][0-9a-zA-Z\-]*)((\.[a-zA-Z0-9\x{00A0}-\x{FFFF}\-_]+)+)$/u', $selector, $selector_matches)) {
+        $unparseable_lines[$index + 1] = $line;
+        continue;
+      }
+
+      // Parse selector into tag + classes and normalize.
+      $tag = $selector_matches[1];
+      $classes = array_filter(explode('.', $selector_matches[2]));
+      $normalized = HTMLRestrictions::fromString(sprintf('<%s class="%s">', $tag, implode(' ', $classes)));
+
+      $styles[] = [
+        'label' => $label,
+        'element' => $normalized->toCKEditor5ElementsArray()[0],
+      ];
+    }
+    return [$styles, $unparseable_lines];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->configuration['styles'] = $form_state->getValue('styles');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'styles' => [],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementsSubset(): array {
+    return array_column($this->configuration['styles'], 'element');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array {
+    $definitions = [];
+    foreach ($this->configuration['styles'] as $style) {
+      [$tag, $classes] = self::getTagAndClasses(HTMLRestrictions::fromString($style['element']));
+      // Transform configured styles to the configuration structure expected by
+      // the CKEditor 5 Style plugin.
+      $definitions[] = [
+        'name' => $style['label'],
+        'element' => $tag,
+        'classes' => $classes,
+      ];
+    }
+    return [
+      'style' => [
+        'definitions' => $definitions,
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
index a7933a122e9353ec528bb99e3f0c7fa136a26b5e..fe2d7992cd013ca49a6a76714a3574d67f3e9ae1 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
@@ -331,10 +331,26 @@ public function getProvidedElements(array $plugin_ids = [], EditorInterface $edi
           $subset = $this->getPlugin($id, $editor)->getElementsSubset();
           $subset_restrictions = HTMLRestrictions::fromString(implode($subset));
           $defined_restrictions = HTMLRestrictions::fromString(implode($defined_elements));
-          $subset_violations = $subset_restrictions->diff($defined_restrictions)->toCKEditor5ElementsArray();
-          if (!empty($subset_violations)) {
-            throw new \LogicException(sprintf('The "%s" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: "%s".', $id, implode(' ', $subset_violations)));
+          // Determine max supported elements by resolving wildcards in the
+          // restrictions defined by the plugin.
+          $max_supported = $defined_restrictions;
+          if (!$defined_restrictions->getWildcardSubset()->allowsNothing()) {
+            $concrete_tags_to_use_to_resolve_wildcards = $subset_restrictions->extractPlainTagsSubset();
+            $max_supported = $max_supported->merge($concrete_tags_to_use_to_resolve_wildcards)
+              ->diff($concrete_tags_to_use_to_resolve_wildcards);
           }
+          $not_in_max_supported = $subset_restrictions->diff($max_supported);
+          if (!$not_in_max_supported->allowsNothing()) {
+            // If the editor is still being configured, the configuration may
+            // not yet be valid.
+            if ($editor->isNew()) {
+              $subset = [];
+            }
+            else {
+              throw new \LogicException(sprintf('The "%s" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: "%s".', $id, implode(' ', $not_in_max_supported->toCKEditor5ElementsArray())));
+            }
+          }
+
           // Also detect what is technically a valid subset, but has lost the
           // ability to create tags that are still in the subset. This points to
           // a bug in the plugin's ::getElementsSubset() logic.
@@ -346,6 +362,7 @@ public function getProvidedElements(array $plugin_ids = [], EditorInterface $edi
           if (!$missing_creatable_for_subset->allowsNothing()) {
             throw new \LogicException(sprintf('The "%s" CKEditor 5 plugin implements ::getElementsSubset() and did return a subset ("%s") but the following tags can no longer be created: "%s".', $id, implode($subset_restrictions->toCKEditor5ElementsArray()), implode($missing_creatable_for_subset->toCKEditor5ElementsArray())));
           }
+
           $defined_elements = $subset;
         }
       }
diff --git a/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php b/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php
index 3bc43daf7eebaa257536a0c0446f96a851aebe68..eb3c6df06e2d80c0607c899c8bba1137af2a9f26 100644
--- a/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php
+++ b/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php
@@ -623,6 +623,7 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
     $submitted_editor->setSettings($settings);
     $eventual_editor_and_format_for_plugin_settings_visibility = $this->getEventualEditorWithPrimedFilterFormat($form_state, $submitted_editor);
     $settings['plugins'] = [];
+    $default_configurations = [];
     foreach ($this->ckeditor5PluginManager->getDefinitions() as $plugin_id => $definition) {
       if (!$definition->isConfigurable()) {
         continue;
@@ -636,6 +637,12 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
       // @see editor_image_upload_settings_form()
       $default_configuration = $plugin->defaultConfiguration();
       $configuration_stored_out_of_band = empty($default_configuration);
+      // If this plugin is configurable but has not yet had user interaction,
+      // the default configuration will still be active and may trigger
+      // validation errors. Do not trigger those validation errors until the
+      // form is actually saved, to allow the user to first configure other
+      // CKEditor 5 functionality.
+      $default_configurations[$plugin_id] = $default_configuration;
 
       if ($form_state->hasValue(['plugins', $plugin_id])) {
         $subform = $form['plugins'][$plugin_id];
@@ -672,6 +679,31 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
     $eventual_editor_and_format = $this->getEventualEditorWithPrimedFilterFormat($form_state, $submitted_editor);
     $violations = CKEditor5::validatePair($eventual_editor_and_format, $eventual_editor_and_format->getFilterFormat());
     foreach ($violations as $violation) {
+      $property_path_parts = explode('.', $violation->getPropertyPath());
+
+      // Special case: AJAX updates that do not submit the form (that cannot
+      // result in configuration being saved).
+      if ($form_state->getSubmitHandlers() === ['editor_form_filter_admin_format_editor_configure']) {
+        // Ensure that plugins' validation constraints do not immediately
+        // trigger a validation error: the user may choose to configure other
+        // CKEditor 5 aspects first.
+        if ($property_path_parts[0] === 'settings' && $property_path_parts[1] === 'plugins') {
+          $plugin_id = $property_path_parts[2];
+          // This CKEditor 5 plugin settings form was just added: the user has
+          // not yet had a chance to configure it.
+          if (!$form_state->hasValue(['plugins', $plugin_id])) {
+            continue;
+          }
+          // This CKEditor 5 plugin settings form was added recently, the user
+          // is triggering AJAX rebuilds of the configuration UI because they're
+          // configuring other functionality first. Only require these to be
+          // valid at form submission time.
+          if ($form_state->getValue(['plugins', $plugin_id]) === $default_configurations[$plugin_id]) {
+            continue;
+          }
+        }
+      }
+
       $form_item_name = static::mapPairViolationPropertyPathsToFormNames($violation->getPropertyPath(), $form);
       // When adding a toolbar item, it is possible that not all conditions for
       // using it have been met yet. FormBuilder refuses to rebuild forms when a
@@ -816,6 +848,11 @@ protected function getEventualEditorWithPrimedFilterFormat(SubformStateInterface
    */
   protected static function createEphemeralPairedEditor(EditorInterface $editor, FilterFormatInterface $filter_format): EditorInterface {
     $paired_editor = clone $editor;
+    // If the editor is still being configured, the configuration may not yet be
+    // valid. Explicitly mark the ephemeral paired editor as new to allow other
+    // code to treat this accordingly.
+    // @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getProvidedElements()
+    $paired_editor->enforceIsNew(TRUE);
     $reflector = new \ReflectionObject($paired_editor);
     $property = $reflector->getProperty('filterFormat');
     $property->setAccessible(TRUE);
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraint.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraint.php
index b9c3dc6062afcf327dc193d282feba2c1435377c..a80feafd4252423d670bde276874cc0321dc4cd6 100644
--- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraint.php
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraint.php
@@ -23,4 +23,25 @@ class CKEditor5ElementConstraint extends Constraint {
    */
   public $message = 'The following tag is not valid HTML: %provided_element.';
 
+  /**
+   * Violation message when a required attribute is missing.
+   *
+   * @var string
+   */
+  public $missingRequiredAttributeMessage = 'The following tag is missing the required attribute <code>@required_attribute_name</code>: <code>@provided_element</code>.';
+
+  /**
+   * Violation message when a required attribute does not allow enough values.
+   *
+   * @var string
+   */
+  public $requiredAttributeMinValuesMessage = 'The following tag does not have the minimum of @min_attribute_value_count allowed values for the required attribute <code>@required_attribute_name</code>: <code>@provided_element</code>.';
+
+  /**
+   * Validation constraint option to impose attributes to be specified.
+   *
+   * @var null|array
+   */
+  public $requiredAttributes = NULL;
+
 }
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php
index 285e57bc607bb011fe17320842a729ba330fa408..c8a361449f10ccfc490545fa18777f1ae3bdde11 100644
--- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php
@@ -32,6 +32,41 @@ public function validate($element, $constraint) {
         ->setParameter('%provided_element', $element)
         ->addViolation();
     }
+
+    // The optional "requiredAttributes" constraint property allows more
+    // detailed validation.
+    if (isset($constraint->requiredAttributes)) {
+      $allowed_elements = $parsed->getAllowedElements();
+      $tag = array_keys($allowed_elements)[0];
+      $attribute_restrictions = $allowed_elements[$tag];
+      assert(is_array($constraint->requiredAttributes));
+      foreach ($constraint->requiredAttributes as $required_attribute) {
+        // Validate attributeName.
+        $required_attribute_name = $required_attribute['attributeName'];
+        if (!is_array($attribute_restrictions) || !isset($attribute_restrictions[$required_attribute_name])) {
+          $this->context->buildViolation($constraint->missingRequiredAttributeMessage)
+            ->setParameter('@provided_element', $element)
+            ->setParameter('@required_attribute_name', $required_attribute_name)
+            ->addViolation();
+          continue;
+        }
+
+        $attribute_values = $attribute_restrictions[$required_attribute_name];
+
+        // Validate minAttributeValueCount if specified.
+        if (isset($required_attribute['minAttributeValueCount'])) {
+          $min_attribute_value_count = $required_attribute['minAttributeValueCount'];
+          if (!is_array($attribute_values) || count($attribute_values) < $min_attribute_value_count) {
+            $this->context->buildViolation($constraint->requiredAttributeMinValuesMessage)
+              ->setParameter('@provided_element', $element)
+              ->setParameter('@required_attribute_name', $required_attribute_name)
+              ->setParameter('@min_attribute_value_count', $min_attribute_value_count)
+              ->addViolation();
+            continue;
+          }
+        }
+      }
+    }
   }
 
 }
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/FundamentalCompatibilityConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/FundamentalCompatibilityConstraintValidator.php
index cb8ce30579b9108b26f9e18b88815715620d86f5..1cac8c16b15c8df37c9d4bfb7cfb1ec3ef7cbe9e 100644
--- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/FundamentalCompatibilityConstraintValidator.php
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/FundamentalCompatibilityConstraintValidator.php
@@ -6,6 +6,7 @@
 
 use Drupal\ckeditor5\HTMLRestrictions;
 use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginElementsSubsetInterface;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\editor\EditorInterface;
 use Drupal\filter\FilterFormatInterface;
@@ -199,32 +200,36 @@ private function checkAllHtmlTagsAreCreatable(EditorInterface $text_editor, Fund
       foreach ($non_creatable_tags->toCKEditor5ElementsArray() as $non_creatable_tag) {
         // Find the plugin which has a non-creatable tag.
         $needle = HTMLRestrictions::fromString($non_creatable_tag);
-        $matching_plugins = array_filter($enabled_definitions, function (CKEditor5PluginDefinition $d) use ($needle) {
+        $matching_plugins = array_filter($enabled_definitions, function (CKEditor5PluginDefinition $d) use ($needle, $text_editor) {
           if (!$d->hasElements()) {
             return FALSE;
           }
-          $haystack = HTMLRestrictions::fromString(implode($d->getElements()));
-          return !$haystack->intersect($needle)->allowsNothing();
+          $haystack = new HTMLRestrictions($this->pluginManager->getProvidedElements([$d->id()], $text_editor, FALSE, FALSE));
+          return !$haystack->extractPlainTagsSubset()->intersect($needle)->allowsNothing();
         });
         assert(count($matching_plugins) === 1);
         $plugin_definition = reset($matching_plugins);
         assert($plugin_definition instanceof CKEditor5PluginDefinition);
 
         // Compute which attributes it would be able to create on this tag.
-        $matching_elements = array_filter($plugin_definition->getElements(), function (string $element) use ($needle) {
-          $haystack = HTMLRestrictions::fromString($element);
-          return !$haystack->intersect($needle)->allowsNothing();
-        });
-        $attributes_on_tag = HTMLRestrictions::fromString(implode($matching_elements));
+        $provided_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements([$plugin_definition->id()], $text_editor, FALSE, FALSE));
+        $attributes_on_tag = $provided_elements->intersect(
+          new HTMLRestrictions(array_fill_keys(array_keys($needle->getAllowedElements()), TRUE))
+        );
 
         $violation = $this->context->buildViolation($constraint->nonCreatableTagMessage)
           ->setParameter('@non_creatable_tag', $non_creatable_tag)
           ->setParameter('%plugin', $plugin_definition->label())
           ->setParameter('@attributes_on_tag', implode(', ', $attributes_on_tag->toCKEditor5ElementsArray()));
 
+        // If this plugin has a configurable subset, associate the violation
+        // with the property path pointing to this plugin's settings form.
+        if (is_a($plugin_definition->getClass(), CKEditor5PluginElementsSubsetInterface::class, TRUE)) {
+          $violation->atPath(sprintf('settings.plugins.%s', $plugin_definition->id()));
+        }
         // If this plugin is associated with a toolbar item, associate the
         // violation with the property path pointing to the active toolbar item.
-        if ($plugin_definition->hasToolbarItems()) {
+        elseif ($plugin_definition->hasToolbarItems()) {
           $toolbar_items = $plugin_definition->getToolbarItems();
           $active_toolbar_items = array_intersect(
             $text_editor->getSettings()['toolbar']['items'],
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PluginManagerDependentValidatorTrait.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PluginManagerDependentValidatorTrait.php
index ae52ece32d484246690767252b43b442dcd950cc..12d4ec8a4da1fcc0677fac9f487aaef2a3d5d1df 100644
--- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PluginManagerDependentValidatorTrait.php
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PluginManagerDependentValidatorTrait.php
@@ -4,7 +4,11 @@
 
 namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
 
+// cspell:ignore enableable
+
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
 use Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface;
+use Drupal\editor\EditorInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -40,4 +44,49 @@ public static function create(ContainerInterface $container) {
     );
   }
 
+  /**
+   * Gets all other enabled CKEditor 5 plugin definitions.
+   *
+   * @param \Drupal\editor\EditorInterface $text_editor
+   *   A Text Editor config entity configured to use CKEditor 5.
+   * @param string $except
+   *   A CKEditor 5 plugin ID to exclude: all enabled plugins other than this
+   *   one are returned.
+   *
+   * @return \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition[]
+   *   A list of CKEditor 5 plugin definitions keyed by plugin ID.
+   */
+  private function getOtherEnabledPlugins(EditorInterface $text_editor, string $except): array {
+    $enabled_plugins = $this->pluginManager->getEnabledDefinitions($text_editor);
+    unset($enabled_plugins[$except]);
+    return $enabled_plugins;
+  }
+
+  /**
+   * Gets all disabled CKEditor 5 plugin definitions the user can enable.
+   *
+   * @param \Drupal\editor\EditorInterface $text_editor
+   *   A Text Editor config entity configured to use CKEditor 5.
+   *
+   * @return \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition[]
+   *   A list of CKEditor 5 plugin definitions keyed by plugin ID.
+   */
+  private function getEnableableDisabledPlugins(EditorInterface $text_editor) {
+    $disabled_plugins = array_diff_key(
+      $this->pluginManager->getDefinitions(),
+      $this->pluginManager->getEnabledDefinitions($text_editor)
+    );
+    // Only consider plugins that can be explicitly enabled by the user: plugins
+    // that have a toolbar item and do not have conditions. Those are the only
+    // plugins that are truly available for the site builder to enable without
+    // other consequences.
+    // In the future, we may choose to expand this, but it will require complex
+    // infrastructure to generate messages that explain which of the conditions
+    // are already fulfilled and which are not.
+    $enableable_disabled_plugins = array_filter($disabled_plugins, function (CKEditor5PluginDefinition $definition) {
+      return $definition->hasToolbarItems() && !$definition->hasConditions();
+    });
+    return $enableable_disabled_plugins;
+  }
+
 }
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PrecedingConstraintAwareValidatorTrait.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PrecedingConstraintAwareValidatorTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..0e6ae75076cc7619051d56998a0bc8489bdcacb2
--- /dev/null
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PrecedingConstraintAwareValidatorTrait.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
+
+use Drupal\Core\TypedData\Validation\ExecutionContext;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintViolationInterface;
+
+/**
+ * A constraint may need preceding constraints to not have been violated.
+ *
+ * @internal
+ */
+trait PrecedingConstraintAwareValidatorTrait {
+
+  /**
+   * Checks whether any preceding constraints have been violated.
+   *
+   * @param \Symfony\Component\Validator\Constraint $current_constraint
+   *   The constraint currently being validated.
+   *
+   * @return bool
+   *   TRUE if any preceding constraints have been violated, FALSE otherwise.
+   */
+  protected function hasViolationsForPrecedingConstraints(Constraint $current_constraint): bool {
+    assert($this->context instanceof ExecutionContext);
+    $earlier_constraints = iterator_to_array($this->getPrecedingConstraints($current_constraint));
+    $earlier_violations = array_filter(
+      iterator_to_array($this->context->getViolations()),
+      function (ConstraintViolationInterface $violation) use ($earlier_constraints) {
+        return in_array($violation->getConstraint(), $earlier_constraints);
+      }
+    );
+    return !empty($earlier_violations);
+  }
+
+  /**
+   * Gets the constraints preceding the given constraint in the current context.
+   *
+   * @param \Symfony\Component\Validator\Constraint $needle
+   *   The constraint to find the preceding constraints for.
+   *
+   * @return iterable
+   *   The preceding constraints.
+   */
+  private function getPrecedingConstraints(Constraint $needle): iterable {
+    assert($this->context instanceof ExecutionContext);
+    $constraints = $this->context->getMetadata()->getConstraints();
+    if (!in_array($needle, $constraints)) {
+      throw new \OutOfBoundsException();
+    }
+    foreach ($constraints as $constraint) {
+      if ($constraint != $needle) {
+        yield $constraint;
+      }
+    }
+  }
+
+}
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php
index 0dcb00259448262545a2199c326332f5338ee095..f0ff31e5c5c4a8874e284c8f0aa9b42839107b91 100644
--- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php
@@ -4,8 +4,9 @@
 
 namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
 
+// cspell:ignore enableable
+
 use Drupal\ckeditor5\HTMLRestrictions;
-use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Symfony\Component\Validator\Constraint;
@@ -38,25 +39,15 @@ public function validate($value, Constraint $constraint) {
     }
 
     $text_editor = $this->createTextEditorObjectFromContext();
-    $enabled_plugins = $this->pluginManager->getEnabledDefinitions($text_editor);
-    $disabled_plugins = array_diff_key($this->pluginManager->getDefinitions(), $enabled_plugins);
-    // Only consider plugins that can be explicitly enabled by the user: plugins
-    // that have a toolbar item and do not have conditions. Those are the only
-    // plugins that are truly available for the site builder to enable without
-    // other consequences.
-    // In the future, we may choose to expand this, but it will require complex
-    // infrastructure to generate messages that explain which of the conditions
-    // are already fulfilled and which are not.
-    $disabled_plugins = array_filter($disabled_plugins, function (CKEditor5PluginDefinition $definition) {
-      return $definition->hasToolbarItems() && !$definition->hasConditions();
-    });
-    unset($enabled_plugins['ckeditor5_sourceEditing']);
+
+    $other_enabled_plugins = $this->getOtherEnabledPlugins($text_editor, 'ckeditor5_sourceEditing');
+    $enableable_disabled_plugins = $this->getEnableableDisabledPlugins($text_editor);
 
     // An array of tags enabled by every plugin other than Source Editing.
-    $enabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enabled_plugins), $text_editor, FALSE));
-    $disabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($disabled_plugins), $text_editor, FALSE));
-    $enabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enabled_plugins), $text_editor, FALSE, TRUE));
-    $disabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($disabled_plugins), $text_editor, FALSE, TRUE));
+    $enabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($other_enabled_plugins), $text_editor, FALSE));
+    $disabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enableable_disabled_plugins), $text_editor, FALSE));
+    $enabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($other_enabled_plugins), $text_editor, FALSE, TRUE));
+    $disabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enableable_disabled_plugins), $text_editor, FALSE, TRUE));
 
     // The single element for which source editing is enabled, which we are
     // checking now.
@@ -85,7 +76,7 @@ public function validate($value, Constraint $constraint) {
     foreach ([$enabled_plugin_overlap, $disabled_plugin_overlap] as $overlap) {
       $checking_enabled = $overlap === $enabled_plugin_overlap;
       if (!$overlap->allowsNothing()) {
-        $plugins_to_check_against = $checking_enabled ? $enabled_plugins : $disabled_plugins;
+        $plugins_to_check_against = $checking_enabled ? $other_enabled_plugins : $enableable_disabled_plugins;
         $plain_tags_to_check_against = $checking_enabled ? $enabled_plugin_plain_tags : $disabled_plugin_plain_tags;
         $tags_plugin_report = $this->pluginsSupplyingTagsMessage($overlap, $plugins_to_check_against, $enabled_plugin_elements);
         $message = $checking_enabled ? $constraint->enabledPluginsMessage : $constraint->availablePluginsMessage;
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraint.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..68d473dd7e8d68807d93d2d99de4d5da6dc2ae86
--- /dev/null
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraint.php
@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
+
+// cspell:ignore enableable
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Styles can only be specified for HTML5 tags and extra classes.
+ *
+ * @Constraint(
+ *   id = "StyleSensibleElement",
+ *   label = @Translation("Styles can only be specified for already supported tags.", context = "Validation"),
+ * )
+ *
+ * @internal
+ */
+class StyleSensibleElementConstraint extends Constraint {
+
+  /**
+   * When a style is defined for a non-HTML5 tag.
+   *
+   * @var string
+   */
+  public $nonHtml5TagMessage = 'A style can only be specified for an HTML 5 tag. <code>@tag</code> is not an HTML5 tag.';
+
+  /**
+   * When a Style is defined with classes supported by an enabled plugin.
+   *
+   * @var string
+   */
+  public $conflictingEnabledPluginMessage = 'A style must only specify classes not supported by other plugins. The <code>@classes</code> classes on <code>@tag</code> are already supported by the enabled %plugin plugin.';
+
+  /**
+   * When a Style is defined with classes supported by a disabled plugin.
+   *
+   * @var string
+   */
+  public $conflictingDisabledPluginMessage = 'A style must only specify classes not supported by other plugins. The <code>@classes</code> classes on <code>@tag</code> are supported by the %plugin plugin. Remove this style and enable that plugin instead.';
+
+}
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraintValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..4be7d8ba1402b272f055325487c7bdf5176d4833
--- /dev/null
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraintValidator.php
@@ -0,0 +1,165 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
+
+// cspell:ignore enableable
+
+use Drupal\ckeditor5\HTMLRestrictions;
+use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Styles can only be specified for HTML5 tags and extra classes.
+ *
+ * @internal
+ */
+class StyleSensibleElementConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  use PrecedingConstraintAwareValidatorTrait;
+  use PluginManagerDependentValidatorTrait;
+  use TextEditorObjectDependentValidatorTrait;
+
+  /**
+   * {@inheritdoc}
+   *
+   * @throws \Symfony\Component\Validator\Exception\UnexpectedTypeException
+   *   Thrown when the given constraint is not supported by this validator.
+   */
+  public function validate($element, Constraint $constraint) {
+    if (!$constraint instanceof StyleSensibleElementConstraint) {
+      throw new UnexpectedTypeException($constraint, StyleSensibleElementConstraint::class);
+    }
+    // The preceding constraints (in this case: CKEditor5Element) must be valid.
+    if ($this->hasViolationsForPrecedingConstraints($constraint)) {
+      return;
+    }
+
+    $text_editor = $this->createTextEditorObjectFromContext();
+
+    // The single tag for which a style is specified, which we are checking now.
+    $style_element = HTMLRestrictions::fromString($element);
+    assert(count($style_element->getAllowedElements()) === 1);
+    [$tag, $classes] = Style::getTagAndClasses($style_element);
+
+    // Ensure the tag is in the range supported by the Style plugin.
+    $superset = HTMLRestrictions::fromString('<$any-html5-element class>');
+    $supported_range = $superset->merge($style_element->extractPlainTagsSubset());
+    if (!$style_element->diff($supported_range)->allowsNothing()) {
+      $this->context->buildViolation($constraint->nonHtml5TagMessage)
+        ->setParameter('@tag', sprintf("<%s>", $tag))
+        ->addViolation();
+      return;
+    }
+
+    // Get the list of tags enabled by every plugin other than Style.
+    $other_enabled_plugins = $this->getOtherEnabledPlugins($text_editor, 'ckeditor5_style');
+    $enableable_disabled_plugins = $this->getEnableableDisabledPlugins($text_editor);
+
+    $other_enabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($other_enabled_plugins), $text_editor, FALSE));
+    $disabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enableable_disabled_plugins), $text_editor, FALSE));
+
+    // Next, validate that the classes specified for this style are not
+    // supported by an enabled plugin.
+    if (self::intersectionWithClasses($style_element, $other_enabled_plugin_elements)) {
+      $this->context->buildViolation($constraint->conflictingEnabledPluginMessage)
+        ->setParameter('@tag', sprintf("<%s>", $tag))
+        ->setParameter('@classes', implode(", ", $classes))
+        ->setParameter('%plugin', $this->findStyleConflictingPluginLabel($style_element))
+        ->addViolation();
+    }
+    // Next, validate that the classes specified for this style are not
+    // supported by a disabled plugin.
+    elseif (self::intersectionWithClasses($style_element, $disabled_plugin_elements)) {
+      $this->context->buildViolation($constraint->conflictingDisabledPluginMessage)
+        ->setParameter('@tag', sprintf("<%s>", $tag))
+        ->setParameter('@classes', implode(", ", $classes))
+        ->setParameter('%plugin', $this->findStyleConflictingPluginLabel($style_element))
+        ->addViolation();
+    }
+  }
+
+  /**
+   * Checks if there is an intersection on allowed 'class' attribute values.
+   *
+   * @param \Drupal\ckeditor5\HTMLRestrictions $a
+   *   One set of HTML restrictions.
+   * @param \Drupal\ckeditor5\HTMLRestrictions $b
+   *   Another set of HTML restrictions.
+   *
+   * @return bool
+   *   Whether there is an intersection.
+   */
+  private static function intersectionWithClasses(HTMLRestrictions $a, HTMLRestrictions $b): bool {
+    // Compute the intersection, but first resolve wildcards, by merging
+    // tags of the other operand. Because only tags are merged, this cannot
+    // introduce a 'class' attribute intersection.
+    // For example: a plugin may support `<$text-container class="foo">`. On its
+    // own that would not trigger an intersection, but when resolved into
+    // concrete tags it could.
+    $tags_from_a = array_diff(array_keys($a->getConcreteSubset()->getAllowedElements()), ['*']);
+    $tags_from_b = array_diff(array_keys($b->getConcreteSubset()->getAllowedElements()), ['*']);
+    $a = $a->merge(new HTMLRestrictions(array_fill_keys($tags_from_b, FALSE)));
+    $b = $b->merge(new HTMLRestrictions(array_fill_keys($tags_from_a, FALSE)));
+    $intersection = $a->intersect($b);
+
+    // Leverage the "GHS configuration" representation to easily find whether
+    // there is an intersection for classes. Other implementations are possible.
+    $intersection_as_ghs_config = $intersection->toGeneralHtmlSupportConfig();
+    $ghs_config_classes = array_column($intersection_as_ghs_config, 'classes');
+    return !empty($ghs_config_classes);
+  }
+
+  /**
+   * Finds the plugin with elements that conflict with the style element.
+   *
+   * @param \Drupal\ckeditor5\HTMLRestrictions $needle
+   *   A style definition element: a single tag, plus the 'class' attribute,
+   *   plus >=1 allowed 'class' attribute values.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The label of the plugin that is conflicting with this style.
+   *
+   * @throws \OutOfBoundsException
+   *   When a $needle is provided which does not exist among the other plugins.
+   */
+  private function findStyleConflictingPluginLabel(HTMLRestrictions $needle): TranslatableMarkup {
+    foreach ($this->pluginManager->getDefinitions() as $id => $definition) {
+      // We're looking to find the other plugin, not this one.
+      if ($id === 'ckeditor5_style') {
+        continue;
+      }
+
+      assert($definition instanceof CKEditor5PluginDefinition);
+      if (!$definition->hasElements()) {
+        continue;
+      }
+
+      $haystack = HTMLRestrictions::fromString(implode($definition->getElements()));
+      if ($id === 'ckeditor5_sourceEditing') {
+        // The Source Editing plugin's allowed elements are based on stored
+        // config. This differs from all other plugins, which establish allowed
+        // elements as part of their definition. Because of this, the $haystack
+        // is calculated differently for Source Editing.
+        $text_editor = $this->createTextEditorObjectFromContext();
+        $editor_plugins = $text_editor->getSettings()['plugins'];
+        if (!empty($editor_plugins['ckeditor5_sourceEditing'])) {
+          $source_tags = $editor_plugins['ckeditor5_sourceEditing']['allowed_tags'];
+          $haystack = HTMLRestrictions::fromString(implode($source_tags));
+        }
+      }
+      if (self::intersectionWithClasses($needle, $haystack)) {
+        return $definition->label();
+      }
+    }
+
+    throw new \OutOfBoundsException();
+  }
+
+}
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/UniqueLabelInListConstraint.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/UniqueLabelInListConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..5b479f5373a6d161e3250fbc7674be7a6378f09c
--- /dev/null
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/UniqueLabelInListConstraint.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Uniquely labeled list item constraint.
+ *
+ * @Constraint(
+ *   id = "UniqueLabelInList",
+ *   label = @Translation("Unique label in list", context = "Validation"),
+ * )
+ *
+ * @internal
+ */
+class UniqueLabelInListConstraint extends Constraint {
+
+  /**
+   * The default violation message.
+   *
+   * @var string
+   */
+  public $message = 'The label %label is not unique.';
+
+  /**
+   * The key of the label that this validation constraint should check.
+   *
+   * @var null|string
+   */
+  public $labelKey = NULL;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions() {
+    return ['labelKey'];
+  }
+
+}
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/UniqueLabelInListConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/UniqueLabelInListConstraintValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..bcd46b0681424cbdc423b9cf2dfd8695b389c609
--- /dev/null
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/UniqueLabelInListConstraintValidator.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Uniquely labeled list item constraint validator.
+ *
+ * @internal
+ */
+class UniqueLabelInListConstraintValidator extends ConstraintValidator {
+
+  /**
+   * {@inheritdoc}
+   *
+   * @throws \Symfony\Component\Validator\Exception\UnexpectedTypeException
+   *   Thrown when the given constraint is not supported by this validator.
+   */
+  public function validate($list, Constraint $constraint) {
+    if (!$constraint instanceof UniqueLabelInListConstraint) {
+      throw new UnexpectedTypeException($constraint, UniqueLabelInListConstraint::class);
+    }
+
+    $labels = array_column($list, $constraint->labelKey);
+    $label_frequencies = array_count_values($labels);
+
+    foreach ($label_frequencies as $label => $frequency) {
+      if ($frequency > 1) {
+        $this->context->buildViolation($constraint->message)
+          ->setParameter('%label', $label)
+          ->addViolation();
+      }
+    }
+  }
+
+}
diff --git a/core/modules/ckeditor5/src/SmartDefaultSettings.php b/core/modules/ckeditor5/src/SmartDefaultSettings.php
index fe55ab1ad7cacc704e5cd37a5ce1f82cb4126494..b5f29697fde295072048ebdf21d24e7ccb870292 100644
--- a/core/modules/ckeditor5/src/SmartDefaultSettings.php
+++ b/core/modules/ckeditor5/src/SmartDefaultSettings.php
@@ -153,6 +153,15 @@ public function computeSmartDefaultSettings(?EditorInterface $text_editor, Filte
       [$upgraded_settings, $messages] = $this->createSettingsFromCKEditor4($old_editor->getSettings(), HTMLRestrictions::fromTextFormat($old_editor->getFilterFormat()));
       $editor->setSettings($upgraded_settings);
       $editor->setImageUploadSettings($old_editor->getImageUploadSettings());
+      // *Before* determining which elements are still needed for this text
+      // format, ensure that all already enabled plugins that are configurable
+      // have valid settings.
+      // For all already enabled plugins, find the ones that are configurable,
+      // and add their default settings. For enabled plugins with element
+      // subsets, compute the appropriate settings to achieve the subset that
+      // matches the original text format restrictions.
+      $this->addDefaultSettingsForEnabledConfigurablePlugins($editor);
+      $this->computeSubsetSettingForEnabledPluginsWithSubsets($editor, $text_format);
     }
 
     // Add toolbar items based on HTML tags and attributes.
@@ -224,6 +233,9 @@ public function computeSmartDefaultSettings(?EditorInterface $text_editor, Filte
     // and add their default settings. For enabled plugins with element subsets,
     // compute the appropriate settings to achieve the subset that matches the
     // original text format restrictions.
+    // Note: if switching from CKEditor 4, this will already have happened for
+    // plugins that were already enabled in CKEditor 4. It's harmless to compute
+    // this again.
     $this->addDefaultSettingsForEnabledConfigurablePlugins($editor);
     $this->computeSubsetSettingForEnabledPluginsWithSubsets($editor, $text_format);
 
@@ -780,12 +792,13 @@ private static function selectCandidate(array $candidates, HTMLRestrictions $sti
    *   The text editor config entity to update.
    *
    * @return array|null
-   *   NULL when nothing happened, otherwise an array with three values:
+   *   NULL when nothing happened, otherwise an array with four values:
    *   1. a description (for use in a message) of which CKEditor 5 plugins were
    *      enabled to match the HTML tags allowed by the text format.
    *   2. a description (for use in a message) of which CKEditor 5 plugins were
    *      enabled to match the HTML attributes allowed by the text format.
-   *   3. the unsupported elements, in an HTMLRestrictions value object
+   *   3. the unsupported elements, in an HTMLRestrictions value object.
+   *   4. the list of enabled plugin labels.
    */
   private function addToolbarItemsToMatchHtmlElementsInFormat(FilterFormatInterface $format, EditorInterface $editor): ?array {
     $html_restrictions_needed_elements = $format->getHtmlRestrictions();
@@ -797,11 +810,9 @@ private function addToolbarItemsToMatchHtmlElementsInFormat(FilterFormatInterfac
     $enabled_definitions = $this->pluginManager->getEnabledDefinitions($editor);
     $disabled_definitions = array_diff_key($all_definitions, $enabled_definitions);
     $enabled_plugins = array_keys($enabled_definitions);
-    $provided_elements = $this->pluginManager->getProvidedElements($enabled_plugins);
+    $provided_elements = $this->pluginManager->getProvidedElements($enabled_plugins, $editor);
     $provided = new HTMLRestrictions($provided_elements);
     $needed = HTMLRestrictions::fromTextFormat($format);
-    $still_needed = $needed->diff($provided);
-
     // Plugins only supporting <tag attr> cannot create the tag. For that, they
     // must support plain <tag> too. With this being the case, break down what
     // is needed based on what is currently provided.
@@ -812,11 +823,12 @@ private function addToolbarItemsToMatchHtmlElementsInFormat(FilterFormatInterfac
     $provided_plain_tags = new HTMLRestrictions(
       $this->pluginManager->getProvidedElements($enabled_plugins, NULL, FALSE, TRUE)
     );
+
+    // Determine the still needed plain tags, the still needed attributes, and
+    // the union of both.
     $still_needed_plain_tags = $needed->extractPlainTagsSubset()->diff($provided_plain_tags);
-    $still_needed_attributes = $still_needed->diff($still_needed_plain_tags);
-    // Merging $still_needed_plain_tags with $still_needed_attributes must
-    // always equal $still_needed.
-    assert($still_needed_plain_tags->merge($still_needed_attributes)->diff($still_needed)->allowsNothing());
+    $still_needed_attributes = $needed->diff($provided)->diff($still_needed_plain_tags);
+    $still_needed = $still_needed_plain_tags->merge($still_needed_attributes);
 
     if (!$still_needed->allowsNothing()) {
       // Select plugins for supporting the still needed plain tags.
@@ -889,6 +901,7 @@ private function addToolbarItemsToMatchHtmlElementsInFormat(FilterFormatInterfac
           NULL,
           NULL,
           $still_needed,
+          NULL,
         ];
       }
     }
diff --git a/core/modules/ckeditor5/tests/modules/ckeditor5_plugin_elements_subset/ckeditor5_plugin_elements_subset.ckeditor5.yml b/core/modules/ckeditor5/tests/modules/ckeditor5_plugin_elements_subset/ckeditor5_plugin_elements_subset.ckeditor5.yml
index c5218e7e2f5e02ce8ff49d943cdc263e650c158b..97952121f3eb41e273c31941c88da20ef10e194c 100644
--- a/core/modules/ckeditor5/tests/modules/ckeditor5_plugin_elements_subset/ckeditor5_plugin_elements_subset.ckeditor5.yml
+++ b/core/modules/ckeditor5/tests/modules/ckeditor5_plugin_elements_subset/ckeditor5_plugin_elements_subset.ckeditor5.yml
@@ -7,3 +7,5 @@ ckeditor5_plugin_elements_subset_sneakySuperset:
     elements:
       - <foo>
       - <bar>
+      - <bar baz>
+      - <$any-html5-element class>
diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php
index 64cb3d675032d6c74e3a801473fa00e25dea145d..07a26cc22112c8c8d00fc0e790afc89b2a92172c 100644
--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php
+++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php
@@ -48,7 +48,7 @@ class CKEditor5AllowedTagsTest extends CKEditor5TestBase {
    *
    * @var string
    */
-  protected $defaultElementsAfterUpdatingToCkeditor5 = '<br> <p> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <cite> <dl> <dt> <dd> <a hreflang href> <blockquote cite> <ul type> <ol type start> <img src alt data-entity-type data-entity-uuid> <strong> <em> <code> <li>';
+  protected $defaultElementsAfterUpdatingToCkeditor5 = '<br> <p> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <cite> <dl> <dt> <dd> <img src alt data-entity-type data-entity-uuid> <a hreflang href> <blockquote cite> <ul type> <ol type start> <strong> <em> <code> <li>';
 
   /**
    * Test enabling CKEditor 5 in a way that triggers validation.
diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5TestBase.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5TestBase.php
index 052ba5e0d1d1da327f72547355a26e4c59e3f008..d8729e7a94729ecde5d2cc817fb3b4a0618a0f19 100644
--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5TestBase.php
+++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5TestBase.php
@@ -137,4 +137,14 @@ protected function assertHtmlEsqueFieldValueEquals($field, $value, TraversableEl
     $assert_session->assert((bool) preg_match($regex, $actual), $message);
   }
 
+  /**
+   * Checks that no real-time validation errors are present.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   */
+  protected function assertNoRealtimeValidationErrors(): void {
+    $assert_session = $this->assertSession();
+    $this->assertSame('', $assert_session->elementExists('css', '[data-drupal-selector="ckeditor5-realtime-validation-messages-container"]')->getHtml());
+  }
+
 }
diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php
index 0c5b03967932b1d06a52e872a22865faadff85dd..3687d2cee7bb37c5cfd4d49c9f717e71c2582cf4 100644
--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php
+++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php
@@ -1624,24 +1624,4 @@ protected function getLastPreviewRequestTransferSize() {
     return $this->getSession()->evaluateScript($javascript);
   }
 
-  /**
-   * Selects text inside an element.
-   *
-   * @param string $selector
-   *   A CSS selector for the element which contents should be selected.
-   */
-  protected function selectTextInsideElement(string $selector): void {
-    $javascript = <<<JS
-(function() {
-  const el = document.querySelector("$selector");
-  const range = document.createRange();
-  range.selectNodeContents(el);
-  const sel = window.getSelection();
-  sel.removeAllRanges();
-  sel.addRange(range);
-})();
-JS;
-    $this->getSession()->evaluateScript($javascript);
-  }
-
 }
diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/StyleTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/StyleTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1cc9674586e4b9beb930825874f718049f859368
--- /dev/null
+++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/StyleTest.php
@@ -0,0 +1,318 @@
+<?php
+
+namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
+
+// cspell:ignore sourceediting
+
+use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
+use Drupal\editor\Entity\Editor;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
+use Symfony\Component\Validator\ConstraintViolation;
+
+/**
+ * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style
+ * @group ckeditor5
+ * @internal
+ */
+class StyleTest extends CKEditor5TestBase {
+
+  use CKEditor5TestTrait;
+
+  /**
+   * @covers \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style::buildConfigurationForm
+   */
+  public function testStyleSettingsForm() {
+    $this->drupalLogin($this->drupalCreateUser(['administer filters']));
+
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
+
+    $this->createNewTextFormat($page, $assert_session);
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // The Style plugin settings form should not be present.
+    $assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style"]');
+
+    $this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-style'));
+    $this->triggerKeyUp('.ckeditor5-toolbar-item-style', 'ArrowDown');
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // No validation error upon enabling the Style plugin.
+    $this->assertNoRealtimeValidationErrors();
+    $assert_session->pageTextContains('No styles configured');
+
+    // Still no validation error when configuring other functionality first.
+    $this->triggerKeyUp('.ckeditor5-toolbar-item-undo', 'ArrowDown');
+    $assert_session->assertWaitOnAjaxRequest();
+    $this->assertNoRealtimeValidationErrors();
+
+    // The Style plugin settings form should now be present and should have no
+    // styles configured.
+    $page->clickLink('Style');
+    $this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]'));
+
+    $javascript = <<<JS
+      const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
+      allowedTags.value = 'p.foo.bar  | Foobar paragraph';
+      allowedTags.dispatchEvent(new Event('input'));
+JS;
+    $this->getSession()->executeScript($javascript);
+
+    // Immediately save the configuration. Intentionally do nothing that would
+    // trigger an AJAX rebuild.
+    $page->pressButton('Save configuration');
+    $assert_session->pageTextContains('Added text format');
+
+    // Verify that the configuration was saved.
+    $this->drupalGet('admin/config/content/formats/manage/ckeditor5');
+    $page->clickLink('Style');
+    $this->assertNotNull($styles_textarea = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]'));
+
+    $this->assertSame("p.foo.bar|Foobar paragraph\n", $styles_textarea->getValue());
+    $assert_session->pageTextContains('One style configured');
+    $allowed_html_field = $assert_session->fieldExists('filters[filter_html][settings][allowed_html]');
+    $this->assertStringContainsString('<p class="foo bar">', $allowed_html_field->getValue());
+
+    // Attempt to use an unsupported HTML5 tag.
+    $javascript = <<<JS
+      const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
+      allowedTags.value = 's.redacted|Redacted';
+      allowedTags.dispatchEvent(new Event('change'));
+JS;
+    $this->getSession()->executeScript($javascript);
+
+    // The CKEditor 5 module should refuse to specify styles on tags that cannot
+    // (yet) be created.
+    // @see \Drupal\ckeditor5\Plugin\Validation\Constraint\FundamentalCompatibilityConstraintValidator::checkAllHtmlTagsAreCreatable()
+    $assert_session->waitForElement('css', '[role=alert][data-drupal-message-type="error"]:contains("The Style plugin needs another plugin to create <s>, for it to be able to create the following attributes: <s class="redacted">. Enable a plugin that supports creating this tag. If none exists, you can configure the Source Editing plugin to support it.")');
+    // The entire vertical tab for "Style" settings should be marked up as the
+    // cause of the error, which means the "Styles" text area in there is marked
+    // too.
+    $assert_session->elementExists('css', '.vertical-tabs__pane[data-ckeditor5-plugin-id="ckeditor5_style"][aria-invalid="true"]');
+    $assert_session->elementExists('css', '.vertical-tabs__pane[data-ckeditor5-plugin-id="ckeditor5_style"] textarea[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"][aria-invalid="true"]');
+
+    // Attempt to save anyway: the warning should become an error.
+    $page->pressButton('Save configuration');
+    $assert_session->pageTextNotContains('Added text format');
+    $assert_session->elementExists('css', '[aria-label="Error message"]:contains("The Style plugin needs another plugin to create <s>, for it to be able to create the following attributes: <s class="redacted">. Enable a plugin that supports creating this tag. If none exists, you can configure the Source Editing plugin to support it.")');
+
+    // Now, attempt to use a supported non-HTML5 tag.
+    // @see \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator
+    $javascript = <<<JS
+      const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
+      allowedTags.value = 'drupal-media.sensational|Sensational media';
+      allowedTags.dispatchEvent(new Event('change'));
+JS;
+    $this->getSession()->executeScript($javascript);
+
+    // The CKEditor 5 module should refuse to allow styles on non-HTML5 tags.
+    $assert_session->waitForElement('css', '[role=alert][data-drupal-message-type="error"]:contains("A style can only be specified for an HTML 5 tag. <drupal-media> is not an HTML5 tag.")');
+    // The vertical tab for "Style" settings should not be marked up as the cause
+    // of the error, but only the "Styles" text area in the vertical tab.
+    $assert_session->elementNotExists('css', '.vertical-tabs__pane[data-ckeditor5-plugin-id="ckeditor5_style"][aria-invalid="true"]');
+    $assert_session->elementExists('css', '.vertical-tabs__pane[data-ckeditor5-plugin-id="ckeditor5_style"] textarea[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"][aria-invalid="true"]');
+
+    // Test configuration overlaps across plugins.
+    $this->drupalGet('admin/config/content/formats/manage/ckeditor5');
+    $this->assertNotEmpty($assert_session->elementExists('css', '.ckeditor5-toolbar-item-sourceEditing'));
+    $this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowDown');
+    $assert_session->assertWaitOnAjaxRequest();
+    // The Source Editing plugin settings form should now be present and should
+    // have no allowed tags configured.
+    $page->clickLink('Source editing');
+    $this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]'));
+
+    // Make `<aside class>` creatable.
+    $javascript = <<<JS
+      const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]');
+      allowedTags.value = '<aside class>';
+      allowedTags.dispatchEvent(new Event('change'));
+JS;
+    $this->getSession()->executeScript($javascript);
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Create a style with `aside` and a class name.
+    $javascript = <<<JS
+      const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
+      allowedTags.value = 'aside.error|Aside';
+      allowedTags.dispatchEvent(new Event('change'));
+JS;
+    $this->getSession()->executeScript($javascript);
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // The CKEditor 5 module should refuse to create configuration overlaps
+    // across plugins.
+    // @see \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator::findStyleConflictingPluginLabel()
+    $assert_session->waitForElement('css', '[role=alert][data-drupal-message-type="error"]:contains("A style must only specify classes not supported by other plugins.")');
+  }
+
+  /**
+   * Tests Style functionality: setting a class, expected style choices.
+   */
+  public function testStyleFunctionality() {
+    FilterFormat::create([
+      'format' => 'test_format',
+      'name' => 'Test format',
+      'filters' => [
+        'filter_html' => [
+          'status' => TRUE,
+          'settings' => [
+            'allowed_html' => '<p class="highlighted interesting"> <br> <a href class="reliable"> <blockquote class="famous"> <h2 class="red-heading">',
+          ],
+        ],
+      ],
+    ])->save();
+    Editor::create([
+      'editor' => 'ckeditor5',
+      'format' => 'test_format',
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'heading',
+            'link',
+            'blockQuote',
+            'style',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_heading' => [
+            'enabled_headings' => [
+              'heading2',
+            ],
+          ],
+          'ckeditor5_style' => [
+            'styles' => [
+              [
+                'label' => 'Highlighted & interesting',
+                'element' => '<p class="highlighted interesting">',
+              ],
+              [
+                'label' => 'Red heading',
+                'element' => '<h2 class="red-heading">',
+              ],
+              [
+                'label' => 'Reliable source',
+                'element' => '<a class="reliable">',
+              ],
+              [
+                'label' => 'Famous',
+                'element' => '<blockquote class="famous">',
+              ],
+            ],
+          ],
+        ],
+      ],
+      'image_upload' => [
+        'status' => FALSE,
+      ],
+    ])->save();
+    $this->assertSame([], array_map(
+      function (ConstraintViolation $v) {
+        return (string) $v->getMessage();
+      },
+      iterator_to_array(CKEditor5::validatePair(
+        Editor::load('test_format'),
+        FilterFormat::load('test_format')
+      ))
+    ));
+
+    // Create a sample entity to test CKEditor 5.
+    $node = $this->createNode([
+      'type' => 'page',
+      'title' => 'A selection of the history of Drupal',
+      'body' => [
+        'value' => '<h2>Upgrades</h2><p class="history">Drupal has historically been difficult to upgrade from one major version to the next.</p><p class="highlighted interesting">This changed with Drupal 8.</p><blockquote class="famous"><p>Updating from Drupal 8\'s latest version to Drupal 9.0.0 should be as easy as updating between minor versions of Drupal 8.</p></blockquote><p> — <a class="reliable" href="https://dri.es/making-drupal-upgrades-easy-forever">Dries</a></p>',
+        'format' => 'test_format',
+      ],
+    ]);
+    $node->save();
+
+    // Observe.
+    $this->drupalLogin($this->drupalCreateUser([
+      'use text format test_format',
+      'bypass node access',
+    ]));
+    $this->drupalGet($node->toUrl('edit-form'));
+    $this->waitForEditor();
+
+    // Select the <h2>, assert that no style is active currently..
+    $this->selectTextInsideElement('h2');
+    $assert_session = $this->assertSession();
+    $style_dropdown = $assert_session->elementExists('css', '.ck-style-dropdown');
+    $this->assertSame('Styles', $style_dropdown->getText());
+
+    // Click the dropdown, check the available styles.
+    $style_dropdown->click();
+    $buttons = $style_dropdown->findAll('css', '.ck-dropdown__panel button');
+    $this->assertCount(4, $buttons);
+    $this->assertSame('Highlighted & interesting', $buttons[0]->find('css', '.ck-button__label')->getText());
+    $this->assertSame('Red heading', $buttons[1]->find('css', '.ck-button__label')->getText());
+    $this->assertSame('Famous', $buttons[2]->find('css', '.ck-button__label')->getText());
+    $this->assertSame('Reliable source', $buttons[3]->find('css', '.ck-button__label')->getText());
+    $this->assertSame('true', $buttons[0]->getAttribute('aria-disabled'));
+    $this->assertFalse($buttons[1]->hasAttribute('aria-disabled'));
+    $this->assertSame('true', $buttons[2]->getAttribute('aria-disabled'));
+    // @todo Uncomment this after https://github.com/ckeditor/ckeditor5/issues/11709 is fixed.
+    // $this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
+    $this->assertTrue($buttons[0]->hasClass('ck-off'));
+    $this->assertTrue($buttons[1]->hasClass('ck-off'));
+    $this->assertTrue($buttons[2]->hasClass('ck-off'));
+    $this->assertTrue($buttons[3]->hasClass('ck-off'));
+
+    // Apply the "Red heading" style and verify it has the expected effect.
+    $assert_session->elementExists('css', '.ck-editor__main h2:not(.red-heading)');
+    $buttons[1]->click();
+    $assert_session->elementExists('css', '.ck-editor__main h2.red-heading');
+    $this->assertTrue($buttons[0]->hasClass('ck-off'));
+    $this->assertTrue($buttons[1]->hasClass('ck-on'));
+    $this->assertTrue($buttons[2]->hasClass('ck-off'));
+    $this->assertTrue($buttons[3]->hasClass('ck-off'));
+    $this->assertSame('Red heading', $style_dropdown->getText());
+
+    // Select the first paragraph and observe changes in:
+    // - styles dropdown label
+    // - button states
+    $this->selectTextInsideElement('p');
+    $this->assertSame('Styles', $style_dropdown->getText());
+    $style_dropdown->click();
+    $this->assertTrue($buttons[0]->hasClass('ck-off'));
+    $this->assertTrue($buttons[1]->hasClass('ck-off'));
+    $this->assertTrue($buttons[2]->hasClass('ck-off'));
+    $this->assertTrue($buttons[3]->hasClass('ck-off'));
+    $this->assertFalse($buttons[0]->hasAttribute('aria-disabled'));
+    $this->assertSame('true', $buttons[1]->getAttribute('aria-disabled'));
+    $this->assertSame('true', $buttons[2]->getAttribute('aria-disabled'));
+    // @todo Uncomment this after https://github.com/ckeditor/ckeditor5/issues/11709 is fixed.
+    // $this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
+    // Close the dropdown.
+    $style_dropdown->click();
+
+    // Select the blockquote and observe changes in:
+    // - styles dropdown label
+    // - button states
+    $this->selectTextInsideElement('blockquote');
+    $this->assertSame('Famous', $style_dropdown->getText());
+    $style_dropdown->click();
+    $this->assertTrue($buttons[0]->hasClass('ck-off'));
+    $this->assertTrue($buttons[1]->hasClass('ck-off'));
+    $this->assertTrue($buttons[2]->hasClass('ck-on'));
+    $this->assertTrue($buttons[3]->hasClass('ck-off'));
+    $this->assertFalse($buttons[0]->hasAttribute('aria-disabled'));
+    $this->assertSame('true', $buttons[1]->getAttribute('aria-disabled'));
+    $this->assertFalse($buttons[2]->hasAttribute('aria-disabled'));
+    // @todo Uncomment this after https://github.com/ckeditor/ckeditor5/issues/11709 is fixed.
+    // $this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
+    // Close the dropdown.
+    $style_dropdown->click();
+
+    // The resulting markup should be identical to the starting markup, with two
+    // changes:
+    // 1. the `red-heading` class has been added to the `<h2>`
+    // 2. the `history` class has been removed from the `<p>`, because CKEditor
+    //    5 has not been configured for this: if a Style had configured for it,
+    //    it would have been retained.
+    $this->assertSame('<h2 class="red-heading">Upgrades</h2><p>Drupal has historically been difficult to upgrade from one major version to the next.</p><p class="highlighted interesting">This changed with Drupal 8.</p><blockquote class="famous"><p>Updating from Drupal 8\'s latest version to Drupal 9.0.0 should be as easy as updating between minor versions of Drupal 8.</p></blockquote><p>— <a class="reliable" href="https://dri.es/making-drupal-upgrades-easy-forever">Dries</a></p>', $this->getEditorDataAsHtmlString());
+  }
+
+}
diff --git a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
index 7be8fb3ecd466cdeefbd789a5a786dbf6125a98c..ace2815ee370b5e80fb113a5bb698b7606fea9a7 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
@@ -975,26 +975,30 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s
   }
 
   /**
-   * Tests detection of invalid CKEditor5PluginElementsSubsetInterface class.
+   * Tests detection of invalid CKEditor5PluginElementsSubsetInterface classes.
+   *
+   * @dataProvider providerProvidedElementsInvalidElementSubset
    */
-  public function testProvidedElementsInvalidElementSubset(): void {
+  public function testProvidedElementsInvalidElementSubset(array $configured_subset, string $expected_exception_message): void {
     $this->enableModules(['ckeditor5_plugin_elements_subset']);
 
-    // Configure the sneaky superset plugin to have a random tag as the subset.
+    // Configure the sneaky superset plugin.
     $sneaky_plugin_id = 'ckeditor5_plugin_elements_subset_sneakySuperset';
-    $random_tag_name = strtolower($this->randomMachineName());
-    $random_tag = "<$random_tag_name>";
     $text_editor = Editor::create([
       'format' => 'dummy',
       'editor' => 'ckeditor5',
       'settings' => [
         'plugins' => [
-          $sneaky_plugin_id => ['configured_subset' => [$random_tag]],
+          $sneaky_plugin_id => ['configured_subset' => $configured_subset],
         ],
       ],
       'image_upload' => [],
     ]);
 
+    // Invalid subsets are allowed on unsaved Text Editor config entities,
+    // because they may have invalid configuration.
+    $text_editor->enforceIsNew(FALSE);
+
     // No exception when getting all provided elements.
     $this->assertGreaterThan(0, count($this->manager->getProvidedElements()));
 
@@ -1005,10 +1009,35 @@ public function testProvidedElementsInvalidElementSubset(): void {
     // editor config entity is passed: only then can a subset be generated based
     // on configuration.
     $this->expectException(\LogicException::class);
-    $this->expectExceptionMessage("The \"ckeditor5_plugin_elements_subset_sneakySuperset\" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: \"$random_tag\".");
+    $this->expectExceptionMessage($expected_exception_message);
     $this->manager->getProvidedElements([$sneaky_plugin_id], $text_editor);
   }
 
+  /**
+   * Data provider.
+   *
+   * @return array
+   *   Test scenarios.
+   */
+  public function providerProvidedElementsInvalidElementSubset(): array {
+    $random_tag_name = strtolower($this->randomMachineName());
+    $random_tag = "<$random_tag_name>";
+    return [
+      'superset: random tag not listed in the plugin definition' => [
+        [$random_tag],
+        "The \"ckeditor5_plugin_elements_subset_sneakySuperset\" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: \"$random_tag\".",
+      ],
+      'subset that omits the essential creatable tag' => [
+        ['<bar baz>'],
+        'The "ckeditor5_plugin_elements_subset_sneakySuperset" CKEditor 5 plugin implements ::getElementsSubset() and did return a subset ("<bar baz>") but the following tags can no longer be created: "<bar>".',
+      ],
+      'subset that tries to leverage the `<$any-html5-element>` wildcard tag but picks a concrete tag that the wildcard tag does not resolve into' => [
+        ['<drupal-media class="sensational">'],
+        'The "ckeditor5_plugin_elements_subset_sneakySuperset" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: "<drupal-media class="sensational">".',
+      ],
+    ];
+  }
+
   /**
    * Tests the enabling of plugins.
    */
diff --git a/core/modules/ckeditor5/tests/src/Kernel/ConfigurablePluginTest.php b/core/modules/ckeditor5/tests/src/Kernel/ConfigurablePluginTest.php
index 73af3391874930861d9bdf56adfe6db74ac12ec7..39a78d9a0a5502bb919b8ddb493038b0e152d296 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/ConfigurablePluginTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/ConfigurablePluginTest.php
@@ -64,6 +64,9 @@ public function testDefaults() {
           'heading6',
         ],
       ],
+      'ckeditor5_style' => [
+        'styles' => [],
+      ],
       'ckeditor5_sourceEditing' => [
         'allowed_tags' => [],
       ],
diff --git a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
index c0a9cece8d2b09aaf4f064e0c19210a456979780..49ec1af0e8d423ceee326aa6f66f6b67deff38cf 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
@@ -283,6 +283,9 @@ protected function setUp(): void {
                 'items' => [
                   'Language',
                   'Styles',
+                  // Blockquote does not have settings. It's present only to
+                  // support an additional tag, to test realistic styles.
+                  'Blockquote',
                 ],
               ],
               [
@@ -299,7 +302,7 @@ protected function setUp(): void {
             'language_list' => 'all',
           ],
           'stylescombo' => [
-            'styles' => "p.callout|Callout\r\nblockquote.interesting|Interesting quote",
+            'styles' => "p.callout|Callout\r\nblockquote.interesting.highlighted|Interesting & highlighted quote\n\nblockquote.famous |    Famous\n",
           ],
           // Plugin setting without upgrade path.
           'llama_contextual_and_button' => [
@@ -309,6 +312,42 @@ protected function setUp(): void {
       ],
     ])->save();
 
+    FilterFormat::create([
+      'format' => 'cke4_stylescombo_span',
+      'name' => 'A CKEditor 4 configured to have span styles',
+      'filters' => [
+        'filter_html' => [
+          'status' => 1,
+          'settings' => [
+            'allowed_html' => '<p> <br> <span class="llama">',
+          ] + $filter_plugin_manager->getDefinition('filter_html')['settings'],
+        ],
+      ],
+    ])->save();
+    Editor::create([
+      'format' => 'cke4_stylescombo_span',
+      'editor' => 'ckeditor',
+      'settings' => [
+        'toolbar' => [
+          'rows' => [
+            0 => [
+              [
+                'name' => 'Whatever',
+                'items' => [
+                  'Styles',
+                ],
+              ],
+            ],
+          ],
+        ],
+        'plugins' => [
+          'stylescombo' => [
+            'styles' => "span.llama|Llama span",
+          ],
+        ],
+      ],
+    ])->save();
+
     FilterFormat::create([
       'format' => 'cke4_contrib_plugins_now_in_core',
       'name' => 'All CKEditor 4 contrib plugins now in core',
@@ -527,6 +566,22 @@ public function provider() {
           ],
         ],
         'plugins' => [
+          'ckeditor5_heading' => [
+            'enabled_headings' => [
+              'heading2',
+              'heading3',
+              'heading4',
+              'heading5',
+              'heading6',
+            ],
+          ],
+          'ckeditor5_imageResize' => [
+            'allow_resize' => TRUE,
+          ],
+          'ckeditor5_list' => [
+            'reversed' => FALSE,
+            'startIndex' => TRUE,
+          ],
           'ckeditor5_sourceEditing' => [
             'allowed_tags' => [
               '<cite>',
@@ -545,22 +600,6 @@ public function provider() {
               '<h6 id>',
             ],
           ],
-          'ckeditor5_heading' => [
-            'enabled_headings' => [
-              'heading2',
-              'heading3',
-              'heading4',
-              'heading5',
-              'heading6',
-            ],
-          ],
-          'ckeditor5_imageResize' => [
-            'allow_resize' => TRUE,
-          ],
-          'ckeditor5_list' => [
-            'reversed' => FALSE,
-            'startIndex' => TRUE,
-          ],
         ],
       ],
       'expected_superset' => '',
@@ -650,12 +689,6 @@ public function provider() {
       'expected_ckeditor5_settings' => [
         'toolbar' => $basic_html_test_case['expected_ckeditor5_settings']['toolbar'],
         'plugins' => [
-          'ckeditor5_sourceEditing' => [
-            'allowed_tags' => array_values(array_diff(
-              $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
-              ['<h4 id>', '<h6 id>'],
-            )),
-          ],
           'ckeditor5_heading' => [
             'enabled_headings' => [
               'heading2',
@@ -665,6 +698,12 @@ public function provider() {
           ],
           'ckeditor5_imageResize' => ['allow_resize' => TRUE],
           'ckeditor5_list' => ['reversed' => FALSE, 'startIndex' => TRUE],
+          'ckeditor5_sourceEditing' => [
+            'allowed_tags' => array_values(array_diff(
+              $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
+              ['<h4 id>', '<h6 id>'],
+            )),
+          ],
         ],
       ],
       'expected_superset' => $basic_html_test_case['expected_superset'],
@@ -689,9 +728,6 @@ public function provider() {
       'expected_ckeditor5_settings' => [
         'toolbar' => $basic_html_test_case['expected_ckeditor5_settings']['toolbar'],
         'plugins' => [
-          'ckeditor5_sourceEditing' => [
-            'allowed_tags' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
-          ],
           'ckeditor5_heading' => [
             'enabled_headings' => [
               'heading1',
@@ -704,6 +740,9 @@ public function provider() {
           ],
           'ckeditor5_imageResize' => ['allow_resize' => TRUE],
           'ckeditor5_list' => ['reversed' => FALSE, 'startIndex' => TRUE],
+          'ckeditor5_sourceEditing' => [
+            'allowed_tags' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
+          ],
         ],
       ],
       'expected_superset' => $basic_html_test_case['expected_superset'],
@@ -733,14 +772,14 @@ public function provider() {
           ),
         ],
         'plugins' => [
+          'ckeditor5_imageResize' => ['allow_resize' => TRUE],
+          'ckeditor5_list' => ['reversed' => FALSE, 'startIndex' => TRUE],
           'ckeditor5_sourceEditing' => [
             'allowed_tags' => array_values(array_diff(
               $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
               ['<h2 id>', '<h3 id>', '<h4 id>', '<h5 id>', '<h6 id>'],
             )),
           ],
-          'ckeditor5_imageResize' => ['allow_resize' => TRUE],
-          'ckeditor5_list' => ['reversed' => FALSE, 'startIndex' => TRUE],
         ],
       ],
       'expected_superset' => $basic_html_test_case['expected_superset'],
@@ -801,13 +840,12 @@ public function provider() {
           ),
         ],
         'plugins' => array_merge(
-          array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins'], 0, 1),
+          $basic_html_test_case['expected_ckeditor5_settings']['plugins'],
           [
             'ckeditor5_alignment' => [
               'enabled_alignments' => ['center', 'justify'],
             ],
           ],
-          array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins'], 1),
         ),
       ],
       'expected_superset' => implode(' ', [
@@ -941,6 +979,9 @@ public function provider() {
       'expected_ckeditor5_settings' => [
         'toolbar' => $basic_html_test_case['expected_ckeditor5_settings']['toolbar'],
         'plugins' => [
+          'ckeditor5_heading' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_heading'],
+          'ckeditor5_imageResize' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_imageResize'],
+          'ckeditor5_list' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_list'],
           'ckeditor5_sourceEditing' => [
             'allowed_tags' => array_merge(
               $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
@@ -1190,12 +1231,30 @@ public function provider() {
         'toolbar' => [
           'items' => [
             'textPartLanguage',
+            'style',
+            'blockQuote',
           ],
         ],
         'plugins' => [
           'ckeditor5_language' => [
             'language_list' => 'all',
           ],
+          'ckeditor5_style' => [
+            'styles' => [
+              [
+                'label' => 'Callout',
+                'element' => '<p class="callout">',
+              ],
+              [
+                'label' => 'Interesting & highlighted quote',
+                'element' => '<blockquote class="interesting highlighted">',
+              ],
+              [
+                'label' => 'Famous',
+                'element' => '<blockquote class="famous">',
+              ],
+            ],
+          ],
         ],
       ],
       'expected_superset' => '',
@@ -1214,6 +1273,46 @@ public function provider() {
       ],
     ];
 
+    yield "cke4_stylescombo_span can be switched to CKEditor 5 without problems, only <span> in Source Editing" => [
+      'format_id' => 'cke4_stylescombo_span',
+      'filters_to_drop' => [],
+      'expected_ckeditor5_settings' => [
+        'toolbar' => [
+          'items' => [
+            'style',
+            'sourceEditing',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_style' => [
+            'styles' => [
+              [
+                'label' => 'Llama span',
+                'element' => '<span class="llama">',
+              ],
+            ],
+          ],
+          'ckeditor5_sourceEditing' => [
+            'allowed_tags' => [
+              '<span>',
+            ],
+          ],
+        ],
+      ],
+      'expected_superset' => '',
+      'expected_fundamental_compatibility_violations' => [],
+      'expected_db_logs' => [
+        'status' => [
+          "The following tags were permitted by the <em class=\"placeholder\">A CKEditor 4 configured to have span styles</em> text format's filter configuration, but no plugin was available that supports them. To ensure the tags remain supported by this text format, the following were added to the Source Editing plugin's <em>Manually editable HTML tags</em>: &lt;span&gt;. The text format must be saved to make these changes active.",
+        ],
+      ],
+      'expected_messages' => [
+        'status' => [
+          'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following:  Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: &lt;span&gt;. Additional details are available in your logs.',
+        ],
+      ],
+    ];
+
     yield "cke4_contrib_plugins_now_in_core can be switched to CKEditor 5 without problems" => [
       'format_id' => 'cke4_contrib_plugins_now_in_core',
       'filters_to_drop' => [],
diff --git a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
index 5f93e70b0f45bae48bf6168ba4ac232236865cf1..9a7505a5f0ad280717bd5f39f7b9cbbf4fd04fe6 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
@@ -63,6 +63,9 @@ protected function setUp(): void {
    * @covers \Drupal\ckeditor5\Plugin\Validation\Constraint\ToolbarItemConstraintValidator
    * @covers \Drupal\ckeditor5\Plugin\Validation\Constraint\ToolbarItemDependencyConstraintValidator
    * @covers \Drupal\ckeditor5\Plugin\Validation\Constraint\EnabledConfigurablePluginsConstraintValidator
+   * @covers \Drupal\ckeditor5\Plugin\Validation\Constraint\CKEditor5ElementConstraintValidator
+   * @covers \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator
+   * @covers \Drupal\ckeditor5\Plugin\Validation\Constraint\UniqueLabelInListConstraintValidator
    * @dataProvider provider
    *
    * @param array $ckeditor5_settings
@@ -328,6 +331,232 @@ public function provider(): array {
       ],
       'violations' => [],
     ];
+    $data['INVALID: Style plugin with no styles'] = [
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'style',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_style' => [
+            'styles' => [],
+          ],
+        ],
+      ],
+      'violations' => [
+        'settings.plugins.ckeditor5_style.styles' => 'Enable at least one style, otherwise disable the Style plugin.',
+      ],
+    ];
+    $data['INVALID: Style plugin configured to add class to GHS-supported non-HTML5 tag'] = [
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'style',
+            'sourceEditing',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_sourceEditing' => [
+            'allowed_tags' => [
+              '<foo>',
+            ],
+          ],
+          'ckeditor5_style' => [
+            'styles' => [
+              [
+                'label' => 'Barry foo',
+                'element' => '<foo class="bar">',
+              ],
+            ],
+          ],
+        ],
+      ],
+      'violations' => [
+        'settings.plugins.ckeditor5_style.styles.0.element' => 'A style can only be specified for an HTML 5 tag. <code>&lt;foo&gt;</code> is not an HTML5 tag.',
+      ],
+    ];
+    $data['INVALID: Style plugin configured to add class to plugin-supported non-HTML5 tag'] = [
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'style',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_style' => [
+            'styles' => [
+              [
+                'label' => 'Sensational media',
+                'element' => '<drupal-media class="sensational">',
+              ],
+            ],
+          ],
+          'media_media' => [
+            'allow_view_mode_override' => FALSE,
+          ],
+        ],
+      ],
+      'violations' => [
+        'settings.plugins.ckeditor5_style.styles.0.element' => 'A style can only be specified for an HTML 5 tag. <code>&lt;drupal-media&gt;</code> is not an HTML5 tag.',
+      ],
+    ];
+    $data['INVALID: Style plugin configured to add class that is supported by a disabled plugin'] = [
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'style',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_style' => [
+            'styles' => [
+              [
+                'label' => 'Justified paragraph',
+                'element' => '<p class="text-align-justify">',
+              ],
+            ],
+          ],
+        ],
+      ],
+      'violations' => [
+        'settings.plugins.ckeditor5_style.styles.0.element' => 'A style must only specify classes not supported by other plugins. The <code>text-align-justify</code> classes on <code>&lt;p&gt;</code> are supported by the <em class="placeholder">Alignment</em> plugin. Remove this style and enable that plugin instead.',
+      ],
+    ];
+    $data['INVALID: Style plugin configured to add class that is supported by an enabled plugin if its configuration were different'] = [
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'style',
+            'alignment',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_alignment' => [
+            'enabled_alignments' => ['center'],
+          ],
+          'ckeditor5_style' => [
+            'styles' => [
+              [
+                'label' => 'Justified paragraph',
+                'element' => '<p class="text-align-justify">',
+              ],
+            ],
+          ],
+        ],
+      ],
+      'violations' => [],
+    ];
+    $data['INVALID: Style plugin configured to add class that is supported by an enabled plugin'] = [
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'style',
+            'alignment',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_alignment' => [
+            'enabled_alignments' => ['justify'],
+          ],
+          'ckeditor5_style' => [
+            'styles' => [
+              [
+                'label' => 'Justified paragraph',
+                'element' => '<p class="text-align-justify">',
+              ],
+            ],
+          ],
+        ],
+      ],
+      'violations' => [
+        'settings.plugins.ckeditor5_style.styles.0.element' => 'A style must only specify classes not supported by other plugins. The <code>text-align-justify</code> classes on <code>&lt;p&gt;</code> are already supported by the enabled <em class="placeholder">Alignment</em> plugin.',
+      ],
+    ];
+    $data['INVALID: Style plugin has multiple styles with same label'] = [
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'blockQuote',
+            'style',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_style' => [
+            'styles' => [
+              0 => [
+                'label' => 'Highlighted',
+                'element' => '<p class="highlighted">',
+              ],
+              1 => [
+                'label' => 'Highlighted',
+                'element' => '<blockquote class="highlighted">',
+              ],
+            ],
+          ],
+        ],
+      ],
+      'violations' => [
+        'settings.plugins.ckeditor5_style.styles' => 'The label <em class="placeholder">Highlighted</em> is not unique.',
+      ],
+    ];
+    $data['INVALID: Style plugin has styles with invalid elements'] = [
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'blockQuote',
+            'style',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_style' => [
+            'styles' => [
+              0 => [
+                'label' => 'missing class attribute',
+                'element' => '<p>',
+              ],
+              1 => [
+                'label' => 'class attribute present but no allowed values listed',
+                'element' => '<blockquote class="">',
+              ],
+            ],
+          ],
+        ],
+      ],
+      'violations' => [
+        'settings.plugins.ckeditor5_style.styles.0.element' => 'The following tag is missing the required attribute <code>class</code>: <code>&lt;p&gt;</code>.',
+        'settings.plugins.ckeditor5_style.styles.1.element' => 'The following tag does not have the minimum of 1 allowed values for the required attribute <code>class</code>: <code>&lt;blockquote class=&quot;&quot;&gt;</code>.',
+      ],
+    ];
+    $data['VALID: Style plugin has multiple styles with different labels'] = [
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'blockQuote',
+            'style',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_style' => [
+            'styles' => [
+              [
+                'label' => 'Callout',
+                'element' => '<p class="callout">',
+              ],
+              [
+                'label' => 'Interesting & highlighted quote',
+                'element' => '<blockquote class="interesting highlighted">',
+              ],
+              [
+                'label' => 'Famous',
+                'element' => '<blockquote class="famous">',
+              ],
+            ],
+          ],
+        ],
+      ],
+      'violations' => [],
+    ];
 
     return $data;
   }
@@ -749,7 +978,8 @@ public function providerPair(): array {
         'settings.plugins.ckeditor5_sourceEditing.allowed_tags.1' => 'The following tag(s) are already supported by available plugins and should not be added to the Source Editing "Manually editable HTML tags" field. Instead, enable the following plugins to support these tags: <em class="placeholder">Table (&lt;table&gt;)</em>.',
         'settings.plugins.ckeditor5_sourceEditing.allowed_tags.3' => 'The following attribute(s) are already supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: <em class="placeholder">Language (&lt;span lang&gt;)</em>.',
         'settings.plugins.ckeditor5_sourceEditing.allowed_tags.5' => 'The following attribute(s) are already supported by available plugins and should not be added to the Source Editing "Manually editable HTML tags" field. Instead, enable the following plugins to support these attributes: <em class="placeholder">Code Block (&lt;code class=&quot;language-*&quot;&gt;)</em>.',
-        'settings.plugins.ckeditor5_sourceEditing.allowed_tags.6' => 'The following attribute(s) are already supported by available plugins and should not be added to the Source Editing "Manually editable HTML tags" field. Instead, enable the following plugins to support these attributes: <em class="placeholder">Alignment (&lt;h2 class=&quot;text-align-center&quot;&gt;)</em>.',
+        // @todo "Style" should be removed from the suggestions in https://www.drupal.org/project/drupal/issues/3271179
+        'settings.plugins.ckeditor5_sourceEditing.allowed_tags.6' => 'The following attribute(s) are already supported by available plugins and should not be added to the Source Editing "Manually editable HTML tags" field. Instead, enable the following plugins to support these attributes: <em class="placeholder">Style (&lt;h2 class=&quot;text-align-center&quot;&gt;), Alignment (&lt;h2 class=&quot;text-align-center&quot;&gt;)</em>.',
       ],
     ];
     $data['INVALID some invalid Source Editable tags provided by plugin and another available in a not enabled plugin'] = [
@@ -985,7 +1215,44 @@ public function providerPair(): array {
       'filters' => [],
       'violations' => [],
     ];
-
+    $data['INVALID: Style plugin configured to add class to unsupported tag'] = [
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'style',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_style' => [
+            'styles' => [
+              [
+                'label' => 'Highlighted',
+                'element' => '<blockquote class="highlighted">',
+              ],
+            ],
+          ],
+        ],
+      ],
+      'image_upload' => [
+        'status' => FALSE,
+      ],
+      'filters' => [
+        'filter_html' => [
+          'id' => 'filter_html',
+          'provider' => 'filter',
+          'status' => TRUE,
+          'weight' => 0,
+          'settings' => [
+            'allowed_html' => '<p> <br> <blockquote class="highlighted">',
+            'filter_html_help' => TRUE,
+            'filter_html_nofollow' => TRUE,
+          ],
+        ],
+      ],
+      'violations' => [
+        'settings.plugins.ckeditor5_style' => 'The <em class="placeholder">Style</em> plugin needs another plugin to create <code>&lt;blockquote&gt;</code>, for it to be able to create the following attributes: <code>&lt;blockquote class=&quot;highlighted&quot;&gt;</code>. Enable a plugin that supports creating this tag. If none exists, you can configure the Source Editing plugin to support it.',
+      ],
+    ];
     return $data;
   }
 
diff --git a/core/modules/ckeditor5/tests/src/Traits/CKEditor5TestTrait.php b/core/modules/ckeditor5/tests/src/Traits/CKEditor5TestTrait.php
index 1b2adbb599857e22ac749452c21ea036372293ca..1556312b6824eb4bb2dd49fc56a072509b4df057 100644
--- a/core/modules/ckeditor5/tests/src/Traits/CKEditor5TestTrait.php
+++ b/core/modules/ckeditor5/tests/src/Traits/CKEditor5TestTrait.php
@@ -134,4 +134,24 @@ protected function getBalloonButton(string $name): NodeElement {
     return $button;
   }
 
+  /**
+   * Selects text inside an element.
+   *
+   * @param string $selector
+   *   A CSS selector for the element which contents should be selected.
+   */
+  protected function selectTextInsideElement(string $selector): void {
+    $javascript = <<<JS
+(function() {
+  const el = document.querySelector(".ck-editor__main $selector");
+  const range = document.createRange();
+  range.selectNodeContents(el);
+  const sel = window.getSelection();
+  sel.removeAllRanges();
+  sel.addRange(range);
+})();
+JS;
+    $this->getSession()->evaluateScript($javascript);
+  }
+
 }
diff --git a/core/modules/ckeditor5/tests/src/Unit/StylePluginTest.php b/core/modules/ckeditor5/tests/src/Unit/StylePluginTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1418467d8dd21c2a224eddbcbdf46ecaf209c477
--- /dev/null
+++ b/core/modules/ckeditor5/tests/src/Unit/StylePluginTest.php
@@ -0,0 +1,88 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\ckeditor5\Unit;
+
+use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style;
+use Drupal\editor\EditorInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style
+ * @group ckeditor5
+ * @internal
+ */
+class StylePluginTest extends UnitTestCase {
+
+  /**
+   * Provides a list of configs to test.
+   */
+  public function providerGetDynamicPluginConfig(): array {
+    return [
+      'default configuration (empty)' => [
+        [
+          'styles' => [],
+        ],
+        [
+          'style' => [
+            'definitions' => [],
+          ],
+        ],
+      ],
+      'Simple' => [
+        [
+          'styles' => [
+            ['label' => 'fancy blockquote', 'element' => '<blockquote class="fancy">'],
+          ],
+        ],
+        [
+          'style' => [
+            'definitions' => [
+              [
+                'name' => 'fancy blockquote',
+                'element' => 'blockquote',
+                'classes' => ['fancy'],
+              ],
+            ],
+          ],
+        ],
+      ],
+      'Complex' => [
+        [
+          'styles' => [
+            ['label' => 'fancy highlighted blockquote', 'element' => '<blockquote class="fancy highlighted">'],
+            ['label' => 'important foobar', 'element' => '<foobar class="important">'],
+          ],
+        ],
+        [
+          'style' => [
+            'definitions' => [
+              [
+                'name' => 'fancy highlighted blockquote',
+                'element' => 'blockquote',
+                'classes' => ['fancy', 'highlighted'],
+              ],
+              [
+                'name' => 'important foobar',
+                'element' => 'foobar',
+                'classes' => ['important'],
+              ],
+            ],
+          ],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * @covers ::getDynamicPluginConfig
+   * @dataProvider providerGetDynamicPluginConfig
+   */
+  public function testGetDynamicPluginConfig(array $configuration, array $expected_dynamic_config): void {
+    $plugin = new Style($configuration, 'ckeditor5_style', NULL);
+    $dynamic_plugin_config = $plugin->getDynamicPluginConfig([], $this->prophesize(EditorInterface::class)->reveal());
+    $this->assertSame($expected_dynamic_config, $dynamic_plugin_config);
+  }
+
+}
diff --git a/core/package.json b/core/package.json
index c0dd3f4787b7b54a07304f6668965f6ddb391af9..66226aa591fb8ab305fa1756db6b5808fc3698a7 100644
--- a/core/package.json
+++ b/core/package.json
@@ -59,6 +59,7 @@
     "@ckeditor/ckeditor5-remove-format": "35.0.x",
     "@ckeditor/ckeditor5-source-editing": "35.0.x",
     "@ckeditor/ckeditor5-special-characters": "35.0.x",
+    "@ckeditor/ckeditor5-style": "35.0.x",
     "@ckeditor/ckeditor5-table": "35.0.x",
     "@drupal/once": "1.0.x",
     "@popperjs/core": "2.11.x",
diff --git a/core/yarn.lock b/core/yarn.lock
index b29a54177e5093a6d5a25d00634dcb6d84b5cdbf..9a12a4b8557f8ea4190a611f40c90f36b70db51c 100644
--- a/core/yarn.lock
+++ b/core/yarn.lock
@@ -1137,6 +1137,13 @@
   dependencies:
     ckeditor5 "^35.0.1"
 
+"@ckeditor/ckeditor5-style@35.0.x":
+  version "35.0.1"
+  resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-style/-/ckeditor5-style-35.0.1.tgz#1d321d3bef67ba07f8a329d0061b4782ed1aa542"
+  integrity sha512-Z/GyXt0J+0ua+X2eIRN2dBZU42z60wgS3hLcijIpj8rAz+SnlRQhFJ5hgdz188jwAksoxPR94Vgs4oJU40t8ww==
+  dependencies:
+    ckeditor5 "^35.0.1"
+
 "@ckeditor/ckeditor5-table@35.0.x":
   version "35.0.1"
   resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-table/-/ckeditor5-table-35.0.1.tgz#7a1a3c339cd3f74cf7024f15f772bd2c2a9877b5"