diff --git a/src/Elastic.Markdown/.postcssrc b/src/Elastic.Markdown/.postcssrc
new file mode 100644
index 000000000..9bdfb70ff
--- /dev/null
+++ b/src/Elastic.Markdown/.postcssrc
@@ -0,0 +1,6 @@
+{
+ "plugins": {
+ "@tailwindcss/postcss": {},
+ "postcss-import": {},
+ }
+}
diff --git a/src/Elastic.Markdown/Assets/hljs-merge-html-plugin.js b/src/Elastic.Markdown/Assets/hljs-merge-html-plugin.js
new file mode 100644
index 000000000..fda658a34
--- /dev/null
+++ b/src/Elastic.Markdown/Assets/hljs-merge-html-plugin.js
@@ -0,0 +1,180 @@
+export const mergeHTMLPlugin = (function () {
+ 'use strict';
+
+ var originalStream;
+
+ /**
+ * @param {string} value
+ * @returns {string}
+ */
+ function escapeHTML(value) {
+ return value
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+ /* plugin itself */
+
+ /** @type {HLJSPlugin} */
+ const mergeHTMLPlugin = {
+ // preserve the original HTML token stream
+ "before:highlightElement": ({ el }) => {
+ originalStream = nodeStream(el);
+ },
+ // merge it afterwards with the highlighted token stream
+ "after:highlightElement": ({ el, result, text }) => {
+ if (!originalStream.length) return;
+
+ const resultNode = document.createElement('div');
+ resultNode.innerHTML = result.value;
+ result.value = mergeStreams(originalStream, nodeStream(resultNode), text);
+ el.innerHTML = result.value;
+ }
+ };
+
+ /* Stream merging support functions */
+
+ /**
+ * @typedef Event
+ * @property {'start'|'stop'} event
+ * @property {number} offset
+ * @property {Node} node
+ */
+
+ /**
+ * @param {Node} node
+ */
+ function tag(node) {
+ return node.nodeName.toLowerCase();
+ }
+
+ /**
+ * @param {Node} node
+ */
+ function nodeStream(node) {
+ /** @type Event[] */
+ const result = [];
+ (function _nodeStream(node, offset) {
+ for (let child = node.firstChild; child; child = child.nextSibling) {
+ if (child.nodeType === 3) {
+ offset += child.nodeValue.length;
+ } else if (child.nodeType === 1) {
+ result.push({
+ event: 'start',
+ offset: offset,
+ node: child
+ });
+ offset = _nodeStream(child, offset);
+ // Prevent void elements from having an end tag that would actually
+ // double them in the output. There are more void elements in HTML
+ // but we list only those realistically expected in code display.
+ if (!tag(child).match(/br|hr|img|input/)) {
+ result.push({
+ event: 'stop',
+ offset: offset,
+ node: child
+ });
+ }
+ }
+ }
+ return offset;
+ })(node, 0);
+ return result;
+ }
+
+ /**
+ * @param {any} original - the original stream
+ * @param {any} highlighted - stream of the highlighted source
+ * @param {string} value - the original source itself
+ */
+ function mergeStreams(original, highlighted, value) {
+ let processed = 0;
+ let result = '';
+ const nodeStack = [];
+
+ function selectStream() {
+ if (!original.length || !highlighted.length) {
+ return original.length ? original : highlighted;
+ }
+ if (original[0].offset !== highlighted[0].offset) {
+ return (original[0].offset < highlighted[0].offset) ? original : highlighted;
+ }
+
+ /*
+ To avoid starting the stream just before it should stop the order is
+ ensured that original always starts first and closes last:
+
+ if (event1 == 'start' && event2 == 'start')
+ return original;
+ if (event1 == 'start' && event2 == 'stop')
+ return highlighted;
+ if (event1 == 'stop' && event2 == 'start')
+ return original;
+ if (event1 == 'stop' && event2 == 'stop')
+ return highlighted;
+
+ ... which is collapsed to:
+ */
+ return highlighted[0].event === 'start' ? original : highlighted;
+ }
+
+ /**
+ * @param {Node} node
+ */
+ function open(node) {
+ /** @param {Attr} attr */
+ function attributeString(attr) {
+ return ' ' + attr.nodeName + '="' + escapeHTML(attr.value) + '"';
+ }
+ // @ts-ignore
+ result += '<' + tag(node) + [].map.call(node.attributes, attributeString).join('') + '>';
+ }
+
+ /**
+ * @param {Node} node
+ */
+ function close(node) {
+ result += '' + tag(node) + '>';
+ }
+
+ /**
+ * @param {Event} event
+ */
+ function render(event) {
+ (event.event === 'start' ? open : close)(event.node);
+ }
+
+ while (original.length || highlighted.length) {
+ let stream = selectStream();
+ result += escapeHTML(value.substring(processed, stream[0].offset));
+ processed = stream[0].offset;
+ if (stream === original) {
+ /*
+ On any opening or closing tag of the original markup we first close
+ the entire highlighted node stack, then render the original tag along
+ with all the following original tags at the same offset and then
+ reopen all the tags on the highlighted stack.
+ */
+ nodeStack.reverse().forEach(close);
+ do {
+ render(stream.splice(0, 1)[0]);
+ stream = selectStream();
+ } while (stream === original && stream.length && stream[0].offset === processed);
+ nodeStack.reverse().forEach(open);
+ } else {
+ if (stream[0].event === 'start') {
+ nodeStack.push(stream[0].node);
+ } else {
+ nodeStack.pop();
+ }
+ render(stream.splice(0, 1)[0]);
+ }
+ }
+ return result + escapeHTML(value.substr(processed));
+ }
+
+ return mergeHTMLPlugin;
+}());
diff --git a/src/Elastic.Markdown/Assets/main.js b/src/Elastic.Markdown/Assets/main.js
index e69de29bb..2ea9c46b6 100644
--- a/src/Elastic.Markdown/Assets/main.js
+++ b/src/Elastic.Markdown/Assets/main.js
@@ -0,0 +1,18 @@
+import hljs from "highlight.js";
+import {mergeHTMLPlugin} from "./hljs-merge-html-plugin";
+
+hljs.registerLanguage('apiheader', function() {
+ return {
+ case_insensitive: true, // language is case-insensitive
+ keywords: 'GET POST PUT DELETE HEAD OPTIONS PATCH',
+ contains: [
+ hljs.HASH_COMMENT_MODE,
+ {
+ className: "subst", // (pathname: path1/path2/dothis) color #ab5656
+ begin: /(?<=(?:\/|GET |POST |PUT |DELETE |HEAD |OPTIONS |PATH))[^?\n\r\/]+/,
+ }
+ ], }
+})
+
+hljs.addPlugin(mergeHTMLPlugin);
+hljs.highlightAll();
diff --git a/src/Elastic.Markdown/Assets/markdown/typography.css b/src/Elastic.Markdown/Assets/markdown/typography.css
new file mode 100644
index 000000000..e8c9cc4e8
--- /dev/null
+++ b/src/Elastic.Markdown/Assets/markdown/typography.css
@@ -0,0 +1,29 @@
+#elastic-docs-v3 {
+ h1 {
+ @apply text-4xl text-ink font-semibold mb-6;
+ line-height: 1.2em;
+ letter-spacing: -0.04em;
+ }
+
+ h2 {
+ @apply text-2xl text-ink font-bold mb-6;
+ line-height: 1.2em;
+ letter-spacing: -0.02em;
+ }
+
+ h3 {
+ @apply text-xl text-ink font-bold mb-6;
+ line-height: 1.2em;
+ letter-spacing: -0.02em;
+ }
+
+ p {
+ @apply text-base text-body mb-6;
+ line-height: 1.5em;
+ letter-spacing: 0;
+ }
+
+ a {
+ @apply text-blue-elastic hover:underline underline-offset-4;
+ }
+}
diff --git a/src/Elastic.Markdown/Assets/styles.css b/src/Elastic.Markdown/Assets/styles.css
index bd36c718c..e74cf4612 100644
--- a/src/Elastic.Markdown/Assets/styles.css
+++ b/src/Elastic.Markdown/Assets/styles.css
@@ -1,8 +1,8 @@
-@import "legacy/pygments.css";
-@import "legacy/shibuya.css";
-@import "legacy/mystnb.css";
-@import "legacy/copybutton.css";
-@import "legacy/togglebutton.css";
-@import "legacy/sphinx-design.min.css";
-@import "legacy/custom.css";
-@import "legacy/atom-one-light.css";
+@import "tailwindcss";
+@import "highlight.js/styles/atom-one-dark.css";
+@import "./theme.css";
+@import "./markdown/typography.css";
+
+main.markdown-content {
+ max-width: 80ch;
+}
diff --git a/src/Elastic.Markdown/Assets/theme.css b/src/Elastic.Markdown/Assets/theme.css
new file mode 100644
index 000000000..f57d7627c
--- /dev/null
+++ b/src/Elastic.Markdown/Assets/theme.css
@@ -0,0 +1,44 @@
+@theme {
+ --color-*: initial;
+
+ --color-white: #FFFFFF;
+ --color-black: #000000;
+
+ --color-body: #515151;
+
+ --color-ink: #343741;
+ --color-ink-light: #535966;
+ --color-ink-dark: #1C1E23;
+
+ --color-gray: #E6EBF2;
+ --color-gray-light: #F5F7FA;
+ --color-gray-dark: #D4DAE5;
+
+ --color-blue-elastic: #0077CC;
+ --color-blue-sky: #36B9FF;
+ --color-blue-midnight: #20377D;
+
+ --color-red-light: #FB6363;
+ --color-red-dark: #D93333;
+
+ --color-green-light: #3CD278;
+ --color-green-dark: #148742;
+
+ --color-teal: #00BFB3;
+ --color-teal-light: #48EFCF;
+ --color-teal-dark: #00857F;
+
+ --color-poppy: #FA744E;
+ --color-poppy-light: #FF957D;
+ --color-poppy-dark: #E2543D;
+
+ --color-pink: #F04E98;
+ --color-pink-light: #F990C6;
+ --color-pink-dark: #DD0A73;
+
+ --color-yellow: #FEC514;
+ --color-yellow-light: #FFD836;
+ --color-yellow-dark: #F9B110;
+
+ --spacing: 4px;
+}
diff --git a/src/Elastic.Markdown/Slices/Layout/_Head.cshtml b/src/Elastic.Markdown/Slices/Layout/_Head.cshtml
index 5a09c8e35..b72ceb6e1 100644
--- a/src/Elastic.Markdown/Slices/Layout/_Head.cshtml
+++ b/src/Elastic.Markdown/Slices/Layout/_Head.cshtml
@@ -7,17 +7,24 @@
-
-
+
+
+
+
+
+
+
+
+
+ :root {
+ --sy-f-text: "Inter", var(--sy-f-sys), var(--sy-f-cjk), sans-serif;
+ --sy-f-heading: "Inter", var(--sy-f-sys), var(--sy-f-cjk), sans-serif;
+ }
+
diff --git a/src/Elastic.Markdown/Slices/_Layout.cshtml b/src/Elastic.Markdown/Slices/_Layout.cshtml
index e544fbc92..5b5772e4b 100644
--- a/src/Elastic.Markdown/Slices/_Layout.cshtml
+++ b/src/Elastic.Markdown/Slices/_Layout.cshtml
@@ -1,4 +1,31 @@
@inherits RazorLayoutSlice
+@if (Model.IsRedesign)
+{
+
+
+
+ @Model.Title
+
+
+
+
+
+
+
+
+
+
+ @await RenderBodyAsync()
+
+
+
+
+
+
+
+}
+else
+{
@(await RenderPartialAsync(_Head.Create(Model)))
@@ -98,4 +125,5 @@
@await RenderSectionAsync("scripts")
-