This is an automated email from the ASF dual-hosted git repository.

Gerrrr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/otava-website.git


The following commit(s) were added to refs/heads/master by this push:
     new b9affd6  Swap theme & Add front page animations (#13)
b9affd6 is described below

commit b9affd6cd66d1da8acd0cec09df7aa5cfc4d2607
Author: Sarthak Singh <[email protected]>
AuthorDate: Thu Jul 2 21:47:44 2026 +0200

    Swap theme & Add front page animations (#13)
    
    * Add front page animations
    
    Make animation start when you scroll to it
    
    nit
    
    Clean up pnpm, IDE configuration, and patch files
    
    * Add tagline and premium features grid to the homepage
    
    * Update style of Change Detection heading and description to match 
FeaturesGrid
    
    * Clarify in FlowAnimation that performance tests publish CPU/throughput 
metrics
    
    * Remove em dashes from homepage animations text
    
    * Expand FlowAnimation with Performance Tests node and remove redundant 
FeaturesGrid header
    
    * Light color scheme
    
    Also fixes dangling line on the first animation
    
    * Fix typecheck errors and remove YAML Templates feature card
    
    * Add asf-header
    
    ---------
    
    Co-authored-by: Alex Sorokoumov <[email protected]>
---
 .gitignore                                         |  13 +
 docusaurus.config.ts                               |   4 +
 package-lock.json                                  |  98 +++-
 package.json                                       |   3 +
 .../HomepageFeatures/ChangePointDemo.module.css    | 219 +++++++++
 .../HomepageFeatures/ChangePointDemo.tsx           | 545 +++++++++++++++++++++
 .../HomepageFeatures/FeaturesGrid.module.css       | 173 +++++++
 src/components/HomepageFeatures/FeaturesGrid.tsx   | 100 ++++
 .../HomepageFeatures/FlowAnimation.module.css      | 132 +++++
 src/components/HomepageFeatures/FlowAnimation.tsx  | 321 ++++++++++++
 src/components/HomepageFeatures/index.tsx          |  12 +-
 src/components/HomepageFeatures/styles.module.css  |  35 +-
 src/css/custom.css                                 | 118 ++++-
 .../styles.module.css => global.d.ts}              |  22 +-
 src/pages/index.module.css                         |  62 ++-
 src/pages/index.tsx                                |   3 +
 tsconfig.json                                      |   5 +-
 17 files changed, 1836 insertions(+), 29 deletions(-)

diff --git a/.gitignore b/.gitignore
index b2d6de3..b5b0f07 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,16 @@
 npm-debug.log*
 yarn-debug.log*
 yarn-error.log*
+pnpm-debug.log*
+
+# IDEs and editors
+.idea/
+.vscode/
+*.sw?
+
+# Patch files
+*.patch
+
+# Alternate package manager files
+pnpm-lock.yaml
+pnpm-workspace.yaml
diff --git a/docusaurus.config.ts b/docusaurus.config.ts
index c6d3527..5b780b2 100644
--- a/docusaurus.config.ts
+++ b/docusaurus.config.ts
@@ -77,6 +77,10 @@ const config: Config = {
         
"sha384-nB0miv6/jRmo5UMMR1wu3Gz6NLsoTkbqJghGIsx//Rlm+ZU03BU6SQNC66gy8m5B",
       crossorigin: "anonymous",
     },
+    {
+      href: 
"https://fonts.googleapis.com/css2?family=Manrope:[email protected]&family=Space+Grotesk:[email protected]&family=JetBrains+Mono:wght@400;500;700&display=swap";,
+      type: "text/css",
+    },
   ],
 
   themeConfig: {
diff --git a/package-lock.json b/package-lock.json
index e7bab5b..a44c2eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
         "@docusaurus/preset-classic": "*",
         "@mdx-js/react": "*",
         "clsx": "*",
+        "motion": "^12.42.0",
         "prism-react-renderer": "*",
         "react": "*",
         "react-dom": "*",
@@ -164,6 +165,7 @@
       "resolved": 
"https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.1.tgz";,
       "integrity": 
"sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@algolia/client-common": "5.52.1",
         "@algolia/requester-browser-xhr": "5.52.1",
@@ -289,6 +291,7 @@
       "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz";,
       "integrity": 
"sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@babel/code-frame": "^7.29.0",
         "@babel/generator": "^7.29.0",
@@ -2096,6 +2099,7 @@
         }
       ],
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=18"
       },
@@ -2118,6 +2122,7 @@
         }
       ],
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=18"
       }
@@ -2227,6 +2232,7 @@
       "resolved": 
"https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz";,
       "integrity": 
"sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "cssesc": "^3.0.0",
         "util-deprecate": "^1.0.2"
@@ -2648,6 +2654,7 @@
       "resolved": 
"https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz";,
       "integrity": 
"sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "cssesc": "^3.0.0",
         "util-deprecate": "^1.0.2"
@@ -3666,6 +3673,7 @@
       "resolved": 
"https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.1.tgz";,
       "integrity": 
"sha512-2jRVrtzjf8LClGTHQlwlwuD3wQXRx3WEoF7XUarJ8Ou+0onV+SLtejsyfY9JLpfUh9hPhXM4pbBGkyAY4Bi3HQ==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@docusaurus/core": "3.10.1",
         "@docusaurus/logger": "3.10.1",
@@ -4666,6 +4674,7 @@
       "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz";,
       "integrity": 
"sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@types/mdx": "^2.0.0"
       },
@@ -5138,6 +5147,7 @@
       "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz";,
       "integrity": 
"sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@babel/core": "^7.21.3",
         "@svgr/babel-preset": "8.1.0",
@@ -5497,6 +5507,7 @@
       "resolved": 
"https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz";,
       "integrity": 
"sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "csstype": "^3.2.2"
       }
@@ -5838,6 +5849,7 @@
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz";,
       "integrity": 
"sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
       "license": "MIT",
+      "peer": true,
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -5905,6 +5917,7 @@
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz";,
       "integrity": 
"sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "fast-deep-equal": "^3.1.3",
         "fast-uri": "^3.0.1",
@@ -5950,6 +5963,7 @@
       "resolved": 
"https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.1.tgz";,
       "integrity": 
"sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@algolia/abtesting": "1.18.1",
         "@algolia/client-abtesting": "5.52.1",
@@ -6411,6 +6425,7 @@
         }
       ],
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "baseline-browser-mapping": "^2.10.12",
         "caniuse-lite": "^1.0.30001782",
@@ -7377,6 +7392,7 @@
       "resolved": 
"https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz";,
       "integrity": 
"sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "cssesc": "^3.0.0",
         "util-deprecate": "^1.0.2"
@@ -8717,6 +8733,7 @@
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz";,
       "integrity": 
"sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "fast-json-stable-stringify": "^2.0.0",
@@ -8906,6 +8923,33 @@
         "url": "https://github.com/sponsors/rawify";
       }
     },
+    "node_modules/framer-motion": {
+      "version": "12.42.0",
+      "resolved": 
"https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.0.tgz";,
+      "integrity": 
"sha512-wp7EJnfWaaEScVygKv3e20udoRz+LbtxScsuTkakAxfXmt+ReC6WyPW2nINRAGvd+hG9odwcjBLyOTPjH5pBRA==",
+      "license": "MIT",
+      "dependencies": {
+        "motion-dom": "^12.42.0",
+        "motion-utils": "^12.39.0",
+        "tslib": "^2.4.0"
+      },
+      "peerDependencies": {
+        "@emotion/is-prop-valid": "*",
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/is-prop-valid": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/fresh": {
       "version": "0.5.2",
       "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz";,
@@ -13176,6 +13220,47 @@
         "url": "https://github.com/sponsors/ljharb";
       }
     },
+    "node_modules/motion": {
+      "version": "12.42.0",
+      "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.0.tgz";,
+      "integrity": 
"sha512-Qhwvu9sVl5/URSq5CNzwMCpSKK8Uhnrwb6VO977kZyj/wOCS7mWebJUnBoHx5cZU1Zv8a9BD5CSICWKAlrLJgA==",
+      "license": "MIT",
+      "dependencies": {
+        "framer-motion": "^12.42.0",
+        "tslib": "^2.4.0"
+      },
+      "peerDependencies": {
+        "@emotion/is-prop-valid": "*",
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/is-prop-valid": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/motion-dom": {
+      "version": "12.42.0",
+      "resolved": 
"https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.0.tgz";,
+      "integrity": 
"sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w==",
+      "license": "MIT",
+      "dependencies": {
+        "motion-utils": "^12.39.0"
+      }
+    },
+    "node_modules/motion-utils": {
+      "version": "12.39.0",
+      "resolved": 
"https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz";,
+      "integrity": 
"sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==",
+      "license": "MIT"
+    },
     "node_modules/mrmime": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz";,
@@ -13344,6 +13429,7 @@
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz";,
       "integrity": 
"sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "fast-json-stable-stringify": "^2.0.0",
@@ -13876,6 +13962,7 @@
         }
       ],
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "nanoid": "^3.3.11",
         "picocolors": "^1.1.1",
@@ -14779,6 +14866,7 @@
       "resolved": 
"https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz";,
       "integrity": 
"sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "cssesc": "^3.0.0",
         "util-deprecate": "^1.0.2"
@@ -15595,6 +15683,7 @@
       "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz";,
       "integrity": 
"sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -15604,6 +15693,7 @@
       "resolved": 
"https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz";,
       "integrity": 
"sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "scheduler": "^0.27.0"
       },
@@ -15659,6 +15749,7 @@
       "resolved": 
"https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz";,
       "integrity": 
"sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@types/react": "*"
       },
@@ -15687,6 +15778,7 @@
       "resolved": 
"https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz";,
       "integrity": 
"sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@babel/runtime": "^7.12.13",
         "history": "^4.9.0",
@@ -17501,7 +17593,8 @@
       "version": "2.8.1",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz";,
       "integrity": 
"sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
-      "license": "0BSD"
+      "license": "0BSD",
+      "peer": true
     },
     "node_modules/tsyringe": {
       "version": "4.10.0",
@@ -17582,6 +17675,7 @@
       "integrity": 
"sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
       "devOptional": true,
       "license": "Apache-2.0",
+      "peer": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -17951,6 +18045,7 @@
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz";,
       "integrity": 
"sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "fast-json-stable-stringify": "^2.0.0",
@@ -18150,6 +18245,7 @@
       "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz";,
       "integrity": 
"sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@types/eslint-scope": "^3.7.7",
         "@types/estree": "^1.0.8",
diff --git a/package.json b/package.json
index 4c50529..89c2901 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
     "@docusaurus/preset-classic": "*",
     "@mdx-js/react": "*",
     "clsx": "*",
+    "motion": "^12.42.0",
     "prism-react-renderer": "*",
     "react": "*",
     "react-dom": "*",
@@ -29,6 +30,8 @@
     "@docusaurus/module-type-aliases": "*",
     "@docusaurus/tsconfig": "*",
     "@docusaurus/types": "*",
+    "@types/react": "^19.2.17",
+    "@types/react-dom": "^19.2.3",
     "typescript": "*"
   },
   "browserslist": {
diff --git a/src/components/HomepageFeatures/ChangePointDemo.module.css 
b/src/components/HomepageFeatures/ChangePointDemo.module.css
new file mode 100644
index 0000000..637f29e
--- /dev/null
+++ b/src/components/HomepageFeatures/ChangePointDemo.module.css
@@ -0,0 +1,219 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.demo {
+  /* theme-adaptive palette */
+  --cp-line: var(--ifm-color-primary);
+  --cp-before: var(--otava-improve);
+  --cp-after: var(--otava-regress);
+  --cp-heat: var(--otava-accent2);
+  --cp-muted: var(--ifm-color-emphasis-500);
+
+  width: 100%;
+  max-width: 45rem;
+  margin: 0 auto;
+  padding: 1rem 1.25rem 1.5rem;
+  border: 0.0625rem solid var(--ifm-color-emphasis-200);
+  border-radius: 0.75rem;
+  background: var(--ifm-background-surface-color);
+  box-shadow: 0 0.0625rem 0.1875rem rgba(0, 0, 0, 0.06);
+}
+
+.title {
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+  font-size: 0.95rem;
+  font-weight: 600;
+  margin-bottom: 0.5rem;
+  color: var(--ifm-heading-color);
+}
+.tagBad,
+.tagGood {
+  font-size: 0.72rem;
+  font-weight: 700;
+  padding: 0.1rem 0.5rem;
+  border-radius: 62.4375rem;
+  transition: opacity 0.4s ease;
+}
+.tagBad {
+  color: var(--status-error-text);
+  background: var(--status-error-subtle);
+}
+.tagGood {
+  color: var(--status-success-text);
+  background: var(--status-success-subtle);
+}
+
+.svg {
+  width: 100%;
+  height: auto;
+  display: block;
+  overflow: visible;
+}
+
+.axis {
+  stroke: var(--ifm-color-emphasis-300);
+  stroke-width: 1;
+}
+
+.line {
+  fill: none;
+  stroke: var(--cp-line);
+  stroke-width: 2.5;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+.dot {
+  fill: var(--cp-line);
+  transition: fill 0.4s ease;
+}
+.dotAfter {
+  fill: var(--cp-after);
+}
+
+.segBefore {
+  fill: var(--cp-before);
+  opacity: 0.08;
+}
+.segAfter {
+  fill: var(--cp-after);
+  opacity: 0.08;
+}
+
+.meanBefore {
+  stroke: var(--cp-before);
+  stroke-width: 1.5;
+  stroke-dasharray: 5 4;
+}
+.meanAfter {
+  stroke: var(--cp-after);
+  stroke-width: 1.5;
+  stroke-dasharray: 5 4;
+}
+
+.splitSweep {
+  stroke: var(--cp-muted);
+  stroke-width: 1.5;
+  stroke-dasharray: 3 3;
+  opacity: 0.7;
+}
+.splitLocked {
+  stroke: var(--cp-after);
+  stroke-width: 2.5;
+}
+.splitRejected {
+  stroke: var(--cp-muted);
+  stroke-width: 2;
+  stroke-dasharray: 5 4;
+}
+
+.qcell {
+  fill: var(--cp-heat);
+  transition: opacity 0.2s ease;
+}
+.qlabel {
+  fill: var(--cp-muted);
+  font-size: 11px;
+  font-family: var(--ifm-font-family-monospace);
+}
+
+.commit {
+  fill: var(--cp-muted);
+  font-size: 11px;
+  text-anchor: middle;
+  font-family: var(--ifm-font-family-monospace);
+}
+
+.badge {
+  fill: var(--ifm-background-color);
+  stroke: var(--cp-after);
+  stroke-width: 1.5;
+}
+.badgePct {
+  fill: var(--cp-after);
+  font-size: 18px;
+  font-weight: 700;
+  font-family: var(--ifm-font-family-monospace);
+}
+.badgeMeta {
+  fill: var(--cp-muted);
+  font-size: 10px;
+  font-family: var(--ifm-font-family-monospace);
+}
+
+.reportWrap {
+  min-height: 9.375rem;
+  margin-top: 1rem;
+}
+
+.report {
+  background: var(--neutral-900);
+  border: 0.0625rem solid var(--teal-900);
+  border-radius: 0.5rem;
+  padding: 0.85rem 1rem;
+  font-family: var(--ifm-font-family-monospace);
+  font-size: 0.82rem;
+  line-height: 1.7;
+  color: #c9d1d9;
+  overflow-x: auto;
+  white-space: pre;
+}
+
+.reportLine {
+  white-space: pre;
+}
+
+.reportCmd {
+  color: #7ee787;
+}
+.reportHdr {
+  color: #e3b341;
+  font-weight: 700;
+}
+.reportBad {
+  color: var(--cp-after);
+}
+.reportIso {
+  color: #8b949e;
+}
+.commitBad {
+  color: var(--cp-after);
+  font-weight: 700;
+}
+.commitGood {
+  color: var(--cp-before);
+  font-weight: 700;
+}
+.reportAct {
+  color: var(--otava-star);
+}
+.reportOk {
+  color: var(--cp-before);
+  font-weight: 700;
+}
+
+.caption {
+  margin-top: 0.75rem;
+  text-align: center;
+  font-size: 0.95rem;
+  color: var(--ifm-color-emphasis-700);
+  min-height: 2.5em;
+}
diff --git a/src/components/HomepageFeatures/ChangePointDemo.tsx 
b/src/components/HomepageFeatures/ChangePointDemo.tsx
new file mode 100644
index 0000000..5ca6b4d
--- /dev/null
+++ b/src/components/HomepageFeatures/ChangePointDemo.tsx
@@ -0,0 +1,545 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useState, useEffect, useRef } from "react";
+import {
+  AnimatePresence,
+  motion,
+  useInView,
+  useReducedMotion,
+} from "motion/react";
+import styles from "./ChangePointDemo.module.css";
+
+/**
+ * A config-driven SVG animation (powered by Framer Motion) that illustrates
+ * how Otava analyzes a single metric's time series:
+ *
+ *   1. draw the series
+ *   2. sweep across every candidate split, scoring divergence (Q̂)
+ *   3. either LOCK onto a significant split (regression) ...
+ *      ... or REJECT every split as noise (stable)
+ *   4. report the verdict — alert on a real change, stay quiet otherwise
+ *
+ * The numbers are illustrative but follow the real per-metric pipeline.
+ */
+
+export type DemoConfig = {
+  title: string;
+  metric: string;
+  values: number[];
+  commits: string[];
+  /** index where the regime shifts, or null if the series is just noisy */
+  changeIndex: number | null;
+  domain: [number, number];
+  /** shared Q̂ normalization so both demos use the same heat scale */
+  qScale: number;
+  captions: { until: number; text: string }[];
+};
+
+// SVG geometry
+const W = 640;
+const H = 340;
+const PAD_L = 48;
+const PAD_R = 28;
+const PAD_T = 36;
+const PAD_B = 96;
+const PLOT_W = W - PAD_L - PAD_R;
+const PLOT_H = H - PAD_T - PAD_B;
+
+const mean = (a: number[]) => a.reduce((s, v) => s + v, 0) / a.length;
+
+// Illustrative Q-hat for a split starting at `idx`.
+function qhatOf(values: number[], idx: number): number {
+  const left = values.slice(0, idx);
+  const right = values.slice(idx);
+  if (left.length === 0 || right.length === 0) return 0;
+  const sep = Math.abs(mean(left) - mean(right));
+  const balance = (left.length * right.length) / values.length;
+  return sep * balance;
+}
+
+function bestSplit(values: number[]): number {
+  let best = 1;
+  let bestQ = -Infinity;
+  for (let i = 1; i < values.length; i++) {
+    const q = qhatOf(values, i);
+    if (q > bestQ) {
+      bestQ = q;
+      best = i;
+    }
+  }
+  return best;
+}
+
+const clamp01 = (n: number) => Math.min(1, Math.max(0, n));
+const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
+const phase = (p: number, from: number, to: number) => {
+  const t = clamp01((p - from) / (to - from));
+  return t * t * (3 - 2 * t);
+};
+
+const LOOP_MS = 15000;
+const LINE_LEN = 900;
+
+// ── the two demos ──────────────────────────────────────────────
+const REGRESSION: DemoConfig = {
+  title: "A real regression: Otava alerts",
+  metric: "throughput",
+  values: [120, 118, 121, 119, 95, 97, 94, 96],
+  commits: ["c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7"],
+  changeIndex: 4,
+  domain: [80, 130],
+  qScale: 1,
+  captions: [
+    { until: 0.12, text: "A single metric's history, with one point per 
commit." },
+    {
+      until: 0.44,
+      text: "Score every possible before/after split by divergence (Q̂).",
+    },
+    { until: 0.54, text: "Lock onto the split with the highest divergence." },
+    {
+      until: 0.7,
+      text: "Confirm it with a significance test of whole segments, not single 
points.",
+    },
+    {
+      until: 1.01,
+      text: "Persistent shift → alert, and isolate the offending commit.",
+    },
+  ],
+};
+
+const STABLE: DemoConfig = {
+  title: "Just noise: Otava stays quiet",
+  metric: "throughput",
+  values: [104, 117, 96, 112, 99, 119, 94, 110],
+  commits: ["d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7"],
+  changeIndex: null,
+  domain: [80, 130],
+  qScale: 1,
+  captions: [
+    { until: 0.12, text: "Same metric, but the data is just noisy this time." 
},
+    {
+      until: 0.44,
+      text: "Score every possible before/after split by divergence (Q̂).",
+    },
+    {
+      until: 0.7,
+      text: "Even the best split fails the significance test (high p-value).",
+    },
+    {
+      until: 1.01,
+      text: "No persistent shift → no false alarm. Nothing is sent.",
+    },
+  ],
+};
+
+// Share one heat scale across both so "loud" vs "quiet" reads honestly.
+const Q_SCALE = Math.max(
+  ...REGRESSION.values.map((_, i) => qhatOf(REGRESSION.values, i)),
+);
+REGRESSION.qScale = Q_SCALE;
+STABLE.qScale = Q_SCALE;
+
+export const DEMOS = { REGRESSION, STABLE };
+
+function captionIndex(cfg: DemoConfig, p: number) {
+  return cfg.captions.findIndex((c) => p < c.until);
+}
+
+export default function ChangePointDemo({
+  config,
+}: {
+  config: DemoConfig;
+}): JSX.Element {
+  const reduce = useReducedMotion();
+  const { values, commits, changeIndex, domain } = config;
+  const n = values.length;
+
+  const x = (i: number) => PAD_L + (i * PLOT_W) / (n - 1);
+  const y = (v: number) =>
+    PAD_T + (1 - (v - domain[0]) / (domain[1] - domain[0])) * PLOT_H;
+  const splitX = (idx: number) => (x(idx - 1) + x(idx)) / 2;
+
+  const isRegression = changeIndex !== null;
+  const markIdx = isRegression ? (changeIndex as number) : bestSplit(values);
+  const meanBefore = mean(values.slice(0, markIdx));
+  const meanAfter = mean(values.slice(markIdx));
+  const pctChange = (meanAfter / meanBefore - 1) * 100;
+
+  const SPLIT_INDICES = values.map((_, i) => i).filter((i) => i >= 1);
+
+  // managed clock triggered by viewport entry
+  const ref = useRef<HTMLElement>(null);
+  const isInView = useInView(ref, { amount: 0.35 });
+  const [p, setP] = useState(reduce ? 1 : 0);
+
+  useEffect(() => {
+    if (reduce) {
+      setP(1);
+      return;
+    }
+    if (!isInView) {
+      setP(0);
+      return;
+    }
+
+    let startTime = performance.now();
+    let animationFrameId: number;
+
+    const tick = (now: number) => {
+      const elapsed = now - startTime;
+      const progress = (elapsed % LOOP_MS) / LOOP_MS;
+      setP(progress);
+      animationFrameId = requestAnimationFrame(tick);
+    };
+
+    animationFrameId = requestAnimationFrame(tick);
+    return () => {
+      cancelAnimationFrame(animationFrameId);
+    };
+  }, [isInView, reduce]);
+
+  // phase timeline
+  const drawT = phase(p, 0.0, 0.1);
+  const sweepT = phase(p, 0.12, 0.44);
+  const lockT = phase(p, 0.44, 0.52);
+  const testT = phase(p, 0.54, 0.68);
+  const reportShown = p >= 0.7;
+
+  const sweepIdx = lerp(1, n - 1, sweepT);
+  const sweepPx = lerp(splitX(1), splitX(n - 1), sweepT);
+  const barX = lockT > 0 ? lerp(sweepPx, splitX(markIdx), lockT) : sweepPx;
+  const locked = lockT >= 1;
+
+  const linePath = values
+    .map((v, i) => `${i === 0 ? "M" : "L"} ${x(i)} ${y(v)}`)
+    .join(" ");
+
+  return (
+    <motion.figure
+      ref={ref}
+      className={styles.demo}
+      initial={{ opacity: 0, y: 24 }}
+      whileInView={{ opacity: 1, y: 0 }}
+      viewport={{ once: true, amount: 0.35 }}
+      transition={{ duration: 0.6, ease: "easeOut" }}
+    >
+      <figcaption className={styles.title}>
+        <span
+          className={isRegression ? styles.tagBad : styles.tagGood}
+          style={{ opacity: reportShown ? 1 : 0.35 }}
+        >
+          {isRegression ? "● regression" : "● stable"}
+        </span>
+        {config.title}
+      </figcaption>
+
+      <svg
+        viewBox={`0 0 ${W} ${H}`}
+        className={styles.svg}
+        role="img"
+        aria-label={config.title}
+      >
+        <line
+          x1={PAD_L}
+          y1={PAD_T + PLOT_H}
+          x2={W - PAD_R}
+          y2={PAD_T + PLOT_H}
+          className={styles.axis}
+        />
+
+        {/* segments + means (only when a real change-point locks) */}
+        {isRegression && locked && (
+          <g style={{ opacity: testT }}>
+            <rect
+              x={PAD_L}
+              y={PAD_T}
+              width={splitX(markIdx) - PAD_L}
+              height={PLOT_H}
+              className={styles.segBefore}
+            />
+            <rect
+              x={splitX(markIdx)}
+              y={PAD_T}
+              width={W - PAD_R - splitX(markIdx)}
+              height={PLOT_H}
+              className={styles.segAfter}
+            />
+            <line
+              x1={PAD_L}
+              y1={y(meanBefore)}
+              x2={splitX(markIdx)}
+              y2={y(meanBefore)}
+              className={styles.meanBefore}
+            />
+            <line
+              x1={splitX(markIdx)}
+              y1={y(meanAfter)}
+              x2={W - PAD_R}
+              y2={y(meanAfter)}
+              className={styles.meanAfter}
+            />
+          </g>
+        )}
+
+        {/* sweeping split bar */}
+        {sweepT > 0 && !locked && (
+          <line
+            x1={barX}
+            y1={PAD_T - 6}
+            x2={barX}
+            y2={PAD_T + PLOT_H + 6}
+            className={styles.splitSweep}
+          />
+        )}
+
+        {/* verdict bar: locked (regression) or rejected (stable) */}
+        <AnimatePresence>
+          {isRegression && locked && (
+            <motion.line
+              x1={splitX(markIdx)}
+              y1={PAD_T - 6}
+              x2={splitX(markIdx)}
+              y2={PAD_T + PLOT_H + 6}
+              className={styles.splitLocked}
+              initial={{ pathLength: 0, opacity: 0 }}
+              animate={{ pathLength: 1, opacity: 1 }}
+              exit={{ opacity: 0 }}
+              transition={{ type: "spring", stiffness: 220, damping: 22 }}
+            />
+          )}
+        </AnimatePresence>
+        {!isRegression && p >= 0.5 && (
+          <line
+            x1={splitX(markIdx)}
+            y1={PAD_T - 6}
+            x2={splitX(markIdx)}
+            y2={PAD_T + PLOT_H + 6}
+            className={styles.splitRejected}
+            style={{ opacity: phase(p, 0.5, 0.62) * (reportShown ? 0.5 : 1) }}
+          />
+        )}
+
+        {/* metric line */}
+        <path
+          d={linePath}
+          className={styles.line}
+          style={{
+            strokeDasharray: LINE_LEN,
+            strokeDashoffset: LINE_LEN * (1 - drawT),
+          }}
+        />
+
+        {/* points */}
+        {values.map((v, i) => (
+          <circle
+            key={i}
+            cx={x(i)}
+            cy={y(v)}
+            r={4.5}
+            className={
+              isRegression && locked && i >= markIdx
+                ? `${styles.dot} ${styles.dotAfter}`
+                : styles.dot
+            }
+            style={{ opacity: drawT }}
+          />
+        ))}
+
+        {/* commit labels */}
+        {commits.map((c, i) => (
+          <text
+            key={c}
+            x={x(i)}
+            y={PAD_T + PLOT_H + 18}
+            className={styles.commit}
+            style={{ opacity: drawT }}
+          >
+            {c}
+          </text>
+        ))}
+
+        {/* Q̂ divergence strip */}
+        {SPLIT_INDICES.map((idx) => {
+          const cx = splitX(idx);
+          const cellW = PLOT_W / n;
+          const revealed = sweepIdx >= idx - 0.05 || p >= 0.46;
+          const intensity = revealed ? qhatOf(values, idx) / config.qScale : 0;
+          return (
+            <rect
+              key={idx}
+              x={cx - cellW / 2}
+              y={PAD_T + PLOT_H + 32}
+              width={cellW - 3}
+              height={14}
+              rx={2}
+              className={styles.qcell}
+              style={{ opacity: revealed ? 0.18 + 0.8 * intensity : 0.08 }}
+            />
+          );
+        })}
+        <text
+          x={PAD_L}
+          y={PAD_T + PLOT_H + 60}
+          className={styles.qlabel}
+          style={{ opacity: phase(p, 0.16, 0.26) }}
+        >
+          Q̂ divergence per candidate split
+        </text>
+
+        {/* report badge (regression only) */}
+        <AnimatePresence>
+          {isRegression && reportShown && (
+            <motion.g
+              initial={{ opacity: 0, y: 8, scale: 0.9 }}
+              animate={{ opacity: 1, y: 0, scale: 1 }}
+              exit={{ opacity: 0 }}
+              transition={{ type: "spring", stiffness: 260, damping: 20 }}
+              style={{ transformBox: "fill-box", transformOrigin: "left top" }}
+            >
+              <rect
+                x={splitX(markIdx) + 8}
+                y={PAD_T + 6}
+                width={132}
+                height={42}
+                rx={6}
+                className={styles.badge}
+              />
+              <text
+                x={splitX(markIdx) + 18}
+                y={PAD_T + 24}
+                className={styles.badgePct}
+              >
+                {pctChange.toFixed(1)}%
+              </text>
+              <text
+                x={splitX(markIdx) + 18}
+                y={PAD_T + 39}
+                className={styles.badgeMeta}
+              >
+                p &lt; 0.001 · {commits[markIdx]}
+              </text>
+            </motion.g>
+          )}
+        </AnimatePresence>
+      </svg>
+
+      {/* report card */}
+      <div className={styles.reportWrap}>
+        <AnimatePresence>
+          {reportShown && (
+            <motion.div
+              className={styles.report}
+              initial={{ opacity: 0, y: 10 }}
+              animate={{ opacity: 1, y: 0 }}
+              exit={{ opacity: 0 }}
+              transition={{ duration: 0.3 }}
+            >
+              {(isRegression
+                ? regressionReport(config, meanBefore, meanAfter, pctChange)
+                : stableReport(config)
+              ).map((line, i) => (
+                <motion.div
+                  key={line.key}
+                  className={styles.reportLine}
+                  initial={{ opacity: 0, x: -6 }}
+                  animate={{ opacity: 1, x: 0 }}
+                  transition={{ delay: i * 0.18, duration: 0.25 }}
+                >
+                  {line}
+                </motion.div>
+              ))}
+            </motion.div>
+          )}
+        </AnimatePresence>
+      </div>
+
+      <figcaption className={styles.caption}>
+        <AnimatePresence mode="wait">
+          <motion.span
+            key={captionIndex(config, p)}
+            initial={{ opacity: 0, y: 6 }}
+            animate={{ opacity: 1, y: 0 }}
+            exit={{ opacity: 0, y: -6 }}
+            transition={{ duration: 0.3 }}
+          >
+            {config.captions[Math.max(0, captionIndex(config, p))].text}
+          </motion.span>
+        </AnimatePresence>
+      </figcaption>
+    </motion.figure>
+  );
+}
+
+function regressionReport(
+  cfg: DemoConfig,
+  before: number,
+  after: number,
+  pct: number,
+): JSX.Element[] {
+  const c = cfg.commits;
+  const i = cfg.changeIndex as number;
+  return [
+    <span key="cmd" className={styles.reportCmd}>
+      $ otava analyze {cfg.metric} --format regressions_only
+    </span>,
+    <span key="hdr" className={styles.reportHdr}>
+      Regressions in {cfg.metric}:
+    </span>,
+    <span key="metric">
+      {"  "}
+      {cfg.metric} : <b>{before.toFixed(1)}</b>
+      {"  ⟶  "}
+      <b className={styles.reportBad}>{after.toFixed(1)}</b>
+      {"   ("}
+      <b className={styles.reportBad}>{pct.toFixed(1)}%</b>
+      {")   p < 0.001"}
+    </span>,
+    <span key="iso" className={styles.reportIso}>
+      {"  ▸ isolated to commit "}
+      <span className={styles.commitBad}>{c[i]}</span>
+      {"   (last good "}
+      <span className={styles.commitGood}>{c[i - 1]}</span>
+      {" ⟶ first bad "}
+      <span className={styles.commitBad}>{c[i]}</span>
+      {")"}
+    </span>,
+    <span key="act" className={styles.reportAct}>
+      {"  → alert sent to Slack · suggested action: revert "}
+      <span className={styles.commitBad}>{c[i]}</span>
+    </span>,
+  ];
+}
+
+function stableReport(cfg: DemoConfig): JSX.Element[] {
+  return [
+    <span key="cmd" className={styles.reportCmd}>
+      $ otava analyze {cfg.metric} --format regressions_only
+    </span>,
+    <span key="ok" className={styles.reportOk}>
+      ✓ No regressions found in {cfg.metric}.
+    </span>,
+    <span key="why" className={styles.reportIso}>
+      {"  best split rejected: p = 0.58 > 0.01 threshold"}
+    </span>,
+    <span key="quiet" className={styles.reportIso}>
+      {"  8 data points recorded · 0 alerts sent"}
+    </span>,
+  ];
+}
diff --git a/src/components/HomepageFeatures/FeaturesGrid.module.css 
b/src/components/HomepageFeatures/FeaturesGrid.module.css
new file mode 100644
index 0000000..9cee0af
--- /dev/null
+++ b/src/components/HomepageFeatures/FeaturesGrid.module.css
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.container {
+  margin: 6rem auto 4rem;
+  max-width: 68.75rem;
+  padding: 0 1.5rem;
+}
+
+.sectionTitle {
+  font-size: 2.2rem;
+  font-weight: 700;
+  letter-spacing: -0.02em;
+  background: var(--otava-aurora);
+  -webkit-background-clip: text;
+  background-clip: text;
+  color: transparent;
+  margin-bottom: 0.75rem;
+}
+
+.sectionSubtitle {
+  font-size: 1.25rem;
+  color: var(--ifm-color-emphasis-700);
+  max-width: 43.75rem;
+  margin: 0 auto;
+  font-weight: 400;
+  line-height: 1.5;
+}
+
+.grid {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 2rem;
+  margin-top: 7rem;
+}
+
+.card {
+  background: var(--ifm-background-surface-color);
+  border: 0.0625rem solid var(--ifm-color-emphasis-200);
+  border-radius: var(--ifm-card-border-radius);
+  padding: 2.25rem 2rem;
+  transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  position: relative;
+  box-shadow: 0 0.25rem 1.5rem rgba(31, 31, 43, 0.06);
+}
+
+.card::before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 100%;
+  border-radius: var(--ifm-card-border-radius);
+  background: radial-gradient(
+    50rem circle at var(--x, 0px) var(--y, 0px),
+    rgba(63, 138, 134, 0.08),
+    transparent 40%
+  );
+  opacity: 0;
+  transition: opacity 0.5s ease;
+  pointer-events: none;
+}
+
+.card:hover {
+  transform: translateY(-0.375rem) scale(1.015);
+  border-color: rgba(63, 138, 134, 0.45);
+  box-shadow:
+    0 0.875rem 2.5rem rgba(35, 43, 43, 0.1),
+    0 0 1.5625rem rgba(63, 138, 134, 0.12);
+}
+
+.card:hover::before {
+  opacity: 1;
+}
+
+.iconWrapper {
+  width: 3rem;
+  height: 3rem;
+  border-radius: 0.75rem;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(63, 138, 134, 0.1);
+  color: var(--ifm-color-primary);
+  margin-bottom: 1.5rem;
+  transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.iconWrapper svg {
+  width: 1.5rem;
+  height: 1.5rem;
+}
+
+.card:hover .iconWrapper {
+  background: var(--otava-aurora);
+  color: #ffffff;
+  transform: scale(1.08) rotate(2deg);
+  box-shadow: 0 0 0.9375rem rgba(63, 138, 134, 0.3);
+}
+
+.cardTitle {
+  font-size: 1.375rem;
+  font-weight: 600;
+  margin-bottom: 0.85rem;
+  color: var(--ifm-heading-color);
+  letter-spacing: -0.01em;
+}
+
+.cardDesc {
+  font-size: 0.96875rem;
+  line-height: 1.62;
+  color: var(--ifm-color-emphasis-700);
+  opacity: 0.9;
+  margin: 0;
+  font-weight: 400;
+  transition: opacity 0.3s ease;
+}
+
+.card:hover .cardDesc {
+  opacity: 1;
+}
+
+@media (max-width: 62.25rem) {
+  .grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 1.5rem;
+  }
+  .container {
+    margin: 4rem auto 3rem;
+  }
+  .sectionTitle {
+    font-size: 1.9rem;
+  }
+}
+
+@media (max-width: 40rem) {
+  .grid {
+    grid-template-columns: 1fr;
+    gap: 1.25rem;
+  }
+  .container {
+    margin: 3rem auto 2rem;
+  }
+  .sectionTitle {
+    font-size: 1.7rem;
+  }
+  .sectionSubtitle {
+    font-size: 1.1rem;
+  }
+  .card {
+    padding: 1.75rem 1.5rem;
+  }
+}
diff --git a/src/components/HomepageFeatures/FeaturesGrid.tsx 
b/src/components/HomepageFeatures/FeaturesGrid.tsx
new file mode 100644
index 0000000..1a5091b
--- /dev/null
+++ b/src/components/HomepageFeatures/FeaturesGrid.tsx
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import styles from "./FeaturesGrid.module.css";
+
+interface FeatureItem {
+  title: string;
+  description: string;
+  icon: JSX.Element;
+}
+
+const FEATURES: FeatureItem[] = [
+  {
+    title: "Change Point Detection",
+    description: "Say goodbye to fragile static thresholds. Otava analyzes 
historical distributions to spot persistent performance shifts, ignoring 
temporary spikes and random noise.",
+    icon: (
+      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" 
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+        <path d="M3 3v18h18" />
+        <path d="M18.7 8l-5.1 5.2-2.8-2.7L7 14.3" />
+        <line x1="12" y1="5" x2="12" y2="19" strokeDasharray="3,3" />
+      </svg>
+    ),
+  },
+  {
+    title: "Feature Branch CI",
+    description: "Verify regressions before merging. Run benchmarks against 
your branch and compare the tail performance directly with your main branch 
baseline using the CLI.",
+    icon: (
+      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" 
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+        <line x1="6" y1="3" x2="6" y2="15" />
+        <circle cx="18" cy="6" r="3" />
+        <circle cx="6" cy="18" r="3" />
+        <path d="M18 9a9 9 0 0 1-9 9" />
+      </svg>
+    ),
+  },
+  {
+    title: "Flexible Data Ingestion",
+    description: "Ingest metrics directly from Graphite, PostgreSQL, Google 
BigQuery, or simple CSV files. Integrates smoothly with your existing 
benchmarking infrastructure.",
+    icon: (
+      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" 
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+        <ellipse cx="12" cy="5" rx="9" ry="3" />
+        <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
+        <path d="M3 12c0 1.66 4 3 9 3s9-1.34 9-3" />
+      </svg>
+    ),
+  },
+  {
+    title: "Slack & Grafana Alerts",
+    description: "Automatically annotate Grafana dashboards with detected 
change points and send Slack alerts when regressions are isolated to a specific 
commit range.",
+    icon: (
+      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" 
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+        <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
+        <path d="M13.73 21a2 2 0 0 1-3.46 0" />
+      </svg>
+    ),
+  },
+
+  {
+    title: "In-Browser Playground",
+    description: "Try before you install. Run Otava directly inside your 
browser on our interactive playground website to analyze sample performance 
datasets instantly.",
+    icon: (
+      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" 
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+        <polyline points="4 17 10 11 4 5" />
+        <line x1="12" y1="19" x2="20" y2="19" />
+      </svg>
+    ),
+  },
+];
+
+export default function FeaturesGrid(): JSX.Element {
+  return (
+    <div className={styles.container}>
+      <div className={styles.grid}>
+        {FEATURES.map((feature, idx) => (
+          <div key={idx} className={styles.card} id={`feature-card-${idx}`}>
+            <div className={styles.iconWrapper}>{feature.icon}</div>
+            <h3 className={styles.cardTitle}>{feature.title}</h3>
+            <p className={styles.cardDesc}>{feature.description}</p>
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+}
diff --git a/src/components/HomepageFeatures/FlowAnimation.module.css 
b/src/components/HomepageFeatures/FlowAnimation.module.css
new file mode 100644
index 0000000..4e182f7
--- /dev/null
+++ b/src/components/HomepageFeatures/FlowAnimation.module.css
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.wrap {
+  max-width: 56.25rem;
+  margin: -1.5rem auto 7.5rem;
+  padding: 0 1rem;
+}
+
+.svg {
+  width: 100%;
+  height: auto;
+  display: block;
+  overflow: visible;
+}
+
+.lane {
+  stroke: var(--ifm-color-emphasis-300);
+  stroke-width: 1.5;
+  stroke-dasharray: 4 4;
+}
+.laneAlert {
+  stroke: var(--otava-regress);
+  stroke-width: 1.5;
+  stroke-dasharray: 4 4;
+}
+
+.packetIn {
+  fill: var(--ifm-color-primary);
+}
+.packetAlert {
+  fill: var(--otava-regress);
+}
+
+.srcNode {
+  fill: var(--ifm-background-surface-color);
+  stroke: var(--ifm-color-primary);
+  stroke-width: 1.5;
+}
+
+.testNode {
+  fill: var(--ifm-background-surface-color);
+  stroke: var(--otava-accent2);
+  stroke-width: 1.5;
+}
+
+.nodeLabel {
+  fill: var(--ifm-font-color-base);
+  font-size: 13px;
+  font-weight: 600;
+  text-anchor: middle;
+  font-family: var(--ifm-font-family-base);
+}
+
+.engine {
+  fill: var(--ifm-color-primary);
+  filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.2));
+}
+.engineAlert {
+  fill: var(--otava-regress);
+  filter: drop-shadow(0 0 14px rgba(181, 71, 74, 0.5));
+}
+.engineName {
+  fill: #fff;
+  font-size: 18px;
+  font-weight: 700;
+  text-anchor: middle;
+}
+.engineSub {
+  fill: #fff;
+  font-size: 10px;
+  opacity: 0.9;
+  text-anchor: middle;
+  font-family: var(--ifm-font-family-monospace);
+}
+.engineSubAlert {
+  fill: #fff;
+  font-size: 10px;
+  font-weight: 700;
+  text-anchor: middle;
+  font-family: var(--ifm-font-family-monospace);
+}
+
+.alertNode {
+  fill: var(--ifm-background-surface-color);
+  stroke: var(--otava-regress);
+  stroke-width: 1.5;
+}
+.alertLabel {
+  fill: var(--otava-regress);
+  font-size: 12px;
+  font-weight: 700;
+  text-anchor: middle;
+  font-family: var(--ifm-font-family-base);
+}
+.actionNode {
+  fill: rgba(138, 63, 67, 0.1);
+  stroke: var(--otava-star);
+  stroke-width: 1.5;
+}
+.actionLabel {
+  fill: var(--otava-star);
+  font-size: 12px;
+  font-weight: 700;
+  text-anchor: middle;
+  font-family: var(--ifm-font-family-base);
+}
+
+.caption {
+  margin: 0.5rem auto 0;
+  text-align: center;
+  font-size: 0.9rem;
+  color: var(--ifm-color-emphasis-700);
+  max-width: 35rem;
+  min-height: 2.4em;
+}
diff --git a/src/components/HomepageFeatures/FlowAnimation.tsx 
b/src/components/HomepageFeatures/FlowAnimation.tsx
new file mode 100644
index 0000000..9c3dd12
--- /dev/null
+++ b/src/components/HomepageFeatures/FlowAnimation.tsx
@@ -0,0 +1,321 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useState } from "react";
+import {
+  AnimatePresence,
+  motion,
+  useMotionValueEvent,
+  useReducedMotion,
+  useTime,
+  useTransform,
+} from "motion/react";
+import styles from "./FlowAnimation.module.css";
+
+/**
+ * Banner animation telling the operational story:
+ *   - data is recorded *continuously* from every source into Otava
+ *   - most of the time nothing happens — Otava just records & analyzes
+ *   - only when a real change-point is detected does Otava raise an ALERT
+ *     (to Slack) and surface an action: revert the offending commit
+ */
+
+// SVG geometry
+const W = 880;
+const H = 248;
+
+// New Performance Test Node on the left
+const TEST_X = 24;
+const TEST_W = 140;
+const NODE_H = 34;
+
+// Data Sources shifted to the right
+const SRC_X = 236;
+const SRC_W = 96;
+
+// Otava box shifted to the right
+const BOX_X = 486;
+const BOX_W = 120;
+const BOX_Y = 92;
+const BOX_H = 64;
+const CX = BOX_X + BOX_W / 2;
+const CY = BOX_Y + BOX_H / 2;
+
+// Output nodes on the far right
+const OUT_X = W - 24 - 120; // 880 - 24 - 120 = 736
+const OUT_W = 120;
+
+const SOURCES = ["CSV", "PostgreSQL", "BigQuery", "Graphite"];
+
+const srcY = (i: number) => 40 + i * 56; // 40, 96, 152, 208
+const ALERT_Y = 104;
+const ACTION_Y = 168;
+
+const testFrom = { x: TEST_X + TEST_W, y: CY };
+const testTo = (i: number) => ({ x: SRC_X, y: srcY(i) });
+const inFrom = (i: number) => ({ x: SRC_X + SRC_W, y: srcY(i) });
+const inTo = { x: BOX_X, y: CY };
+const outFrom = { x: BOX_X + BOX_W, y: CY };
+const alertTo = { x: OUT_X, y: ALERT_Y };
+
+const CYCLE = 1.6; // seconds for an in-packet to traverse a lane
+const PACKETS_PER_LANE = 2;
+
+const LOOP_MS = 9000;
+
+// A continuously-streaming data packet (always flowing = constant recording).
+function InPacket({
+  from,
+  to,
+  delay,
+}: {
+  from: { x: number; y: number };
+  to: { x: number; y: number };
+  delay: number;
+}) {
+  return (
+    <motion.circle
+      r={4}
+      className={styles.packetIn}
+      initial={{ cx: from.x, cy: from.y, opacity: 0 }}
+      animate={{
+        cx: [from.x, to.x],
+        cy: [from.y, to.y],
+        opacity: [0, 1, 1, 0],
+      }}
+      transition={{ duration: CYCLE, ease: "linear", repeat: Infinity, delay }}
+    />
+  );
+}
+
+export default function FlowAnimation(): JSX.Element {
+  const reduce = useReducedMotion();
+
+  const time = useTime();
+  const progress = useTransform(time, (t) =>
+    reduce ? 0.75 : (t % LOOP_MS) / LOOP_MS,
+  );
+  const [p, setP] = useState(reduce ? 0.75 : 0);
+  useMotionValueEvent(progress, "change", setP);
+
+  // Most of the loop = quietly recording. A change is "detected" late in the
+  // loop, which fires the alert + action.
+  const alerting = p >= 0.55 && p < 0.92;
+  const status = alerting ? "change detected" : "recording…";
+
+  return (
+    <motion.div
+      className={styles.wrap}
+      initial={{ opacity: 0, y: 16 }}
+      animate={{ opacity: 1, y: 0 }}
+      transition={{ duration: 0.6, ease: "easeOut" }}
+    >
+      <svg
+        viewBox={`0 0 ${W} ${H}`}
+        className={styles.svg}
+        role="img"
+        aria-label="Performance tests run and publish metrics like CPU and 
throughput to CSV, PostgreSQL, BigQuery, or Graphite. Otava continuously 
ingests and monitors these sources, alerting Slack only when a real regression 
is detected."
+      >
+        {/* test-to-source lanes */}
+        {SOURCES.map((_, i) => {
+          const t = testTo(i);
+          return (
+            <line
+              key={`tl${i}`}
+              x1={testFrom.x}
+              y1={testFrom.y}
+              x2={t.x}
+              y2={t.y}
+              className={styles.lane}
+            />
+          );
+        })}
+
+        {/* source-to-otava lanes */}
+        {SOURCES.map((_, i) => {
+          const f = inFrom(i);
+          return (
+            <line
+              key={`il${i}`}
+              x1={f.x}
+              y1={f.y}
+              x2={inTo.x}
+              y2={inTo.y}
+              className={styles.lane}
+            />
+          );
+        })}
+        {/* alert lane — only appears once Otava turns red (a change is 
detected) */}
+        {alerting && (
+          <line
+            x1={outFrom.x}
+            y1={outFrom.y}
+            x2={alertTo.x}
+            y2={alertTo.y}
+            className={styles.laneAlert}
+          />
+        )}
+
+        {/* continuous inbound data packets */}
+        {!reduce &&
+          SOURCES.map((_, i) =>
+            Array.from({ length: PACKETS_PER_LANE }).map((__, k) => (
+              <g key={`packets-${i}-${k}`}>
+                {/* 1. from performance tests to data source */}
+                <InPacket
+                  from={testFrom}
+                  to={testTo(i)}
+                  delay={i * 0.16 + (k * CYCLE) / PACKETS_PER_LANE}
+                />
+                {/* 2. from data source to Otava (offset by half a cycle for 
smooth continuity) */}
+                <InPacket
+                  from={inFrom(i)}
+                  to={inTo}
+                  delay={i * 0.16 + (k * CYCLE) / PACKETS_PER_LANE + CYCLE / 2}
+                />
+              </g>
+            )),
+          )}
+
+        {/* alert packet — only emitted while alerting */}
+        <AnimatePresence>
+          {alerting && !reduce && (
+            <motion.circle
+              r={5}
+              className={styles.packetAlert}
+              initial={{ cx: outFrom.x, cy: outFrom.y, opacity: 0 }}
+              animate={{
+                cx: [outFrom.x, alertTo.x],
+                cy: [outFrom.y, alertTo.y],
+                opacity: [0, 1, 1, 1],
+              }}
+              transition={{ duration: 0.9, ease: "easeOut", repeat: Infinity }}
+            />
+          )}
+        </AnimatePresence>
+
+        {/* performance tests node */}
+        <g>
+          <rect
+            x={TEST_X}
+            y={CY - NODE_H / 2}
+            width={TEST_W}
+            height={NODE_H}
+            rx={7}
+            className={styles.testNode}
+          />
+          <text x={TEST_X + TEST_W / 2} y={CY + 4} 
className={styles.nodeLabel}>
+            Performance Tests
+          </text>
+        </g>
+
+        {/* source nodes */}
+        {SOURCES.map((name, i) => (
+          <g key={name}>
+            <rect
+              x={SRC_X}
+              y={srcY(i) - NODE_H / 2}
+              width={SRC_W}
+              height={NODE_H}
+              rx={7}
+              className={styles.srcNode}
+            />
+            <text x={SRC_X + SRC_W / 2} y={srcY(i) + 4} 
className={styles.nodeLabel}>
+              {name}
+            </text>
+          </g>
+        ))}
+
+        {/* Otava engine — flashes on detection */}
+        <motion.rect
+          x={BOX_X}
+          y={BOX_Y}
+          width={BOX_W}
+          height={BOX_H}
+          rx={12}
+          className={alerting ? styles.engineAlert : styles.engine}
+          animate={alerting ? { scale: [1, 1.04, 1] } : { scale: 1 }}
+          transition={{ duration: 0.6, repeat: alerting ? Infinity : 0 }}
+          style={{ transformBox: "fill-box", transformOrigin: "center" }}
+        />
+        <text x={CX} y={CY - 6} className={styles.engineName}>
+          Otava
+        </text>
+        <AnimatePresence mode="wait">
+          <motion.text
+            key={status}
+            x={CX}
+            y={CY + 13}
+            className={alerting ? styles.engineSubAlert : styles.engineSub}
+            initial={{ opacity: 0 }}
+            animate={{ opacity: 1 }}
+            exit={{ opacity: 0 }}
+          >
+            {status}
+          </motion.text>
+        </AnimatePresence>
+
+        {/* alert + action nodes (appear on detection) */}
+        <AnimatePresence>
+          {alerting && (
+            <motion.g
+              initial={{ opacity: 0, x: -8 }}
+              animate={{ opacity: 1, x: 0 }}
+              exit={{ opacity: 0 }}
+              transition={{ type: "spring", stiffness: 240, damping: 22 }}
+            >
+              {/* Slack alert */}
+              <rect
+                x={OUT_X}
+                y={ALERT_Y - NODE_H / 2}
+                width={OUT_W}
+                height={NODE_H}
+                rx={7}
+                className={styles.alertNode}
+              />
+              <text x={OUT_X + OUT_W / 2} y={ALERT_Y + 4} 
className={styles.alertLabel}>
+                ⚠ Slack alert
+              </text>
+
+              {/* recommended action */}
+              <rect
+                x={OUT_X}
+                y={ACTION_Y - NODE_H / 2}
+                width={OUT_W}
+                height={NODE_H}
+                rx={7}
+                className={styles.actionNode}
+              />
+              <text x={OUT_X + OUT_W / 2} y={ACTION_Y + 4} 
className={styles.actionLabel}>
+                ↩ Revert PR
+              </text>
+            </motion.g>
+          )}
+        </AnimatePresence>
+      </svg>
+
+      <p className={styles.caption}>
+        {alerting
+          ? "Persistent regression detected in metrics (e.g., CPU or 
throughput) → Otava alerts Slack and pinpoints the bad commit."
+          : "Performance tests run continuously, publishing metrics (like CPU 
and throughput) to data sources. Otava pulls and analyzes them."}
+      </p>
+    </motion.div>
+  );
+}
+
diff --git a/src/components/HomepageFeatures/index.tsx 
b/src/components/HomepageFeatures/index.tsx
index 15d3ce8..34b0fcf 100644
--- a/src/components/HomepageFeatures/index.tsx
+++ b/src/components/HomepageFeatures/index.tsx
@@ -19,13 +19,17 @@
 
 import clsx from "clsx";
 import styles from "./styles.module.css";
+import ChangePointDemo, { DEMOS } from "./ChangePointDemo";
+import FlowAnimation from "./FlowAnimation";
+import FeaturesGrid from "./FeaturesGrid";
 
 export default function HomepageFeatures(): JSX.Element {
   return (
     <section className={styles.features}>
       <div className="container">
+        <FlowAnimation />
         <div className={clsx("text--center", "padding-horiz--md")}>
-          <h2>Change Detection for Continuous Performance Engineering</h2>
+          <h2 className={styles.title}>Change Detection for Continuous 
Performance Engineering</h2>
           <p className={styles.description}>
             Otava performs statistical analysis of performance test results
             stored in CSV files, PostgreSQL, BigQuery, or Graphite database. It
@@ -53,7 +57,13 @@ export default function HomepageFeatures(): JSX.Element {
             </a>
           </div>
         </div>
+        <div className={styles.demoGrid}>
+          <ChangePointDemo config={DEMOS.REGRESSION} />
+          <ChangePointDemo config={DEMOS.STABLE} />
+        </div>
+        <FeaturesGrid />
       </div>
     </section>
   );
 }
+
diff --git a/src/components/HomepageFeatures/styles.module.css 
b/src/components/HomepageFeatures/styles.module.css
index 9657b9e..3e4352d 100644
--- a/src/components/HomepageFeatures/styles.module.css
+++ b/src/components/HomepageFeatures/styles.module.css
@@ -21,10 +21,25 @@
     padding: 2rem 0;
     width: 100%;
 }
+
+.title {
+    font-size: clamp(2.125rem, 5.4vw, 4rem);
+    line-height: 1.04;
+    font-weight: 700;
+    letter-spacing: -0.02em;
+    background: var(--otava-aurora);
+    -webkit-background-clip: text;
+    background-clip: text;
+    color: transparent;
+    margin-bottom: 1.4rem;
+}
+
 .description {
-    max-width: 800px;
+    max-width: 50rem;
     margin: 0 auto 2rem;
-    font-size: 1.2rem;
+    font-size: clamp(1.0625rem, 2.1vw, 1.3125rem);
+    color: var(--ifm-color-emphasis-700);
+    font-weight: 400;
     line-height: 1.6;
 }
 
@@ -33,4 +48,20 @@
     align-items: center;
     justify-content: center;
     gap: 1rem;
+    margin-top: 2.5rem;
+    margin-bottom: 6rem;
+}
+
+.demoGrid {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 1.5rem;
+    max-width: 68.75rem;
+    margin: 2.5rem auto 0;
+}
+
+@media (max-width: 56.25rem) {
+    .demoGrid {
+        grid-template-columns: 1fr;
+    }
 }
diff --git a/src/css/custom.css b/src/css/custom.css
index d1ef6c4..754871f 100644
--- a/src/css/custom.css
+++ b/src/css/custom.css
@@ -17,15 +17,119 @@
  * under the License.
  */
 
+/* ─────────────────────────────────────────────────────────────
+   Otava — Scandinavian teal theme.
+   Cool teal brand with a single brick accent over a low-chroma
+   neutral field. Usage balance per view: 60% neutral, 30% teal,
+   10% brick. Primitives below are the source of truth; the Infima
+   (--ifm-*) and Otava (--otava-*) roles reference them.
+   Typography: Space Grotesk (display) · Manrope (body) · JetBrains Mono.
+   ───────────────────────────────────────────────────────────── */
+
 :root {
-  --ifm-color-primary: #3071a9;
-  --ifm-code-font-size: 95%;
-  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
-  --ifm-footer-background-color:rgba(115, 150, 233, 0.61);
+  /* ── Design-token primitives ─────────────────────────────── */
+  /* Teal — brand scale (~30% of a view) */
+  --teal-50: #f0f7f6;
+  --teal-100: #cfe7e4;
+  --teal-300: #8bc1bd;
+  --teal-500: #3f8a86;
+  --teal-600: #2e6360;
+  --teal-900: #263b3a;
+  /* Neutral — holds the page (~60%) */
+  --neutral-0: #ffffff;
+  --neutral-50: #f6f7f7;
+  --neutral-100: #eaedec;
+  --neutral-200: #d4d8d7;
+  --neutral-400: #9aa1a0;
+  --neutral-600: #5f6766;
+  --neutral-900: #232b2b;
+  /* Brick — the one warm accent (~10%) */
+  --brick-100: #ecdedf;
+  --brick-500: #8a3f43;
+  --brick-700: #6e3033;
+  /* Status — kept distinct from the teal */
+  --status-success: #5e8c44;
+  --status-success-subtle: #eaf0e1;
+  --status-success-text: #3b5a29;
+  --status-warning: #c28a2e;
+  --status-warning-subtle: #f7ecd7;
+  --status-warning-text: #7c571b;
+  --status-error: #b5474a;
+  --status-error-subtle: #f7e0e1;
+  --status-error-text: #7a2c2e;
+
+  /* ── Infima theme wired to the palette ───────────────────── */
+  /* Teal primary */
+  --ifm-color-primary: var(--teal-500);
+  --ifm-color-primary-dark: #38807c;
+  --ifm-color-primary-darker: var(--teal-600);
+  --ifm-color-primary-darkest: var(--teal-900);
+  --ifm-color-primary-light: #5a9d99;
+  --ifm-color-primary-lighter: var(--teal-300);
+  --ifm-color-primary-lightest: var(--teal-100);
+
+  /* Typography */
+  --ifm-font-family-base: 'Manrope', -apple-system, BlinkMacSystemFont,
+    'Segoe UI', Roboto, sans-serif;
+  --ifm-heading-font-family: 'Space Grotesk', 'Manrope', -apple-system,
+    sans-serif;
+  --ifm-font-family-monospace: 'JetBrains Mono', ui-monospace, SFMono-Regular,
+    Menlo, Consolas, monospace;
+  --ifm-code-font-size: 92%;
+
+  /* Corner radii (design uses tight, consistent rounding) */
+  --ifm-global-radius: 0.625rem;
+  --ifm-button-border-radius: 0.5rem;
+  --ifm-card-border-radius: 0.8125rem;
+
+  /* Surfaces & text */
+  --ifm-background-color: var(--neutral-50);
+  --ifm-background-surface-color: var(--neutral-0);
+  --ifm-font-color-base: var(--neutral-900);
+  --ifm-heading-color: var(--neutral-900);
+  --ifm-navbar-background-color: rgba(246, 247, 247, 0.85);
+  --ifm-footer-background-color: var(--neutral-900);
+
+  --ifm-color-emphasis-200: var(--neutral-100);
+  --ifm-color-emphasis-300: var(--neutral-200);
+  --ifm-color-emphasis-500: var(--neutral-400);
+  --ifm-color-emphasis-700: var(--neutral-600);
+  --ifm-toc-border-color: var(--neutral-200);
+
+  /* Otava brand tokens → semantic roles */
+  --otava-accent2: var(--teal-300); /* secondary teal — diagram structure */
+  --otava-regress: var(--status-error); /* regression (bad) */
+  --otava-improve: var(--status-success); /* improvement (good) */
+  --otava-star: var(--brick-500); /* the one warm focused-action accent */
+  --otava-aurora: linear-gradient(
+    115deg,
+    var(--teal-600) 0%,
+    var(--teal-500) 48%,
+    var(--teal-300) 100%
+  );
+
+  --docusaurus-highlighted-code-line-bg: rgba(63, 138, 134, 0.1);
 }
 
+/* Harmonized dark mode (toggle is disabled by default, light is designed-for) 
*/
 [data-theme='dark'] {
-  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
+  --ifm-color-primary: var(--teal-300);
+  --ifm-color-primary-dark: #79b3af;
+  --ifm-color-primary-darker: var(--teal-500);
+  --ifm-color-primary-darkest: var(--teal-600);
+
+  --ifm-background-color: #1a201f;
+  --ifm-background-surface-color: var(--neutral-900);
+  --ifm-font-color-base: var(--neutral-100);
+  --ifm-heading-color: #ffffff;
+  --ifm-navbar-background-color: rgba(26, 32, 31, 0.85);
+  --ifm-footer-background-color: #151a19;
+
+  --ifm-color-emphasis-200: #2e3534;
+  --ifm-color-emphasis-300: #3c4443;
+  --ifm-toc-border-color: #2e3534;
+
+  --docusaurus-highlighted-code-line-bg: rgba(63, 138, 134, 0.18);
 }
 
 .navbar__logo img {
@@ -38,8 +142,8 @@
 
 .header-github-link::before {
   content: '';
-  width: 24px;
-  height: 24px;
+  width: 1.5rem;
+  height: 1.5rem;
   display: flex;
   background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' 
xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 
0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 
0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 
17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 
1.07 1.835 2.809 1.305 
3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 
0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.52 [...]
   no-repeat;
diff --git a/src/components/HomepageFeatures/styles.module.css b/src/global.d.ts
similarity index 75%
copy from src/components/HomepageFeatures/styles.module.css
copy to src/global.d.ts
index 9657b9e..36ebd0e 100644
--- a/src/components/HomepageFeatures/styles.module.css
+++ b/src/global.d.ts
@@ -17,20 +17,12 @@
  * under the License.
  */
 
-.features {
-    padding: 2rem 0;
-    width: 100%;
-}
-.description {
-    max-width: 800px;
-    margin: 0 auto 2rem;
-    font-size: 1.2rem;
-    line-height: 1.6;
-}
+import React from 'react';
 
-.buttons {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    gap: 1rem;
+declare global {
+  namespace JSX {
+    interface Element extends React.JSX.Element {}
+    interface ElementClass extends React.JSX.ElementClass {}
+    interface IntrinsicElements extends React.JSX.IntrinsicElements {}
+  }
 }
diff --git a/src/pages/index.module.css b/src/pages/index.module.css
index 55ce941..fe8204c 100644
--- a/src/pages/index.module.css
+++ b/src/pages/index.module.css
@@ -23,13 +23,52 @@
  */
 
 .heroBanner {
-  padding: 4rem 0;
+  padding: 10rem 0 4.5rem;
   text-align: center;
   position: relative;
   overflow: hidden;
+  color: var(--ifm-font-color-base);
+  /* soft teal wash with a single warm note over the neutral canvas */
+  background:
+    radial-gradient(
+      120% 140% at 50% -20%,
+      rgba(63, 138, 134, 0.18) 0%,
+      rgba(63, 138, 134, 0.1) 35%,
+      rgba(138, 63, 67, 0.06) 60%,
+      transparent 80%
+    ),
+    var(--ifm-background-color);
 }
 
-@media screen and (max-width: 996px) {
+/* faint particle field */
+.heroBanner::before {
+  content: "";
+  position: absolute;
+  inset: 0;
+  background-image:
+    radial-gradient(0.09375rem 0.09375rem at 12% 28%, rgba(63, 138, 134, 0.55) 
50%, transparent),
+    radial-gradient(0.09375rem 0.09375rem at 78% 18%, rgba(139, 193, 189, 0.6) 
50%, transparent),
+    radial-gradient(0.125rem 0.125rem at 64% 62%, var(--otava-star) 50%, 
transparent),
+    radial-gradient(0.0625rem 0.0625rem at 33% 72%, rgba(63, 138, 134, 0.45) 
50%, transparent),
+    radial-gradient(0.09375rem 0.09375rem at 88% 70%, rgba(139, 193, 189, 0.5) 
50%, transparent),
+    radial-gradient(0.0625rem 0.0625rem at 22% 50%, rgba(63, 138, 134, 0.5) 
50%, transparent),
+    radial-gradient(0.09375rem 0.09375rem at 50% 35%, var(--otava-star) 50%, 
transparent);
+  opacity: 0.55;
+  pointer-events: none;
+}
+
+.heroBanner :global(.hero__title) {
+  position: relative;
+  font-size: clamp(3.5rem, 9vw, 7.375rem);
+  line-height: 0.96;
+  letter-spacing: -0.03em;
+  background: var(--otava-aurora);
+  -webkit-background-clip: text;
+  background-clip: text;
+  color: transparent;
+}
+
+@media screen and (max-width: 62.25rem) {
   .heroBanner {
     padding: 2rem;
   }
@@ -40,3 +79,22 @@
   align-items: center;
   justify-content: center;
 }
+
+.heroSubtitle {
+  font-size: clamp(1.1875rem, 2.4vw, 1.5rem);
+  max-width: 46.875rem;
+  margin: 1.5rem auto 0;
+  opacity: 1;
+  font-weight: 400;
+  line-height: 1.5;
+  color: var(--ifm-color-emphasis-700);
+  position: relative;
+}
+
+@media screen and (max-width: 48rem) {
+  .heroSubtitle {
+    margin: 1rem auto 0;
+    padding: 0 1rem;
+  }
+}
+
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index d7f4c54..e11c9b8 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -28,6 +28,9 @@ export default function Home(): JSX.Element {
       <header className={clsx("hero", styles.heroBanner)}>
         <div className="container">
           <h1 className="hero__title">Apache Otava</h1>
+          <p className={styles.heroSubtitle}>
+            Continuous performance regression detection powered by statistical 
change point analysis.
+          </p>
         </div>
       </header>
       <main>
diff --git a/tsconfig.json b/tsconfig.json
index 314eab8..2da86e5 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,6 +2,9 @@
   // This file is not used in compilation. It is here just for a nice editor 
experience.
   "extends": "@docusaurus/tsconfig",
   "compilerOptions": {
-    "baseUrl": "."
+    "baseUrl": ".",
+    "ignoreDeprecations": "6.0",
+    "moduleResolution": "node",
+    "jsx": "react-jsx"
   }
 }

Reply via email to